成为协程的一部分,在取消之前继续进行

时间:2020-02-10 17:29:15

标签: android kotlin kotlin-coroutines

我有一个文件管理类,可以保存一个大文件。文件管理器类是一个应用程序单例,因此它比我的UI类寿命更长。我的活动/片段可以从协程调用文件管理器的save暂停功能,然后在UI中显示成功或失败。例如:

//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块中有任何暂停点,则保存将不会完成,因为协程将被取消。

我想发生的是该文件始终被保存。如果活动仍在进行中,那么我们可以在完成时显示UI更新。

我认为实现此目标的一种方法可能是像这样从suspend函数启动一个新的coroutineScope,但是当取消其父作业时,该作用域似乎仍然被取消。

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

我认为另一种选择可能是使它成为常规函数,以在完成后更新一些LiveData。 Activity可以观察结果的实时数据,并且由于LiveData在销毁生命周期观察者时会自动删除它们,因此Activity不会泄漏到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 个答案:

答案 0 :(得分:2)

您可以用NonCancellable包装不想取消的位。

// May cancel here.
withContext(Dispatchers.IO + NonCancellable) {
    // Will complete, even if cancelled.
}
// May cancel here.

答案 1 :(得分:1)

如果您的代码的生存期范围是整个应用程序的生存期,则这是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。不过,将UI通知与视图分离也是另一个主题。

actor被标记为“过时的协程API”,但这只是一个预先通知,在将来的Kotlin版本中,它将被功能更强大的替代品替代。这并不意味着它已损坏或不受支持。

答案 2 :(得分:0)

我尝试了这个,它似乎可以按照我的描述去做。 FileManager类具有自己的范围,尽管我想它也可以是GlobalScope,因为它是单例类。

我们从协程发起了一项新的工作。这是通过单独的功能完成的,以消除有关作业范围的任何歧义。我使用async 为此,我可以将UI应该响应的异常冒出来。

然后在启动后,我们等待异步作业恢复到原始范围。 await()暂停,直到作业完成并传递任何抛出(在我的情况下,我希望IOExceptions冒泡,以使UI显示错误消息)。因此,如果取消了原始范围,则它的协程将永远不会等待结果,但是启动的作业会一直滚动直到正常完成。我们要确保始终处理的任何异常都应在async函数中处理。否则,如果取消原​​始作业,它们将不会冒泡。

//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
        //...
    }
}