Подходит ли модель Kotlin Structured Concurrency [сопрограммы] для пользовательского интерфейса для записи в БД?

Меня особенно беспокоит вставка данных, инициированных пользователем, в локальную базу данных.

Следующий шаблон преобладает в примерах (в том числе из официальных источников, например, JetBrains, Google / Android) для использования сопрограмм Kotlin в сочетании с моделями представления [Компоненты архитектуры Android].

class CoroutineScopedViewModel : ViewModel(), CoroutineScope {
    private val _job = Job()
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + _job

    override fun onCleared() {
        super.onCleared()
        _job.cancel()
    }

    fun thisIsCalledFromTheUI() = launch {
        /* do some UI stuff on the main thread */
        withContext(Dispatchers.IO) {
            try {
                /* do some IO, e.g. inserting into DB */
            } catch (error: IOException) {
                /* do some exception handling */
            }
        }
    }
}

Это мое понимание документации, что в приведенном выше примере сопрограммы, запущенные в контексте пользовательского интерфейса (определенном через coroutineContext), будут отменены, когда ViewModel будет уничтожен, но что код в блоке withContext(Dispatchers.IO) доберутся до завершения.

Но, прежде чем я перейду к рефакторингу своего проекта из (до 1.0.0) модели сопрограмм с глобальным охватом (запуск / асинхронность), я чувствую, что мне нужно просто прояснить некоторые вещи:

Правильно ли я прочитал документацию? Или уничтожение модели просмотра до того, как блок withContext(Dispatchers.IO) завершится, вызовет отмену и этого задания? Т.е. Можно ли использовать эту модель для вставки данных в мою БД, или может возникнуть какая-то странная проблема с синхронизацией, когда пользователь отвечает или иным образом заставляет владельца ViewModel закрыться, что в конечном итоге приводит к потере данных?

Я не хочу непреднамеренно вводить ошибку синхронизации, потому что я что-то неправильно понял и поэтому преобразовал свой код в модель, аналогичную показанной выше.

РЕДАКТИРОВАТЬ:

Итак, я решил провести небольшой тест, и мне кажется, что все эти примеры, использующие эту модель для записи в базу данных, могут содержать фундаментальную ошибку.

Модификация кода для регистрации происходящего, как такового:

class ChildViewModel : ViewModel(), CoroutineScope {
    private val _job = Job()
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + _job

    override fun onCleared() {
        super.onCleared()
        Log.d("onCleared", "Start")
        _job.cancel()
        Log.d("onCleared", "End")
    }

    fun thisIsCalledFromTheUI() = launch {
        Log.d("thisIsCalledFromTheUI", "Start")
        GlobalScope.launch(Dispatchers.IO) {
            Log.d("GlobalScope", "Start")
            delay(15000)
            Log.d("GlobalScope", "End")
        }
        withContext(Dispatchers.IO) {
            Log.d("withContext", "Start")
            delay(10000)
            Log.d("withContext", "End")
        }
        Log.d("thisIsCalledFromTheUI", "End")
    }
}

В результате, если вы дадите ему поработать до конца:

D/thisIsCalledFromTheUI: Start
D/GlobalScope: Start
D/withContext: Start
D/withContext: End
D/thisIsCalledFromTheUI: End
D/GlobalScope: End

Но если вы закроете Fragment / Activity (не приложение) до завершения withContext, вы получите следующее:

D/thisIsCalledFromTheUI: Start
D/GlobalScope: Start
D/withContext: Start
D/GlobalScope: End

Это указывает, по крайней мере, для меня, что вы не можете использовать это для записи непереходных данных в БД.


person Andre Artus    schedule 16.11.2018    source источник


Ответы (1)


Насколько я понимаю из документации, в приведенном выше примере сопрограммы, запущенные в контексте пользовательского интерфейса (определенном через coroutineContext), будут отменены, когда ViewModel будет уничтожен, но что код в блоке withContext(Dispatchers.IO) будет выполняться до завершения.

Это неправильное чтение документации. withContext не запускает другую сопрограмму, он просто изменяет контекст текущей сопрограммы на время ее блока. Следовательно, эта сопрограмма будет отменена, как и все другие сопрограммы, которые вы запускаете без предоставления нового родительского контекста, с которым связано другое задание (или вообще без задания, как GlobalScope).

Однако предложенная вами идея использовать GlobalScope для постоянных операций - это просто локальный патч для тестируемого вами сценария, вы по-прежнему не получаете гарантии, что он будет работать до конца. Пользователь может полностью выйти из приложения, а Android может убить процесс.

Следовательно, если вашей целью является создание действительно надежного приложения, вы должны учитывать тот факт, что до завершения сопрограммы никакая информация не записывалась в БД. Надеюсь, вы запустите операцию в транзакции БД, которая автоматически откатится, если ваша программа будет убита, иначе будет невозможно предотвратить несоответствия.

person Marko Topolnik    schedule 16.11.2018
comment
Спасибо, это имеет смысл, но почему так много примеров его используют? На практике я ограничиваю ввод-вывод, записывая в базу данных и отправляя ее в серверную часть (действительно трудоемкая операция) с использованием структуры AAC WorkManager. Да, каждая соответствующая операция БД (вставка, обновление) выполняется в транзакции. Выход из приложения меня меньше беспокоит, так как это то, что пользователь, вероятно, заметит и с этим справится. Пользователю может потребоваться день в пути, чтобы получить единственную систему координат, не нужно слишком быстрое нажатие назад или finish, чтобы потребовать повторения (если это останется незамеченным, пока он не вернется). - person Andre Artus; 16.11.2018
comment
Это предложение из документации, которое меня смущает: длительная сопрограмма, выполняющая некоторый ввод-вывод или фоновые вычисления, может сохранять ссылки на соответствующие элементы пользовательского интерфейса дольше, чем это необходимо, предотвращая сборку мусора целых деревьев объектов пользовательского интерфейса. которые уже были уничтожены и больше не будут отображаться. Создается впечатление, что нужно избавиться только от ненужных элементов пользовательского интерфейса, а не от операции ввода-вывода. - person Andre Artus; 16.11.2018
comment
В примерах используется GlobalScope, потому что он для этого --- автономные примеры, которые можно запускать автономно. Я взял за правило никогда не отвечать на вопросы здесь, используя GlobalScope, независимо от того, насколько прост код, который я хочу показать. - person Marko Topolnik; 16.11.2018
comment
Если ваша операция БД в конечном итоге не взаимодействует с графическим интерфейсом пользователя в инициирующем действии, вам не нужно привязывать ее к ее жизненному циклу. В этом случае вы можете ввести область на уровне приложения, привязанную к жизненному циклу YouApplication. Это все равно лучше, чем GlobalScope. Это позволяет вам указать Dispatchers.Main в качестве диспетчера по умолчанию. - person Marko Topolnik; 16.11.2018
comment
Однако у вас по-прежнему будет проблема с удержанием всего дерева графического интерфейса пользователя несуществующей деятельности. Постарайтесь избежать этого, сведя к минимуму объем кода внутри сопрограммы, которую вы launch. Не используйте в нем какие-либо локальные переменные, которые ссылаются (прямо или косвенно) на графический интерфейс. - person Marko Topolnik; 16.11.2018
comment
Извините, я имел в виду, что многие (большинство?) ViewModel конкретных примеров (которые я видел) используют подход withContext(Dispatchers.IO) (не Global.launch). Мне это кажется потенциальной ошибкой. - person Andre Artus; 16.11.2018
comment
Если данные подтверждаются, я записываю их в БД, а также закрываю фрагмент (через сообщение LiveData от виртуальной машины). Пользовательский интерфейс (вызывающий фрагмент) обновляется посредством уведомления (LiveData + Room). Сообщения, происходящие из исключений, отправляются в пользовательский интерфейс. - person Andre Artus; 16.11.2018
comment
Dispatchers.IO предназначен исключительно для выполнения блокирующих вызовов API. Имеет смысл обновлять ViewModel по результатам такого вызова, но в остальном это бессмысленно и неправильно. - person Marko Topolnik; 16.11.2018