NetworkBoundResource với coroutines Kotlin


8

Bạn có ý tưởng nào để triển khai mẫu kho lưu trữ với các coroutines NetworkBoundResource và Kotlin không? Tôi biết chúng ta có thể phóng một coroutine với GlobalScope, nhưng nó có thể dẫn đến rò rỉ coroutine. Tôi muốn chuyển một viewModelScope làm tham số, nhưng hơi khó, khi thực hiện (vì kho lưu trữ của tôi không biết CoroutineScope của bất kỳ ViewModel nào).

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
IMHO, kho lưu trữ sẽ hiển thị các suspendhàm hoặc return Channel/ Flowobject, tùy thuộc vào bản chất của API. Các coroutines thực tế sau đó được thiết lập trong viewmodel. LiveDatađược giới thiệu bởi viewmodel, không phải kho lưu trữ.
CommonsWare

@CommonsWare Vì vậy, bạn đề xuất viết lại NetworkBoundResource để trả về dữ liệu thực tế (hoặc Tài nguyên <T>), mà không sử dụng LiveData trong nó và trong kho lưu trữ?
Kamil Szustak

Bạn là người muốn sử dụng NetworkBoundResource. Nhận xét của tôi tổng quát hơn: IMHO, một triển khai kho lưu trữ của Kotlin sẽ phơi bày các API liên quan đến coroutines.
CommonsWare

Tôi muốn cảm ơn tất cả các bạn để giúp tôi với câu hỏi này và các câu trả lời khác nhau. Và cảm ơn @CommonsWare, người đã gợi ý giúp tôi làm cho mã của mình tốt hơn (một lần nữa)
Valerio

@Commons Bạn muốn khuyên bạn không nên sử dụng Cơ sở dữ liệu phòng với LiveData?
maxbeaudoin

Câu trả lời:


7

Câu trả lời @ N1hk hoạt động đúng, đây chỉ là một triển khai khác không sử dụng flatMapConcattoán tử (được đánh dấu là FlowPreviewtại thời điểm này)

@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
Không phải là tốt hơn để phát Resource.error trong trường hợp ApiErrorResponse sao?
Kamil Szustak

Những gì trang bị thêm phục vụ trở lại nên được?
Mahmood Ali

3

Đây là cách tôi đã thực hiện bằng cách sử dụng livedata-ktxcổ vật; không cần phải vượt qua trong bất kỳ CoroutineScope. Lớp này cũng chỉ sử dụng một loại thay vì hai (ví dụ: Loại kết quả / Loại yêu cầu) vì tôi luôn kết thúc bằng cách sử dụng một bộ chuyển đổi ở nơi khác để ánh xạ các loại đó.

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
}

Giống như @CommonsWare đã nói trong các bình luận, tuy nhiên, sẽ tốt hơn nếu chỉ phơi bày a Flow<T>. Đây là những gì tôi đã cố gắng đưa ra để làm điều đó. Lưu ý rằng tôi đã không sử dụng mã này trong sản xuất, vì vậy người mua hãy cẩn thận.

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
}

Các Flowmã sẽ thực hiện yêu cầu mạng một lần nữa khi các dữ liệu trong cơ sở dữ liệu thay đổi, tôi đăng một câu trả lời mới cho thấy cách xử lý nó
Juan Cruz Soler

Trong thử nghiệm của mình, tôi đặt một điểm dừng bên trong flatMapConcatkhối và cả trong query().map { Resource.Success(it) }khối, sau đó chèn một mục vào cơ sở dữ liệu. Chỉ có điểm dừng sau bị đánh. Nói cách khác, yêu cầu mạng không được thực hiện lại khi dữ liệu trong cơ sở dữ liệu thay đổi.
N1hk

Nếu bạn đặt một điểm dừng ở đây, if (shouldFetch(data))bạn sẽ thấy rằng nó được gọi là hai lần. Lần đầu tiên khi bạn nhận được kết quả từ cơ sở dữ liệu và lần thứ hai khi saveFetchResult(fetch())được gọi
Juan Cruz Soler

Và nếu bạn nghĩ về nó, đó là những gì bạn muốn khi bạn sử dụng Flow. Bạn đang lưu một cái gì đó trong cơ sở dữ liệu và bạn muốn Room cho bạn biết về sự thay đổi đó và gọi lại flatMapConcatmã của bạn . Thay vào đó, bạn không sử dụng Flow<T>và sử dụng Tnếu bạn không muốn hành vi đó
Juan Cruz Soler

3
Bạn nói đúng, tôi hiểu nhầm mã. Các flatMapConcatsẽ trả về một dòng chảy mới được quan sát, vì vậy dòng chảy ban đầu sẽ không còn được gọi. Cả hai câu trả lời đều hành xử theo cùng một cách, vì vậy tôi sẽ giữ cho tôi giống như một cách khác để thực hiện nó. Xin lỗi vì sự nhầm lẫn, và cảm ơn lời giải thích của bạn!
Juan Cruz Soler

0

Tôi mới biết đến Kotlin Coroutine. Tôi chỉ gặp vấn đề này trong tuần này.

Tôi nghĩ rằng nếu bạn đi với mẫu kho lưu trữ như được đề cập trong bài viết ở trên, ý kiến ​​của tôi là cảm thấy thoải mái khi chuyển một CoroutineScope vào NetworkBoundResource . Các CoroutineScope có thể là một trong những thông số của hàm trong Repository , mà trả về một LiveData, như:

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

Vượt qua xây dựng trong phạm vi viewmodelscope như CoroutineScope khi gọi getData () trong ViewModel của bạn, vì vậy NetworkBoundResource sẽ làm việc trong viewmodelscope và bị ràng buộc với vòng đời của ViewModel. Các coroutine trong NetworkBoundResource sẽ bị hủy khi ViewModel chết, đây sẽ là một lợi ích.

Để sử dụng xây dựng trong phạm vi viewmodelscope , đừng quên thêm dưới đây trong build.gradle của bạn.

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha01'
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.