简体   繁体   English

如何取消正在运行的 LiveData 协程块

[英]How to cancel a running LiveData Coroutine Block

By using LiveData's latest version "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03", I have developed a code for a feature called "Search Products" in the ViewModel using LiveData's new building block (LiveData + Coroutine) that performs a synchronous network call using Retrofit and update different flags (isLoading, isError) in ViewModel accordingly.通过使用 LiveData 的最新版本“androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03”,我使用 LiveData 的新构建块(LiveData + Coroutine)为 ViewModel 中名为“搜索产品”的功能开发了代码使用 Retrofit 执行同步网络调用并相应地更新 ViewModel 中的不同标志(isLoading、isError)。 I am using Transforamtions.switchMap on "query" LiveData so whenever there is a change in "query" from the UI, the "Search Products" code starts its executing using Transformations.switchMap.我在“查询”LiveData 上使用 Transforamtions.switchMap,因此每当 UI 中的“查询”发生变化时,“搜索产品”代码就会使用 Transformations.switchMap 开始执行。 Every thing is working fine, except that i want to cancel the previous Retrofit Call whenever a change happens in "query" LiveData.一切正常,除了我想在“查询”LiveData 发生更改时取消之前的改造调用。 Currently i can't see any way to do this.目前我看不到任何方法来做到这一点。 Any help would be appreciated.任何帮助,将不胜感激。

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

You can solve this problem in two ways:您可以通过两种方式解决此问题:

Method # 1 ( Easy Method )方法#1(简单方法)

Just like Mel has explained in his answer , you can keep a referece to the job instance outside of switchMap and cancel instantance of that job right before returning your new liveData in switchMap.就像 Mel 在他的回答中解释的那样,您可以在 switchMap 之外保留对作业实例的引用,并在将新的 liveData 返回到 switchMap 之前取消该作业的实例化。

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

Method # 2 ( Not so clean but self contained and reusable)方法#2(不那么干净,但自包含和可重用)

Since liveData {} builder block runs inside a coroutine scope, you can use a combination of CompletableDeffered and coroutine launch builder to suspend that liveData block and observe query liveData manually to launch jobs for network requests.由于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)
        }
    }
}

You can download and test run both of these methods in this demo project您可以在此 演示项目中下载并测试运行这两种方法

Edit: Updated Method # 1 to add job cancellation on onCleared method as pointed out by yasir in comments.编辑:更新方法 # 1 以在 onCleared 方法上添加作业取消,正如 yasir 在评论中指出的那样。

Retrofit request should be cancelled when parent scope is cancelled.取消父作用域时,应取消改造请求。

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

Then you need to cancel ongoingRequestJob when you need to.然后您需要在需要时取消ongoingRequestJob Next time liveData(context = ongoingRequestJob) is triggered, since it'll return a new job, it should run without problems.下次触发liveData(context = ongoingRequestJob) ,由于它将返回一个新作业,因此它应该可以正常运行。 All you need to left is cancel it where you need to, ie in query.switchMap function scope.您只需要在需要的地方取消它,即在query.switchMap函数范围内取消它。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM