Как я могу гарантировать получение последних данных при использовании Coroutine в Kotlin? - PullRequest
2 голосов
/ 22 апреля 2020

Код A взят из примеров архитектуры проекта, его можно увидеть здесь .

updateTasksFromRemoteDataSource() - это функция приостановки, поэтому она может выполняться асинхронно.

Когда я вызываю функцию getTasks(forceUpdate: Boolean) с параметром True, я боюсь, что return tasksLocalDataSource.getTasks() будет запущен раньше, чем updateTasksFromRemoteDataSource().

Я не знаю, может ли код B гарантировать return tasksLocalDataSource.getTasks() сработает после updateTasksFromRemoteDataSource().

Код A

class DefaultTasksRepository(
    private val tasksRemoteDataSource: TasksDataSource,
    private val tasksLocalDataSource: TasksDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksRepository {

   override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        // Set app as busy while this function executes.
        wrapEspressoIdlingResource {

            if (forceUpdate) {
                try {
                    updateTasksFromRemoteDataSource()
                } catch (ex: Exception) {
                    return Result.Error(ex)
                }
            }
            return tasksLocalDataSource.getTasks()
        }
   }

   private suspend fun updateTasksFromRemoteDataSource() {
        val remoteTasks = tasksRemoteDataSource.getTasks()

        if (remoteTasks is Success) {
            // Real apps might want to do a proper sync, deleting, modifying or adding each task.
            tasksLocalDataSource.deleteAllTasks()
            remoteTasks.data.forEach { task ->
                tasksLocalDataSource.saveTask(task)
            }
        } else if (remoteTasks is Result.Error) {
            throw remoteTasks.exception
        }
    }
   ...

}

Код B

class DefaultTasksRepository(
    private val tasksRemoteDataSource: TasksDataSource,
    private val tasksLocalDataSource: TasksDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksRepository {

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        // Set app as busy while this function executes.
        wrapEspressoIdlingResource {
            coroutineScope {
                if (forceUpdate) {
                    try {
                        updateTasksFromRemoteDataSource()
                    } catch (ex: Exception) {
                        return Result.Error(ex)
                    }
                }
            }
            return tasksLocalDataSource.getTasks()
        }
    }

    ...    

}

Добавленный контент

To Tenfour04: Спасибо!

Если кто-то реализует updateTasksFromRemoteDataSource() с lauch точно так же, как Code C, вы уверены, что Code C is return tasksLocalDataSource.getTasks() сработает после updateTasksFromRemoteDataSource(), когда я вызову функцию getTasks(forceUpdate: Boolean) с параметром True?

Код C

 class DefaultTasksRepository(
        private val tasksRemoteDataSource: TasksDataSource,
        private val tasksLocalDataSource: TasksDataSource,
        private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
    ) : TasksRepository {

       override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
            // Set app as busy while this function executes.
            wrapEspressoIdlingResource {

                if (forceUpdate) {
                    try {
                        updateTasksFromRemoteDataSource()
                    } catch (ex: Exception) {
                        return Result.Error(ex)
                    }
                }
                return tasksLocalDataSource.getTasks()
            }
       }

      private suspend fun updateTasksFromRemoteDataSource() {
            val remoteTasks = tasksRemoteDataSource.getTasks()

            if (remoteTasks is Success) {
                // Real apps might want to do a proper sync, deleting, modifying or adding each task.
                tasksLocalDataSource.deleteAllTasks()
                launch {                                          //I suppose that launch can be fired
                    remoteTasks.data.forEach { task ->
                        tasksLocalDataSource.saveTask(task)
                    }
                }

            } else if (remoteTasks is Result.Error) {
                throw remoteTasks.exception
            }
        }
   }   

Новый добавленный контент

Джоффри: Спасибо!

Я думаю, что код D. можно скомпилировать.

В этом случае, когда forceUpdate верно, tasksLocalDataSource.getTasks() может быть выполнено до updateTasksFromRemoteDataSource().

код D

class DefaultTasksRepository(
    private val tasksRemoteDataSource: TasksDataSource,
    private val tasksLocalDataSource: TasksDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
    private val myCoroutineScope: CoroutineScope
) : TasksRepository {

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        // Set app as busy while this function executes.
        wrapEspressoIdlingResource {

            if (forceUpdate) {
                try {
                    updateTasksFromRemoteDataSource(myCoroutineScope)
                } catch (ex: Exception) {
                    return Result.Error(ex)
                }
            }
            return tasksLocalDataSource.getTasks()
        }
    }

    private suspend fun updateTasksFromRemoteDataSource(myCoroutineScope: CoroutineScope) {
        val remoteTasks = tasksRemoteDataSource.getTasks()

        if (remoteTasks is Success) {
            // Real apps might want to do a proper sync, deleting, modifying or adding each task.
            tasksLocalDataSource.deleteAllTasks()
            myCoroutineScope.launch {
                remoteTasks.data.forEach { task ->
                    tasksLocalDataSource.saveTask(task)
                }
            }

        } else if (remoteTasks is Result.Error) {
            throw remoteTasks.exception
        }
    }

    ...
}

1 Ответ

4 голосов
/ 28 апреля 2020

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

Это означает, что с кодом A все в порядке (когда forceUpdate истинно , tasksLocalDataSource.getTasks() никогда не запустится до того, как updateTasksFromRemoteDataSource() будет выполнено), и coroutineScope в коде B. не требуется.

Теперь в отношении кода C структурированный параллелизм здесь, чтобы спасти вас. Люди просто не могут позвонить launch без CoroutineScope получателя. Поскольку TaskRepository не расширяет CoroutineScope, код C как есть не будет скомпилирован.

Хотя есть два способа сделать эту компиляцию:

  1. Использование GlobalScope.launch {}: это действительно вызовет ожидаемую проблему. Тело такого launch будет работать асинхронно и независимо от вызывающего. updateTasksFromRemoteDataSource может в этом случае вернуться до того, как тело launch будет сделано. Единственный способ контролировать это - использовать .join() на Job, возвращаемом вызовом на launch (который ждет, пока это не будет сделано). Вот почему обычно не рекомендуется использовать GlobalScope, потому что он может «пропускать» сопрограммы.

  2. , обертывающий вызовы к launch в coroutineScope {...} внутри updateTasksFromRemoteDataSource , Это обеспечит завершение всех сопрограмм, запущенных в блоке coroutineScope, до завершения вызова coroutineScope. Обратите внимание, что все, что внутри блока coroutineScope может очень хорошо работать одновременно, хотя, в зависимости от того, как используется launch / async, но в этом весь смысл использования launch в Во-первых, не так ли?

Теперь с Кодом D мой ответ для кода C вроде как все еще сохраняется. Независимо от того, передаете ли вы область или используете GlobalScope, вы фактически создаете сопрограммы с большим жизненным циклом, чем функция приостановки, которая их запускает. Следовательно, это создает проблему, которой вы боитесь. Но зачем вам передавать CoroutineScope, если вы не хотите, чтобы разработчики запускали долгоживущие сопрограммы в предоставленной области?

Если вы этого не сделаете, разработчик вряд ли будет использовать GlobalScope (или любой объем), чтобы сделать это. Обычно плохой стиль - создавать долгоживущие сопрограммы из функции приостановки. Если ваша функция приостанавливается, вызывающие обычно ожидают, что после ее завершения она фактически выполнила свою работу.

...