【问题标题】:Android LiveData and Coroutines - is this an anti-pattern?Android LiveData 和 Coroutines - 这是一种反模式吗?
【发布时间】:2021-05-04 15:50:46
【问题描述】:

我从一位前开发人员那里继承了一个 Kotlin Android 项目。例如,他使用挂起函数来处理网络请求。这种函数的一个例子可能是这样的:

suspend fun performNetworkCall(param1: Long, param2: String): ResultOfCall
{
   Do()
   The()
   Stuff()

   return theResult
}

到目前为止,一切都很好。现在他的片段有ViewModels,并且他在这些模型中也有方法应该异步调用上述挂起函数并返回一些结果。他是这样做的:

sealed class LiveDataResult<out R>
{
    data class Success<T>(val result: T) : LiveDataResult<T>()
    data class Failure(val errorMessage: String) : LiveDataResult<Nothing>()
}


fun fetchSomeData(param1: Long, param2: String): LiveData<LiveDataResult<String>> = liveData {
    val resultOfCall = performNetworkCall(param1, param2)
    if (resultOfCall indicates success)
        emit(LiveDataResult.Success("Yay!")
    else
        emit(LiveDataResult.Failure("Oh No!")
}

在他的片段中,他将这样的方法称为

viewModel.fetchSomeData(0, "Call Me").observe(viewLifecycleOwner) {
    when (it)
    {
        is LiveDataResult.Success -> DoSomethingWith(it.result)
        is LiveDataResult.Failure -> HandleThe(it.errorMessage)
    }
}

我对整个可观察/协程问题还不是很有经验,所以我对这种方法的问题是:

  1. 这会不会堆积一大堆 LiveData 对象,由于观察者仍处于附加状态而无法释放?
  2. 这是一种不好的方法吗?糟糕到需要重构?如果需要重构,应该如何重构?

【问题讨论】:

    标签: kotlin android-livedata kotlin-coroutines suspend


    【解决方案1】:

    我不是这方面的专家,所以我只是根据浏览源代码对协程和 LiveData 如何工作的理解。

    一个典型的 LiveData 并不能通过观察者保持活跃。它就像任何其他典型的对象一样,通过被引用保持活力。

    CoroutineLiveData,然而,一旦启动,将通过其协程继续保持活动状态。我认为协程系统通过挂起函数保持对对象的强引用,直到挂起函数返回并且可以删除延续。因此,fetchSomeData 函数创建的 LiveData 的每个实例都将运行到完成,即使观察者已经达到生命的尽头。当网络调用完成时,没有任何东西可以保存对 LiveData 的引用,因此应该从内存中清除它。

    因此,只是发生了暂时的泄漏。如果发出请求的 Fragment 在收到结果之前关闭,您的网络调用不会被取消。这是因为 CoroutineLiveData 使用自己的内部 CoroutineScope,它不依赖于任何生命周期。如果您多次重新打开 Fragment,例如通过旋转屏幕,您可能有多个过时的网络请求仍在运行。 There are apparently work-arounds to manually cancel, but it's kind of messy.

    另外,在我看来,当您可以直接调用挂起函数时,使用 LiveData 获取单个结果只是增加了额外的复杂性并牺牲了自动取消。现代版本的库(如 Retrofit)已经具有用于发出请求的挂起函数,因此如果在与生命周期相关的 CoroutineScope 上调用挂起函数,则会自动取消网络请求。

    支持自动取消的代码的重构版本可能如下所示:

    suspend fun performNetworkCall(param1: Long, param2: String): ResultOfCall
    {
       val result = setUpAndDoSomeRetrofitSuspendFunctionCall(param1, param2)
       return result
    }
    
    sealed class NetworkResult<out R>
    {
        data class Success<T>(val result: T) : NetworkResult<T>()
        data class Failure(val errorMessage: String) : NetworkResult<Nothing>()
    }
    
    suspend fun fetchSomeData(param1: Long, param2: String): NetworkResult<String> 
    {
        val resultOfCall = performNetworkCall(param1, param2)
        return if (resultOfCall indicates success)
            NetworkResult.Success("Yay!")
        else
            NetworkResult.Failure("Oh No!")
    }
    
    // In Fragment:
    lifecycleScope.launchWhenStarted 
    {
        when (val result = viewModel.fetchSomeData(0, "Call Me"))
        {
            is NetworkResult.Success -> DoSomethingWith(result.result)
            is NetworkResult.Failure -> HandleThe(result.errorMessage)
        }
    }
    

    【讨论】:

    • 非常感谢您的详细解答!到目前为止,我已经尝试了您建议的一次通话,它似乎有效 - 只是我将 launchWhenStarted 替换为 launch,因为大多数内容将在用户操作时执行,而不是在片段启动时执行。我会检查是否可以相应地替换其他调用。
    • 如果你正在修改 UI 组件或访问 Context/Activity,你需要使用launchWhenStartedlaunchWhenResumed 来确保你的协程的主线程部分不会在 Fragment 运行时发生处于分离状态。
    • 啊,我明白了,我解释像 launchWhenStarted 这样的文档会推迟执行直到状态改为 STARTED...
    猜你喜欢
    • 2014-07-31
    • 1970-01-01
    • 1970-01-01
    • 2023-03-11
    • 1970-01-01
    • 2011-01-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多