package us.kostenko.architecturecomponentstmdb.details.viewmodel.netres
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MediatorLiveData
import android.support.annotation.MainThread
import android.support.annotation.WorkerThread
import us.kostenko.architecturecomponentstmdb.common.Coroutines
import us.kostenko.architecturecomponentstmdb.common.api.retrofit.asRetrofitException
import us.kostenko.architecturecomponentstmdb.details.model.MovieError
/**
* 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 <ResultType>
* @param <RequestType>
</RequestType></ResultType> */
abstract class NetworkBoundResource<ResultType, RequestType>
@MainThread constructor(private val coroutines: Coroutines) {
private val result = MediatorLiveData<Resource<ResultType>>()
init {
result.value = Resource.loading(null)
@Suppress("LeakingThis")
val dbSource = loadFromDb()
result.addSourceOnce(dbSource) { data ->
if (shouldFetch(data)) {
fetchFromNetwork(dbSource)
} else {
result.addSource(dbSource) { newData ->
setValue(Resource.success(newData))
}
}
}
}
fun asLiveData() = result as LiveData<Resource<ResultType>>
@MainThread
private fun setValue(newValue: Resource<ResultType>) {
if (result.value != newValue) {
result.value = newValue
}
}
private fun fetchFromNetwork(dbSource: LiveData<ResultType>) = coroutines {
// we re-attach dbSource as a new source, it will dispatch its latest value quickly
result.addSourceOnce(dbSource) { newData ->
setValue(Resource.loading(newData))
}
val (status, message) = try {
fetchData()?.let { saveCallResult(it) }
Status.SUCCESS to null
} catch (e: Throwable) {
onFetchFailed()
val message = e.asRetrofitException().getErrorBodyAs(MovieError::class.java)?.statusMessage ?: e.message
Status.ERROR to message
}
coroutines.onUi {
result.addSource(loadFromDb()) { newData ->
setValue(Resource(status, newData, message))
}
}
}
fun fetchFromNetwork2() = coroutines {
Resource.handle({ fetchData()?.let(::saveCallResult) }, {
coroutines.onUi {
result.addSourceOnce(it.once, loadFromDb()) { newData ->
val message = it.e?.asRetrofitException()?.getErrorBodyAs(MovieError::class.java)?.statusMessage ?: it.e?.message
if (it.e != null) onFetchFailed()
setValue(Resource(it.status, newData, message))
}
}
})
}
protected open fun onFetchFailed() {}
@WorkerThread
protected open fun processResponse(response: ApiSuccessResponse<RequestType>) = response.body
@WorkerThread
protected abstract fun saveCallResult(item: RequestType)
@MainThread
protected abstract fun shouldFetch(data: ResultType?): Boolean
@MainThread
protected abstract fun loadFromDb(): LiveData<ResultType>
@MainThread
protected abstract suspend fun fetchData(): RequestType?
private fun <T, S>MediatorLiveData<T>.addSourceOnce(source: LiveData<S>, observer: (S?) -> Unit) {
addSource(source) { data ->
removeSource(source)
observer(data)
}
}
private fun <T, S>MediatorLiveData<T>.addSourceOnce(once: Boolean, source: LiveData<S>, observer: (S?) -> Unit) {
addSource(source) { data ->
if (once) removeSource(source)
observer(data)
}
}
}
package us.kostenko.architecturecomponentstmdb.details.viewmodel.netres
import retrofit2.Response
import timber.log.Timber
import java.util.regex.Pattern
/**
* Common class used by API responses.
* @param <T> the type of the response object
</T> */
@Suppress("unused") // T is used in extending classes
sealed class ApiResponse<T> {
companion object {
fun <T> create(error: Throwable): ApiErrorResponse<T> {
return ApiErrorResponse(error.message ?: "unknown error")
}
fun <T> create(response: Response<T>): ApiResponse<T> {
return if (response.isSuccessful) {
val body = response.body()
if (body == null || response.code() == 204) {
ApiEmptyResponse()
} else {
ApiSuccessResponse(
body = body,
linkHeader = response.headers()?.get("link")
)
}
} else {
val msg = response.errorBody()?.string()
val errorMsg = if (msg.isNullOrEmpty()) {
response.message()
} else {
msg
}
ApiErrorResponse(errorMsg ?: "unknown error")
}
}
}
}
/**
* separate class for HTTP 204 responses so that we can make ApiSuccessResponse's body non-null.
*/
class ApiEmptyResponse<T> : ApiResponse<T>()
data class ApiSuccessResponse<T>(
val body: T,
val links: Map<String, String>
) : ApiResponse<T>() {
constructor(body: T, linkHeader: String?) : this(
body = body,
links = linkHeader?.extractLinks() ?: emptyMap()
)
val nextPage: Int? by lazy(LazyThreadSafetyMode.NONE) {
links[NEXT_LINK]?.let { next ->
val matcher = PAGE_PATTERN.matcher(next)
if (!matcher.find() || matcher.groupCount() != 1) {
null
} else {
try {
Integer.parseInt(matcher.group(1))
} catch (ex: NumberFormatException) {
Timber.w("cannot parse next page from %s", next)
null
}
}
}
}
companion object {
private val LINK_PATTERN = Pattern.compile("<([^>]*)>[\\s]*;[\\s]*rel=\"([a-zA-Z0-9]+)\"")
private val PAGE_PATTERN = Pattern.compile("\\bpage=(\\d+)")
private const val NEXT_LINK = "next"
private fun String.extractLinks(): Map<String, String> {
val links = mutableMapOf<String, String>()
val matcher = LINK_PATTERN.matcher(this)
while (matcher.find()) {
val count = matcher.groupCount()
if (count == 2) {
links[matcher.group(2)] = matcher.group(1)
}
}
return links
}
}
}
data class ApiErrorResponse<T>(val errorMessage: String) : ApiResponse<T>()
/**
* A generic class that holds a value with its loading status.
* @param <T>
</T> */
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(Status.SUCCESS, data, null)
}
fun <T> error(msg: String, data: T?): Resource<T> {
return Resource(Status.ERROR, data, msg)
}
fun <T> loading(data: T?): Resource<T> {
return Resource(Status.LOADING, data, null)
}
}
}
/**
* Status of a resource that is provided to the UI.
*
*
* These are usually created by the Repository classes where they return
* `LiveData<Resource<T>>` to pass back the latest data to the UI with its fetch status.
*/
enum class Status {
SUCCESS,
ERROR,
LOADING
}
package com.android.example.github.repository
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MediatorLiveData
import android.support.annotation.MainThread
import android.support.annotation.WorkerThread
import com.android.example.github.AppExecutors
import com.android.example.github.api.ApiEmptyResponse
import com.android.example.github.api.ApiErrorResponse
import com.android.example.github.api.ApiResponse
import com.android.example.github.api.ApiSuccessResponse
import com.android.example.github.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 <ResultType>
* @param <RequestType>
</RequestType></ResultType> */
abstract class NetworkBoundResource<ResultType, RequestType>
@MainThread constructor(private val appExecutors: AppExecutors) {
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()
// 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.errorMessage, newData))
}
}
}
}
}
protected open fun onFetchFailed() {}
fun asLiveData() = result as LiveData<Resource<ResultType>>
@WorkerThread
protected open fun processResponse(response: ApiSuccessResponse<RequestType>) = response.body
@WorkerThread
protected abstract fun saveCallResult(item: RequestType)
@MainThread
protected abstract fun shouldFetch(data: ResultType?): Boolean
@MainThread
protected abstract fun loadFromDb(): LiveData<ResultType>
@MainThread
protected abstract fun createCall(): LiveData<ApiResponse<RequestType>>
}