Как отменить работающий блок LiveData Coroutine

Используя последнюю версию LiveData «androidx.lifecycle: lifecycle-liveata-ktx: 2.2.0-alpha03», я разработал код для функции «Поиск продуктов» в ViewModel, используя новый строительный блок LiveData (LiveData + Coroutine), который выполняет синхронный сетевой вызов с помощью Retrofit и соответственно обновляет различные флаги (isLoading, isError) в ViewModel. Я использую Transforamtions.switchMap для «запроса» LiveData, поэтому всякий раз, когда происходит изменение «запроса» из пользовательского интерфейса, код «Search Products» начинает выполнение с помощью 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)
            }
        }
    }
}

person Yasir Ali    schedule 30.08.2019    source источник
comment
Как выглядит ваш модернизированный интерфейс? Используете ли вы данные о приостановке и возврате напрямую? Вы должны обернуть свои данные с помощью интерфейса Call в свой тип возврата и сохранить ссылку на него, чтобы вы могли отменить его при срабатывании switchMap.   -  person Mel    schedule 30.08.2019
comment
Интерфейс дооснащения содержит приостановленную функцию. приостановить забавные поисковые продукты (запрос @Query (query): String)   -  person Yasir Ali    schedule 30.08.2019
comment
Сохранение ссылки на интерфейс Call и его отмена - более приятный способ, но есть ли другой способ, который должен быть похож на отмену задания Coroutine, и все его приостановленные функции останавливаются автоматически?   -  person Yasir Ali    schedule 30.08.2019
comment
В этом случае - если вы отмените область видимости, вызов также должен быть отменен.   -  person Mel    schedule 30.08.2019
comment
Не могли бы вы указать мне, где разместить этот код отмены области действия в приведенном выше примере?   -  person Yasir Ali    schedule 30.08.2019


Ответы (2)


Решить эту проблему можно двумя способами:

Метод №1 (простой метод)

Точно так же, как Мэл объяснил в своем ответе, вы можете сохранить ссылку на экземпляр задания вне switchMap и отменить мгновенное выполнение этого job прямо перед возвратом ваших новых 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()
    }
}

Метод № 2 (не очень чистый, но самодостаточный и многоразовый)

Поскольку блок liveData {} builder выполняется внутри области сопрограммы, вы можете использовать комбинацию CompletableDeffered и launch builder для приостановки этого блока 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)
        }
    }
}

Вы можете загрузить и протестировать оба этих метода в этом демонстрационном проекте

Изменить: обновлен метод № 1, чтобы добавить отмену задания по методу onCleared, как указано Ясиром в комментариях.

person Rafay Ali    schedule 01.09.2019
comment
Метод №1 решил проблему. Я просто хотел бы добавить еще одну вещь: вы также должны отменить это задание в методе onCleared () ViewModel. override fun onCleared() { super.onCleared() completableJob.cancel() } Спасибо @ rafay-ali за решение :-) - person Yasir Ali; 03.09.2019
comment
Ой, да определенно, я, должно быть, пропустил это. Спасибо, что указали :) - person Rafay Ali; 03.09.2019
comment
Как это работает? Я просмотрел источник и обнаружил, что и Job, и CoroutineDispatcher равны CoroutineContext, но как job.cancel() отменить CoroutineLivedata? И как работает этот оператор +? - person Minh Nghĩa; 13.04.2021

Запрос на модернизацию должен быть отменен при отмене родительской области.

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 функции.

person Mel    schedule 30.08.2019
comment
Я хочу отменить задание всякий раз, когда есть изменение в запросе (например, в качестве первой строки кода в блоке switchMap), но когда я это делаю, switchMap никогда не перемещается вперед, потому что continueRequestJob.cancel () всегда останавливает выполнение switchMap Блокировать. val продукты: LiveData ‹Список ‹ProductModel›› = query.switchMap {q - ›liveData (context = continuousRequestJob) {-----› CurrentRequestJob.cancel () - person Yasir Ali; 30.08.2019
comment
Затем вам нужно извлечь логику и иметь кого-то, кто владеет и контролирует проблему, кто-то, кто может сказать логике, что делать и когда это делать. - person Martin Marconcini; 30.08.2019
comment
Вы не туда положили. Не помещайте его в скобки liveData (context = continuousRequestJob), поместите его прямо перед ним, внутри области действия функции switchMap. Вы сразу же отменяете начатую работу. - person Mel; 30.08.2019