Сделать часть сопрограммы продолжить после отмены - PullRequest
3 голосов
/ 10 февраля 2020

У меня есть класс управления файлами, который может сохранить большой файл. Класс файлового менеджера является приложением-одиночкой, поэтому он переживает мои классы пользовательского интерфейса. Моя активность / фрагмент может вызывать save функцию приостановки файлового менеджера из сопрограммы, а затем отображать успех или неудачу в пользовательском интерфейсе. Например:

//In MyActivity:
private fun saveTheFile() = lifecycleScope.launch {
    try {
        myFileManager.saveBigFile()
        myTextView.text = "Successfully saved file"
    } catch (e: IOException) {
        myTextView.text = "Failed to save file"
    }
}

//In MyFileManager
suspend fun saveBigFile() {
    //Set up the parameters
    //...

    withContext(Dispatchers.IO) {
        //Save the file
        //...
    }
}

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

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

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

suspend fun saveBigFile() = coroutineScope {
    //...
}

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

//In MyActivity:
private fun saveTheFile() {
    val result = myFileManager.saveBigFile()
    result.observe(this@MyActivity) {
        myTextView.text = when (it) {
            true -> "Successfully saved file"
            else -> "Failed to save file"
        }
    }
}

//In MyFileManager
fun saveBigFile(): LiveData<Boolean> {
    //Set up the parameters
    //...
    val liveData = MutableLiveData<Boolean>()
    MainScope().launch {
        val success = withContext(Dispatchers.IO) {
            //Save the file
            //...
        }
        liveData.value = success
    }
    return liveData
}

Ответы [ 3 ]

2 голосов
/ 11 февраля 2020

Вы можете обернуть бит, который не хотите отменять, с помощью NonCancellable.

// May cancel here.
withContext(Dispatchers.IO + NonCancellable) {
    // Will complete, even if cancelled.
}
// May cancel here.
1 голос
/ 11 февраля 2020

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

По сути, вы можете сказать

@ObsoleteCoroutinesApi
val executor = GlobalScope.actor<() -> Unit>(Dispatchers.IO) {
    for (task in channel) {
        task()
    }
}

И использовать это так:

private fun saveTheFile() = lifecycleScope.launch {
    executor.send {
        try {
            myFileManager.saveBigFile()
            withContext(Main) {
                myTextView.text = "Successfully saved file"
            }
        } catch (e: IOException) {
            withContext(Main) {
                myTextView.text = "Failed to save file"
            }
        }
    }
}

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

actor помечена как «устаревший сопрограммный API», но это лишь предварительное уведомление о том, что оно будет заменено более мощной альтернативой. в будущей версии Kotlin. Это не значит, что он сломан или не поддерживается.

0 голосов
/ 11 февраля 2020

Я попробовал это, и он, кажется, делает то, что описал, что хотел. Класс FileManager имеет собственную область видимости, хотя я полагаю, что это также может быть GlobalScope, поскольку это одноэлементный класс.

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

Затем после запуска мы ожидаем, что задание asyn c вернется в исходную область. await() приостанавливается до тех пор, пока задание не будет выполнено, и не выполнит любые броски (в моем случае я хочу, чтобы всплыли исключения IOException, чтобы пользовательский интерфейс отображал сообщение об ошибке). Таким образом, если исходная область действия отменяется, ее сопрограмма никогда не ожидает результата, но запущенное задание продолжает выполняться, пока не завершится нормально. Любые исключения, которые мы хотим гарантировать, всегда обрабатываются, должны обрабатываться в функции asyn c. В противном случае они не всплывают, если исходное задание отменено.

//In MyActivity:
private fun saveTheFile() = lifecycleScope.launch {
    try {
        myFileManager.saveBigFile()
        myTextView.text = "Successfully saved file"
    } catch (e: IOException) {
        myTextView.text = "Failed to save file"
    }
}

class MyFileManager private constructor(app: Application):
    CoroutineScope by MainScope() {

    suspend fun saveBigFile() {
        //Set up the parameters
        //...

        val deferred = saveBigFileAsync()
        deferred.await()
    }

    private fun saveBigFileAsync() = async(Dispatchers.IO) {
        //Save the file
        //...
    }
}

...