Created
May 10, 2019 10:09
-
-
Save victory1908/d356bff869f50ed74aa6bfb4d4818991 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>> | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment