Created
November 8, 2017 01:56
-
-
Save kubode/b0ae3b6d3ead8812041788b83838cf9f to your computer and use it in GitHub Desktop.
Architecture Component immutable model cache pattern
This file contains hidden or 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
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) | |
} | |
} |
This file contains hidden or 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
class MyMutableLiveData<T>(initialValue: T) : MutableLiveData<T>() { | |
init { | |
value = initialValue | |
} | |
override fun getValue(): T { | |
@Suppress("UNCHECKED_CAST") | |
return super.getValue() as T | |
} | |
} |
This file contains hidden or 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
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