/*
* 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>
}