/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tamarasdk.repository import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.annotation.MainThread import androidx.annotation.WorkerThread import com.tamarasdk.AppExecutors import com.tamarasdk.api.ApiEmptyResponse import com.tamarasdk.api.ApiErrorResponse import com.tamarasdk.api.ApiResponse import com.tamarasdk.api.ApiSuccessResponse import com.tamarasdk.vo.Resource /** * A generic class that can provide a resource backed by both the sqlite database and the network. * * * You can read more about it in the [Architecture * Guide](https://developer.android.com/arch). * @param * @param */ internal abstract class NetworkBoundResource @MainThread constructor(private val appExecutors: AppExecutors) { private val result = MediatorLiveData>() 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) { if (result.value != newValue) { result.postValue(newValue) } } private fun fetchFromNetwork(dbSource: LiveData) { val apiResponse = createCall() // we re-attach dbSource as a new source, it will dispatch its latest value quickly result.addSource(dbSource) { newData -> setValue(Resource.loading(newData)) } result.addSource(apiResponse) { response -> result.removeSource(apiResponse) result.removeSource(dbSource) when (response) { is ApiSuccessResponse -> { appExecutors.diskIO().execute { saveCallResult(processResponse(response)) appExecutors.mainThread().execute { // we specially request a new live data, // otherwise we will get immediately last cached value, // which may not be updated with latest results received from network. result.addSource(loadFromDb()) { newData -> setValue(Resource.success(newData)) } } } } is ApiEmptyResponse -> { appExecutors.mainThread().execute { // reload from disk whatever we had result.addSource(loadFromDb()) { newData -> setValue(Resource.success(newData)) } } } is ApiErrorResponse -> { onFetchFailed() result.addSource(dbSource) { newData -> setValue(Resource.error(response.error.message ?: "unknown error", newData)) } } } } } protected open fun onFetchFailed() {} fun asLiveData() = result as LiveData> @WorkerThread protected abstract fun processResponse(response: ApiSuccessResponse): ResultType @WorkerThread protected open fun saveCallResult(item: ResultType) { setValue(Resource.success(item)) } @MainThread protected abstract fun shouldFetch(data: ResultType?): Boolean @MainThread protected abstract fun loadFromDb(): LiveData @MainThread protected abstract fun createCall(): LiveData> }