NetworkBoundResource mit Kotlin-Coroutinen

8

Haben Sie Ideen, wie Sie ein Repository-Muster mit NetworkBoundResource- und Kotlin-Coroutinen implementieren können ? Ich weiß, dass wir mit einem GlobalScope eine Coroutine starten können, aber dies kann zu einem Coroutine-Leck führen. Ich möchte ein viewModelScope als Parameter übergeben, aber es ist etwas schwierig, wenn es um die Implementierung geht (da mein Repository kein CoroutineScope eines ViewModel kennt).

abstract class NetworkBoundResource<ResultType, RequestType>
@MainThread constructor(
    private val coroutineScope: CoroutineScope
) {

    private val result = MediatorLiveData<Resource<ResultType>>()

    init {
        result.value = Resource.loading(null)
        @Suppress("LeakingThis")
        val dbSource = loadFromDb()
        result.addSource(dbSource) { data ->
            result.removeSource(dbSource)
            if (shouldFetch(data)) {
                fetchFromNetwork(dbSource)
            } else {
                result.addSource(dbSource) { newData ->
                    setValue(Resource.success(newData))
                }
            }
        }
    }

    @MainThread
    private fun setValue(newValue: Resource<ResultType>) {
        if (result.value != newValue) {
            result.value = newValue
        }
    }

    private fun fetchFromNetwork(dbSource: LiveData<ResultType>) {
        val apiResponse = createCall()
        result.addSource(dbSource) { newData ->
            setValue(Resource.loading(newData))
        }
        result.addSource(apiResponse) { response ->
            result.removeSource(apiResponse)
            result.removeSource(dbSource)
            when (response) {
                is ApiSuccessResponse -> {
                    coroutineScope.launch(Dispatchers.IO) {
                        saveCallResult(processResponse(response))

                        withContext(Dispatchers.Main) {
                            result.addSource(loadFromDb()) { newData ->
                                setValue(Resource.success(newData))
                            }
                        }
                    }
                }

                is ApiEmptyResponse -> {
                    coroutineScope.launch(Dispatchers.Main) {
                        result.addSource(loadFromDb()) { newData ->
                            setValue(Resource.success(newData))
                        }
                    }
                }

                is ApiErrorResponse -> {
                    onFetchFailed()
                    result.addSource(dbSource) { newData ->
                        setValue(Resource.error(response.errorMessage, newData))
                    }
                }
            }
        }
    }
}
Kamil Szustak
quelle
2
IMHO sollte das Repository suspendFunktionen oder Rückgabe Channel/ FlowObjekte verfügbar machen, abhängig von der Art der API. Die eigentlichen Coroutinen werden dann im Ansichtsmodell eingerichtet. LiveDatawird vom Ansichtsmodell eingeführt, nicht vom Repository.
CommonsWare
@CommonsWare Sie schlagen also vor, NetworkBoundResource neu zu schreiben, um tatsächliche Daten (oder Ressource <T>) zurückzugeben, ohne LiveData darin und im Repository zu verwenden?
Kamil Szustak
Sie sind derjenige, der verwenden möchte NetworkBoundResource. Meine Kommentare sind allgemeiner: IMHO sollte eine Kotlin-Repository-Implementierung Coroutinen-bezogene APIs verfügbar machen.
CommonsWare
Ich möchte mich bei allen bedanken, die mir bei dieser Frage und den verschiedenen Antworten geholfen haben. Und danke an @CommonsWare, dessen Hinweis mir geholfen hat, meinen Code (wieder) zu verbessern
Valerio
@CommonsWare, also raten Sie von der Verwendung der Raumdatenbank mit LiveData ab?
Maxbeaudoin

Antworten:

7

@ N1hk Antwort funktioniert richtig, dies ist nur eine andere Implementierung, die den flatMapConcatOperator nicht verwendet (es ist als FlowPreviewin diesem Moment markiert )

@FlowPreview
@ExperimentalCoroutinesApi
abstract class NetworkBoundResource<ResultType, RequestType> {

    fun asFlow() = flow {
        emit(Resource.loading(null))

        val dbValue = loadFromDb().first()
        if (shouldFetch(dbValue)) {
            emit(Resource.loading(dbValue))
            when (val apiResponse = fetchFromNetwork()) {
                is ApiSuccessResponse -> {
                    saveNetworkResult(processResponse(apiResponse))
                    emitAll(loadFromDb().map { Resource.success(it) })
                }
                is ApiErrorResponse -> {
                    onFetchFailed()
                    emitAll(loadFromDb().map { Resource.error(apiResponse.errorMessage, it) })
                }
            }
        } else {
            emitAll(loadFromDb().map { Resource.success(it) })
        }
    }

    protected open fun onFetchFailed() {
        // Implement in sub-classes to handle errors
    }

    @WorkerThread
    protected open fun processResponse(response: ApiSuccessResponse<RequestType>) = response.body

    @WorkerThread
    protected abstract suspend fun saveNetworkResult(item: RequestType)

    @MainThread
    protected abstract fun shouldFetch(data: ResultType?): Boolean

    @MainThread
    protected abstract fun loadFromDb(): Flow<ResultType>

    @MainThread
    protected abstract suspend fun fetchFromNetwork(): ApiResponse<RequestType>
}
Juan Cruz Soler
quelle
1
Ist es nicht besser, Resource.error im Fall ApiErrorResponse auszugeben?
Kamil Szustak
Welche Art der Nachrüstung sollte sein?
Mahmood Ali
3

So habe ich es mit dem livedata-ktxArtefakt gemacht; Sie müssen kein CoroutineScope übergeben. Die Klasse verwendet auch nur einen Typ anstelle von zwei (z. B. ResultType / RequestType), da ich immer einen anderen Adapter verwende, um diese zuzuordnen.

import androidx.lifecycle.LiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.map
import nihk.core.Resource

// Adapted from: https://developer.android.com/topic/libraries/architecture/coroutines
abstract class NetworkBoundResource<T> {

    fun asLiveData() = liveData<Resource<T>> {
        emit(Resource.Loading(null))

        if (shouldFetch(query())) {
            val disposable = emitSource(queryObservable().map { Resource.Loading(it) })

            try {
                val fetchedData = fetch()
                // Stop the previous emission to avoid dispatching the saveCallResult as `Resource.Loading`.
                disposable.dispose()
                saveFetchResult(fetchedData)
                // Re-establish the emission as `Resource.Success`.
                emitSource(queryObservable().map { Resource.Success(it) })
            } catch (e: Exception) {
                onFetchFailed(e)
                emitSource(queryObservable().map { Resource.Error(e, it) })
            }
        } else {
            emitSource(queryObservable().map { Resource.Success(it) })
        }
    }

    abstract suspend fun query(): T
    abstract fun queryObservable(): LiveData<T>
    abstract suspend fun fetch(): T
    abstract suspend fun saveFetchResult(data: T)
    open fun onFetchFailed(exception: Exception) = Unit
    open fun shouldFetch(data: T) = true
}

Wie @CommonsWare in den Kommentaren sagte, wäre es jedoch schöner, nur a freizulegen Flow<T>. Hier ist, was ich versucht habe, um das zu tun. Beachten Sie, dass ich diesen Code in der Produktion nicht verwendet habe. Käufer sollten also aufpassen.

import kotlinx.coroutines.flow.*
import nihk.core.Resource

abstract class NetworkBoundResource<T> {

    fun asFlow(): Flow<Resource<T>> = flow {
        val flow = query()
            .onStart { emit(Resource.Loading<T>(null)) }
            .flatMapConcat { data ->
                if (shouldFetch(data)) {
                    emit(Resource.Loading(data))

                    try {
                        saveFetchResult(fetch())
                        query().map { Resource.Success(it) }
                    } catch (throwable: Throwable) {
                        onFetchFailed(throwable)
                        query().map { Resource.Error(throwable, it) }
                    }
                } else {
                    query().map { Resource.Success(it) }
                }
            }

        emitAll(flow)
    }

    abstract fun query(): Flow<T>
    abstract suspend fun fetch(): T
    abstract suspend fun saveFetchResult(data: T)
    open fun onFetchFailed(throwable: Throwable) = Unit
    open fun shouldFetch(data: T) = true
}
N1hk
quelle
Der FlowCode wird die Netzwerkanforderung erneut stellen, wenn sich die Daten in der Datenbank ändern. Ich habe eine neue Antwort gepostet, die zeigt, wie man damit umgeht
Juan Cruz Soler
Bei meinen Tests habe ich einen Haltepunkt in den flatMapConcatBlock und auch in den query().map { Resource.Success(it) }Block eingefügt und dann ein Element in die Datenbank eingefügt. Nur der letztere Haltepunkt wurde getroffen. Mit anderen Worten, die Netzwerkanforderung wird nicht erneut gestellt, wenn sich die Daten in der Datenbank ändern.
N1hk
Wenn Sie hier einen Haltepunkt setzen, werden if (shouldFetch(data))Sie sehen, dass er zweimal aufgerufen wird. Das erste Mal, wenn Sie die Ergebnisse aus der Datenbank erhalten, und das zweite Mal, wenn saveFetchResult(fetch())es aufgerufen wird
Juan Cruz Soler
Und wenn Sie darüber nachdenken, ist es das, was Sie wollen, wenn Sie a verwenden Flow. Sie speichern etwas in der Datenbank und möchten, dass Room Sie über diese Änderung informiert und Ihren flatMapConcatCode erneut aufruft. Sie haben nicht ein Flow<T>und Tstattdessen verwendet, wenn Sie dieses Verhalten nicht wollen
Juan Cruz Soler
3
Sie haben Recht, ich habe den Code falsch verstanden. Der flatMapConcatgibt einen neuen zu beobachtenden Fluss zurück, sodass der anfängliche Fluss nicht mehr aufgerufen wird. Beide Antworten verhalten sich gleich, daher werde ich meine als eine andere Art der Implementierung beibehalten. Entschuldigen Sie die Verwirrung und vielen Dank für Ihre Erklärung!
Juan Cruz Soler
0

Ich bin neu bei Kotlin Coroutine. Ich bin diese Woche auf dieses Problem gestoßen.

Ich denke, wenn Sie sich für das im obigen Beitrag erwähnte Repository-Muster entscheiden, können Sie meiner Meinung nach ein CoroutineScope an die NetworkBoundResource übergeben . Das CoroutineScope kann einer der Parameter der Funktion im Repository sein , die LiveData zurückgibt, wie:

suspend fun getData(scope: CoroutineScope): LiveDate<T>

Übergeben Sie den integrierten Bereich viewmodelscope als CoroutineScope, wenn Sie getData () in Ihrem ViewModel aufrufen , damit NetworkBoundResource innerhalb des viewmodelscope funktioniert und an den Lebenszyklus des Viewmodels gebunden ist. Die Coroutine in der NetworkBoundResource wird abgebrochen, wenn ViewModel tot ist, was von Vorteil wäre.

Um die Build-in Bereich zu verwenden viewmodelscope , vergessen Sie nicht unten in Ihrem build.gradle hinzufügen.

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha01'
Freddie
quelle