Kotlinコルーチンを使用したNetworkBoundResource


8

NetworkBoundResourceとKotlinコルーチンでリポジトリパターンを実装する方法について何かアイデアはありますか?GlobalScopeを使用してコルーチンを起動できることはわかっていますが、コルーチンのリークにつながる可能性があります。viewModelScopeをパラメーターとして渡したいのですが、実装に関しては少しトリッキーです(私のリポジトリはViewModelのCoroutineScopeを知らないためです)。

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

2
私見、リポジトリはAPIの性質に応じて、suspend関数またはreturn Channel/ Flowオブジェクトを公​​開する必要があります。次に、実際のコルーチンがビューモデルに設定されます。LiveDataリポジトリではなく、ビューモデルによって導入されます。
CommonsWare

@CommonsWareでは、実際のデータ(またはResource <T>)を返すようにNetworkBoundResourceを書き直すことを提案しています。
Kamil Szustak、

あなたが使いたいのはあなたですNetworkBoundResource。私のコメントはより一般的です:私見、Kotlinリポジトリ実装はコルーチン関連のAPIを公開する必要があります。
CommonsWare

この質問とさまざまな答えを手伝ってくれた皆さんに感謝します。@CommonsWareのおかげで、コードを改善するのに役立った(ここでも)
Valerio

1
私はそれを個人的な好みとしてもっと言います。LiveDataRxJavaまたはKotlinコルーチンのいずれかの能力が不足しています。LiveDataアクティビティまたはフラグメントへの「ラストマイル」の通信に非常に適しており、それを考慮して設計されています。あなたがリポジトリをスキップし、ちょうど持っているしたい場合や、小さなアプリケーションのために、ViewModelと話をまっすぐにRoomDatabaseLiveData罰金です。
CommonsWare

回答:


7

@ N1hkの答えは正しく機能します。これは、flatMapConcat演算子を使用しない別の実装です(FlowPreview現時点ではマークされています)。

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


1
ApiErrorResponseのケースでResource.errorを発行する方が良いのではないですか?
Kamil Szustak

改造サーブのリターンタイプはどのようにする必要がありますか?
Mahmood Ali

@MahmoodAli suspend fun someData(@ Query / @ Path):ApiResponse <List <Postitems >> ...データに応じて管理
USMAN osman

3

アップデート(2020-05-27):

前の例よりもKotlin言語に慣用的な方法で、フローAPIを使用し、フアンの回答から借用した方法は、次のようなスタンドアロン関数として表すことができます。

inline fun <ResultType, RequestType> networkBoundResource(
    crossinline query: () -> Flow<ResultType>,
    crossinline fetch: suspend () -> RequestType,
    crossinline saveFetchResult: suspend (RequestType) -> Unit,
    crossinline onFetchFailed: (Throwable) -> Unit = { Unit },
    crossinline shouldFetch: (ResultType) -> Boolean = { true }
) = flow<Resource<ResultType>> {
    emit(Resource.Loading(null))
    val data = query().first()

    val flow = 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)
}

元の答え:

これは、私がlivedata-ktxアーティファクトを使用して行ってきた方法です。CoroutineScopeを渡す必要はありません。また、クラスでは2つのタイプではなく1つのタイプのみを使用します(例:ResultType / RequestType)。これらのマッピングには常に他の場所でアダプターを使用するためです。

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
}

コメントで@CommonsWareが言ったように、ただを公開するほうがいいでしょうFlow<T>。これを行うために思いついたのは次のとおりです。このコードは本番環境では使用していないため、購入者は注意してください。

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
}

Flowデータベースのデータに変更が、私はそれを処理する方法を示していること、新しい答えを投稿するときのコードは再度、ネットワーク要求を行います
フアン・クルーズソレル

テストflatMapConcatでは、query().map { Resource.Success(it) }ブロック内とブロック内にブレークポイントを設定し、データベースにアイテムを挿入しました。後者のブレークポイントのみがヒットしました。つまり、データベース内のデータが変更されても、ネットワーク要求は再度行われません。
N1hk

ここにブレークポイントを置くと、if (shouldFetch(data))2回呼び出されることがわかります。最初にデータベースから結果を取得し、2番目にsaveFetchResult(fetch())呼び出されたとき
Juan Cruz Soler

そして、あなたがそれについて考えるならば、あなたがを使うときにあなたが望むものですFlow。データベースに何かを保存していて、Roomにその変更を通知してflatMapConcatコードを再度呼び出してもらいたいとします。あなたは使っていなかったFlow<T>と使用してT、あなたがその振る舞いをしたくない場合は代わりに
フアン・クルーズソレル

3
あなたは正しい、私はコードを誤解した。flatMapConcat最初の流れがもはや呼び出されますので、新しい流れを観察することに戻りません。どちらの回答も同じように動作するので、別の方法で実装します。混乱してすみません、そしてあなたの説明に感謝します!
Juan Cruz Soler

0

Kotlin Coroutineは初めてです。今週、この問題に遭遇しました。

上記の投稿で述べたようにリポジトリパターンを使用している場合は、CoroutineScopeNetworkBoundResourceに渡してもかまいません。CoroutineScopeは、関数のパラメータの一つとすることができるリポジトリのような、LiveDataを返します。

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

ViewModelでgetData()を呼び出すときに、組み込みスコープviewmodelscopeをCoroutineScopeとして渡すと、NetworkBoundResourceviewmodelscope内で機能し、Viewmodelのライフサイクルにバインドされます。NetworkBoundResourceのコルーチンは、ViewModel無効になるとキャンセルされます。これはメリットです。

組み込みスコープviewmodelscopeを使用するには、build.gradleに以下を追加することを忘れないでください。

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha01'
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.