Skip to content

Instantly share code, notes, and snippets.

@kubode
Created November 8, 2017 01:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kubode/b0ae3b6d3ead8812041788b83838cf9f to your computer and use it in GitHub Desktop.
Save kubode/b0ae3b6d3ead8812041788b83838cf9f to your computer and use it in GitHub Desktop.
Architecture Component immutable model cache pattern
import android.arch.core.executor.testing.InstantTaskExecutorRule
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MediatorLiveData
import android.arch.lifecycle.MutableLiveData
import io.reactivex.subjects.SingleSubject
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
class FooTest {
data class Model(val key: String, val name: String) {
fun merge(other: Model) = copy(name = other.name)
}
@Rule
@JvmField
val rule = InstantTaskExecutorRule()
lateinit var cache: MyMutableLiveData<Map<String, Model>>
lateinit var single: SingleSubject<Model>
lateinit var list: SingleSubject<List<Model>>
@Before
fun setUp() {
cache = MyMutableLiveData(emptyMap())
single = SingleSubject.create()
list = SingleSubject.create()
}
private fun load(key: String): LiveData<Resource<Model>> {
val result = MediatorLiveData<Resource<Model>>()
val api = MutableLiveData<Resource<Model>>()
result.addSource(api) {
if (it is Resource.Success) {
cache.value += it.data.key to (cache.value[it.data.key]?.merge(it.data) ?: it.data)
} else {
result.value = it
}
}
result.addSource(cache) {
val now = api.value
if (now is Resource.Success) {
result.value = Resource.Success(cache.value[now.data.key] ?: now.data)
}
}
api.value = Resource.Loading(cache.value[key])
single.subscribe({
val merged = cache.value[key]?.merge(it) ?: it
cache.value += key to merged
api.value = Resource.Success(merged)
}, {
api.value = Resource.Error(cache.value[key], it)
})
return result
}
private fun search(): LiveData<Resource<List<Model>>> {
val result = MediatorLiveData<Resource<List<Model>>>()
val api = MutableLiveData<Resource<List<Model>>>()
result.addSource(api) {
if (it is Resource.Success) {
it.data
.map { it.key to (cache.value[it.key]?.merge(it) ?: it) }
.toTypedArray()
.let { cache.value += mapOf(*it) }
} else {
result.value = it
}
}
result.addSource(cache) { cache ->
val now = api.value
if (now is Resource.Success) {
result.value = Resource.Success(now.data.mapNotNull { cache?.get(it.key) })
}
}
api.value = Resource.Loading(null)
list.subscribe({
api.value = Resource.Success(it)
}, {
api.value = Resource.Error(null, it)
})
return result
}
private fun update() {
}
@Test
fun load_success() {
val load = load("1").apply { observeForever { } }
assertEquals(Resource.Loading(null), load.value)
single.onSuccess(Model("1", "test"))
assertEquals(Resource.Success(Model("1", "test")), load.value)
assertEquals(mapOf("1" to Model("1", "test")), cache.value)
}
@Test
fun load_error() {
val load = load("2").apply { observeForever { } }
val e = RuntimeException()
single.onError(e)
assertEquals(Resource.Error(null, e), load.value)
assertEquals(emptyMap(), cache.value)
}
@Test
fun load_updateCache() {
cache.value += "1" to Model("1", "test")
val load = load("1").apply { observeForever { } }
assertEquals(Resource.Loading(Model("1", "test")), load.value)
single.onSuccess(Model("1", "foo"))
assertEquals(Resource.Success(Model("1", "foo")), load.value)
assertEquals(mapOf("1" to Model("1", "foo")), cache.value)
}
@Test
fun load_updateResult() {
val load = load("1").apply { observeForever { } }
single.onSuccess(Model("1", "foo"))
cache.value += "1" to Model("1", "test")
assertEquals(Resource.Success(Model("1", "test")), load.value)
}
@Test
fun search_success() {
val s = search().apply { observeForever { } }
assertEquals(Resource.Loading(null), s.value)
list.onSuccess(listOf(Model("1", "foo"), Model("2", "bar")))
assertEquals(Resource.Success(listOf(Model("1", "foo"), Model("2", "bar"))), s.value)
assertEquals(mapOf("1" to Model("1", "foo"), "2" to Model("2", "bar")), cache.value)
cache.value += "2" to Model("2", "baz")
assertEquals(Resource.Success(listOf(Model("1", "foo"), Model("2", "baz"))), s.value)
}
}
class MyMutableLiveData<T>(initialValue: T) : MutableLiveData<T>() {
init {
value = initialValue
}
override fun getValue(): T {
@Suppress("UNCHECKED_CAST")
return super.getValue() as T
}
}
sealed class Resource<out T> {
abstract val data: T?
inline fun <R> fold(onLoading: (T?) -> R, onSuccess: (T) -> R, onError: (T?, Throwable) -> R): R {
return when (this) {
is Loading<T> -> onLoading(data)
is Success<T> -> onSuccess(data)
is Error<T> -> onError(data, error)
}
}
data class Loading<out T>(override val data: T?) : Resource<T>()
data class Success<out T>(override val data: T) : Resource<T>()
data class Error<out T>(override val data: T?, val error: Throwable) : Resource<T>()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment