如何取消 运行 LiveData 协程块

How to cancel a running LiveData Coroutine Block

通过使用 LiveData 的最新版本 "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03",我在 ViewModel 中使用 LiveData 的新构建块(LiveData + Coroutine)开发了一个名为 "Search Products" 的功能的代码,该功能使用 LiveData + Coroutine 执行同步网络调用相应地改造和更新 ViewModel 中的不同标志(isLoading、isError)。我在 "query" LiveData 上使用 Transforamtions.switchMap,所以每当 "query" 从 UI 发生变化时,"Search Products" 代码开始使用 [=21= 执行].一切正常,只是我想在 "query" LiveData 发生变化时取消之前的 Retrofit Call。目前我看不出有什么办法可以做到这一点。任何帮助,将不胜感激。

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)
            }
        }
    }
}

取消父作用域时应取消改造请求。

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) 被触发时,因为它会 return 一个新工作,它应该 运行 没有问题。您只需要在需要的地方取消它,即在 query.switchMap 函数范围内。

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

方法#1(简易方法)

就像 Mel 在他的 中解释的那样,您可以在 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 {} 构建器块 运行s 在协程范围内,您可以结合使用 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 在评论中指出的那样。