Repository pattern support library for Kotlin with Coroutines & Flow.
Available for Android or any Kotlin/JVM projects.
This library provides remote and local cache abstraction and observation with Kotlin Coroutines Flow.
Created according to the following 5 policies.
- Repository pattern
- Abstract remote and local data acquisition.
- Single Source of Truth
- Looks like a single source from the user side.
- Observer pattern
- Observing data with Kotlin Coroutines.
- Return value as soon as possible
- Representing the state of data
The following is the class structure of Repository pattern using this library.
The following is an example of screen display using LoadingState.
Add the following gradle dependency exchanging *.*.* for the latest release.
implementation("com.kazakago.storeflowable:storeflowable:*.*.*")Optional: if you use LoadingState class and related functions only.
implementation("com.kazakago.storeflowable:storeflowable-core:*.*.*")There are only 2 things you have to implement:
- Create a class to manage the in-app cache.
- Create a class to get data from origin server.
First, create a class that inherits Cacher<PARAM, DATA>.
Put the type you want to use as a param in <PARAM>. If you don't need the param, put in the Unit.
object UserCacher : Cacher<UserId, UserData>()Cacher<PARAM, DATA> needs to be used in Singleton pattern, so please make it object class.
Next, create a class that implements Fetcher<PARAM, DATA>.
Put the type you want to use as a param in <PARAM>. If you don't need the param, put in the Unit.
An example is shown below.
class UserFetcher : Fetcher<UserId, UserData> {
private val userApi = UserApi()
// Get data from remote server.
override suspend fun fetch(param: UserId): UserData {
return userApi.fetch(param)
}
}You need to prepare the API access class.
In this case, UserApi class.
After that, you can get the StoreFlowable<DATA> class from the StoreFlowable<PARAM, DATA>.from(Cacher, Fetcher, PARAM) method.
Be sure to go through the created StoreFlowable<DATA> class when getting / updating data.
val userFlowable: StoreFlowable<UserData> = StoreFlowable.from(userCacher, userFetcher, userId)
val userStateFlow: FlowLoadingState<UserData> = userFlowable.publish()You can get the data in the form of FlowLoadingState<DATA> (Same as Flow<LoadingState<DATA>>) by using the publish() method.
LoadingState class is a Sealed Classes that holds raw data.
You can observe the data by collecting Flow.
and branch the data state with doAction() method or when statement.
userStateFlow.collect { userState ->
userState.doAction(
onLoading = { content: UserData? ->
...
},
onCompleted = { content: UserData, _, _ ->
...
},
onError = { exception: Exception ->
...
}
)
}On Android, it is recommended to pass the data to LiveData or StateFlow with ViewModel and display it on the UI.
Refer to the example module for details. This module works as an Android app.
See GithubMetaCacher + GithubMetaFetcher or GithubUserCacher + GithubUserFetcher.
This example accesses the Github API.
Get data without LoadingState class
If you don't need value flow and LoadingState class, you can use requireData() or getData().
requireData() throws an Exception if there is no valid cache and fails to get new data.
getData() returns null instead of Exception.
interface StoreFlowable<DATA> {
suspend fun getData(from: GettingFrom = GettingFrom.Both): DATA?
suspend fun requireData(from: GettingFrom = GettingFrom.Both): DATA
}GettingFrom parameter specifies where to get the data.
enum class GettingFrom {
// Gets a combination of valid cache and remote. (Default behavior)
Both,
// Gets only remotely.
Origin,
// Gets only locally.
Cache,
}However, use requireData() or getData() only for one-shot data acquisition, and consider using publish() if possible.
If you want to ignore the cache and get new data, add forceRefresh parameter to publish().
interface StoreFlowable<DATA> {
fun publish(forceRefresh: Boolean = false): FlowLoadingState<DATA>
}Or you can use refresh() if you are already observing the Flow.
interface StoreFlowable<DATA> {
suspend fun refresh()
}Use validate() if you want to verify that the local cache is valid.
If invalid, get new data remotely.
interface StoreFlowable<DATA> {
suspend fun validate()
}If you want to update the local cache, use the update() method.
Flow observers will be notified.
interface StoreFlowable<DATA> {
suspend fun update(newData: DATA?)
}Use mapContent(transform) to transform content in FlowLoadingStates<DATA>.
val state: FlowLoadingState<Int> = ...
val mappedState: FlowLoadingState<String> = state.mapContent { value: Int ->
value.toString()
}Use combineState(state, transform) to combine multiple FlowLoadingStates<DATA>.
val state1: FlowLoadingState<Int> = ...
val state2: FlowLoadingState<Int> = ...
val combinedState: FlowLoadingState<Int> = state1.combineState(state2) { value1: Int, value2: Int ->
value1 + value2
}You can easily set the cache expiration time. Override expireSeconds variable in your Cacher<PARAM, DATA> class.
The default value is Long.MAX_VALUE (= will NOT expire).
object UserCacher : Cacher<UserId, UserData>() {
override val expireSeconds = 60 * 30 // expiration time is 30 minutes.
}If you want to make the cached data persistent, override the method of your Cacher<PARAM, DATA> class.
object UserCacher : Cacher<UserId, UserData>() {
override val expireSeconds = 60 * 30 // expiration time is 30 minutes.
// Save the data for each parameter in any store.
override suspend fun saveData(data: UserData?, param: UserId) {
...
}
// Get the data from the store for each parameter.
override suspend fun loadData(param: UserId): UserData? {
...
}
// Save the epoch time for each parameter to manage the expiration time.
// If there is no expiration time, no override is needed.
override suspend fun saveDataCachedAt(epochSeconds: Long, param: UserId) {
...
}
// Get the date for managing the expiration time for each parameter.
// If there is no expiration time, no override is needed.
override suspend fun loadDataCachedAt(param: UserId): Long? {
...
}
}This library includes pagination support.
Inherit PaginationCacher<PARAM, DATA> & PaginationFetcher<PARAM, DATA> instead of Cacher<PARAM, DATA> & Fetcher<PARAM, DATA>.
An example is shown below.
object UserListCacher : PaginationCacher<Unit, UserData>()
class UserListFetcher : PaginationFetcher<Unit, UserData> {
private val userListApi = UserListApi()
override suspend fun fetch(param: Unit): PaginationFetcher.Result<UserData> {
val fetched = userListApi.fetch(null, 20)
return PaginationFetcher.Result(data = fetched.data, nextRequestKey = fetched.nextPageToken)
}
override suspend fun fetchNext(nextKey: String, param: Unit): PaginationFetcher.Result<UserData> {
val fetched = userListApi.fetch(nextKey.toLong(), 20)
return PaginationFetcher.Result(data = fetched.data, nextRequestKey = fetched.nextPageToken)
}
}You need to additionally implements fetchNext(nextKey: String, param: PARAM).
And then, You can get the state of additional loading from the next parameter of onCompleted {}.
val userFlowable = StoreFlowable.from(userListCacher, userListFetcher)
userFlowable.publish().collect {
it.doAction(
onLoading = { contents: List<UserData>? ->
// Whole (Initial) data loading.
},
onCompleted = { contents: List<UserData>, next: AdditionalLoadingState, _ ->
// Whole (Initial) data loading completed.
next.doAction(
onFixed = { canRequestAdditionalData: Boolean ->
// No additional processing.
},
onLoading = {
// Additional data loading.
},
onError = { exception: Exception ->
// Additional loading error.
}
)
},
onError = { exception: Exception ->
// Whole (Initial) data loading error.
}
)
}On Android, To display in the RecyclerView, Please use the difference update function. See also DiffUtil.
You can request additional data for paginating using the requestNextData() method.
interface PaginationStoreFlowable<DATA> {
suspend fun requestNextData(continueWhenError: Boolean = true)
}The GithubOrgsCacher + GithubOrgsFetcher or GithubReposCacher + GithubReposFetcher classes in example module implement pagination.
This library also includes two-way pagination support.
Inherit TwoWayPaginationCacher<PARAM, DATA> & TwoWayPaginationFetcher<PARAM, DATA> instead of Cacher<PARAM, DATA> & Fetcher<PARAM, DATA>.
You can request additional data for paginating using the requestNextData(), requestPrevData() method.
interface TwoWayPaginationStoreFlowable<DATA> {
suspend fun requestNextData(continueWhenError: Boolean = true)
suspend fun requestPrevData(continueWhenError: Boolean = true)
}The GithubTwoWayReposCacher + GithubTwoWayReposFetcher classes in example module implement two-way pagination.
This project is licensed under the Apache-2.0 License - see the LICENSE file for details.



