【问题标题】:How to cancel a running LiveData Coroutine Block如何取消正在运行的 LiveData 协程块
【发布时间】:2020-01-03 12:42:29
【问题描述】:

通过使用 LiveData 的最新版本“androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03”,我使用 LiveData 的新构建块(LiveData +协程),它使用 Retrofit 执行同步网络调用,并相应地更新 ViewModel 中的不同标志(isLoading、isError)。我在“查询”LiveData 上使用 Transforamtions.switchMap,因此每当 UI 中的“查询”发生变化时,“搜索产品”代码就会使用 Transformations.switchMap 开始执行。一切正常,除了我想在“查询”LiveData 发生变化时取消之前的改造调用。目前我看不到任何方法可以做到这一点。任何帮助,将不胜感激。

class ProductSearchViewModel : ViewModel() {
    val completableJob = Job()
    private val coroutineScope = CoroutineScope(Dispatchers.IO + completableJob)

    // Query Observable Field
    val query: MutableLiveData<String> = MutableLiveData()

    // IsLoading Observable Field
    private val _isLoading = MutableLiveData<Boolean>()
    val isLoading: LiveData<Boolean> = _isLoading


    val products: LiveData<List<ProductModel>> = query.switchMap { q ->
        liveData(context = coroutineScope.coroutineContext) {
            emit(emptyList())
            _isLoading.postValue(true)
            val service = MyApplication.getRetrofitService()
            val response = service?.searchProducts(q)
            if (response != null && response.isSuccessful && response.body() != null) {
                _isLoading.postValue(false)
                val body = response.body()
                if (body != null && body.results != null) {
                    emit(body.results)
                }
            } else {
                _isLoading.postValue(false)
            }
        }
    }
}

【问题讨论】:

  • 你的改造界面是什么样的?您是否使用暂停并直接返回数据?您应该在返回类型上使用 Call 接口包装数据并保留对它的引用,以便在触发 switchMap 时取消它。
  • 改造界面包含挂起功能。暂停有趣的 searchProducts(@Query("query") query: String)
  • 保留对 Call 接口的引用并取消它是一种更好的方法,但是还有其他方法应该类似于取消 Coroutine Job 并且其所有暂停的功能都会自动停止吗?
  • 在这种情况下 - 如果您取消范围,呼叫也应该被取消。
  • 您能否指出在上面共享的示例中将此范围取消代码放置在何处?

标签: android kotlin android-livedata kotlin-coroutines kotlin-extension


【解决方案1】:

你可以通过两种方式解决这个问题:

方法#1(简单方法)

就像 Mel 在他的 answer 中解释的那样,您可以在 switchMap 之外保留对作业实例的引用,并在 switchMap 中返回新的 liveData 之前取消该作业的实例。

class ProductSearchViewModel : ViewModel() {

    // Job instance
    private var job = Job()

    val products = Transformations.switchMap(_query) {
        job.cancel() // Cancel this job instance before returning liveData for new query
        job = Job() // Create new one and assign to that same variable

        // Pass that instance to CoroutineScope so that it can be cancelled for next query
        liveData(CoroutineScope(job + Dispatchers.IO).coroutineContext) { 
            // Your code here
        }
    }

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

方法#2(不是那么干净,但独立且可重复使用)

由于liveData {} 构建器块在协程范围内运行,您可以使用CompletableDeffered 和协程launch 构建器的组合来暂停该liveData 块并手动观察query liveData 以启动网络请求的作业。

class ProductSearchViewModel : ViewModel() {

    private val _query = MutableLiveData<String>()

    val products: LiveData<List<String>> = liveData {
        var job: Job? = null // Job instance to keep reference of last job

        // LiveData observer for query
        val queryObserver = Observer<String> {
            job?.cancel() // Cancel job before launching new coroutine
            job = GlobalScope.launch {
                // Your code here
            }
        }

        // Observe query liveData here manually
        _query.observeForever(queryObserver)

        try {
            // Create CompletableDeffered instance and call await.
            // Calling await will suspend this current block 
            // from executing anything further from here
            CompletableDeferred<Unit>().await()
        } finally {
            // Since we have called await on CompletableDeffered above, 
            // this will cause an Exception on this liveData when onDestory
            // event is called on a lifeCycle . By wrapping it in 
            // try/finally we can use this to know when that will happen and 
            // cleanup to avoid any leaks.
            job?.cancel()
            _query.removeObserver(queryObserver)
        }
    }
}

您可以在此demo project 中下载并测试运行这两种方法

编辑:更新方法 #1 以在 onCleared 方法上添加作业取消,正如 yasir 在 cmets 中指出的那样。

【讨论】:

  • 方法#1解决了这个问题。我想再补充一件事,那就是,您还必须在 ViewModel 的 onCleared() 方法中取消此作业。 override fun onCleared() { super.onCleared() completableJob.cancel() } 感谢@rafay-ali 的解决方案:-)
  • 糟糕,是的,我一定错过了。感谢您指出:)
  • 这是如何工作的?我浏览了一下源码,发现JobCoroutineDispatcher都是CoroutineContext,但是job.cancel()怎么能取消CoroutineLivedata呢? + 运算符是如何工作的?
【解决方案2】:

当父作用域被取消时,改造请求应该被取消。

class ProductSearchViewModel : ViewModel() {
    val completableJob = Job()
    private val coroutineScope = CoroutineScope(Dispatchers.IO + completableJob)

    /**
     * Adding job that will be used to cancel liveData builder.
     * Be wary - after cancelling, it'll return a new one like:
     *
     *     ongoingRequestJob.cancel() // Cancelled
     *     ongoingRequestJob.isActive // Will return true because getter created a new one
     */
    var ongoingRequestJob = Job(coroutineScope.coroutineContext[Job])
        get() = if (field.isActive) field else Job(coroutineScope.coroutineContext[Job])

    // Query Observable Field
    val query: MutableLiveData<String> = MutableLiveData()

    // IsLoading Observable Field
    private val _isLoading = MutableLiveData<Boolean>()
    val isLoading: LiveData<Boolean> = _isLoading


    val products: LiveData<List<ProductModel>> = query.switchMap { q ->
        liveData(context = ongoingRequestJob) {
            emit(emptyList())
            _isLoading.postValue(true)
            val service = MyApplication.getRetrofitService()
            val response = service?.searchProducts(q)
            if (response != null && response.isSuccessful && response.body() != null) {
                _isLoading.postValue(false)
                val body = response.body()
                if (body != null && body.results != null) {
                    emit(body.results)
                }
            } else {
                _isLoading.postValue(false)
            }
        }
    }
}

那么你需要在需要的时候取消ongoingRequestJob。下次触发liveData(context = ongoingRequestJob) 时,由于它会返回一个新作业,它应该可以正常运行。您只需要在需要的地方取消它,即在query.switchMap 函数范围内。

【讨论】:

  • 我想在查询发生变化时取消作业(即:作为 switchMap 块中的第一行代码)但是当我这样做时,switchMap 永远不会向前移动,因为正在进行的RequestJob.cancel () 总是停止 switchMap 块的执行。 val 产品: LiveData> = query.switchMap { q -> liveData(context = waitingRequestJob) { -----> concurrentRequestJob.cancel()
  • 然后你需要提取逻辑并让某人“拥有”和“控制”问题,有人可以告诉逻辑该做什么以及何时做。
  • 你放错地方了。不要把它放在 liveData(context = concurrentRequestJob) 括号内,把它放在它前面,在 switchMap 函数范围内。您正在取消立即开始的工作。
猜你喜欢
  • 1970-01-01
  • 2021-05-17
  • 2020-09-16
  • 1970-01-01
  • 1970-01-01
  • 2021-10-17
  • 1970-01-01
  • 1970-01-01
  • 2021-11-04
相关资源
最近更新 更多