Skip to content

kazakago/StoreFlowable.kt

Repository files navigation

StoreFlowable.kt

Maven Central javadoc Test License

Repository pattern support library for Kotlin with Coroutines & Flow.
Available for Android or any Kotlin/JVM projects.

Related projects

Overview

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.

https://user-images.githubusercontent.com/7742104/125610947-516c9508-f7fd-4466-81a8-c3ee159cb141.jpg

The following is an example of screen display using LoadingState.

https://user-images.githubusercontent.com/7742104/125714730-381eee65-4126-4ee8-991a-7fc64dfb325c.jpg

Install

Add the following gradle dependency exchanging *.*.* for the latest release. Maven Central

implementation("com.kazakago.storeflowable:storeflowable:*.*.*")

Optional: if you use LoadingState class and related functions only.

implementation("com.kazakago.storeflowable:storeflowable-core:*.*.*")

Get started

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.

1. Create a class to manage the in-app cache

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.

2. Create a class to get data from origin server

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.

3. Build StoreFlowable from Cacher & Fetcher 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.

4. Subscribe FlowLoadingState<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.

Example

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.

Other usage of StoreFlowable<DATA> class

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.

Refresh data

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()
}

Validate cache data

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()
}

Update cache data

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?)
}

FlowLoadingState<DATA> operators

Map FlowLoadingState<DATA>

Use mapContent(transform) to transform content in FlowLoadingStates<DATA>.

val state: FlowLoadingState<Int> = ...
val mappedState: FlowLoadingState<String> = state.mapContent { value: Int ->
    value.toString()
}

Combine multiple FlowLoadingState<DATA>

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
}

Manage Cache

Manage cache expire time

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.
}

Persist data

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? {
        ...
    }
}

Pagination support

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.

Request additional data

You can request additional data for paginating using the requestNextData() method.

interface PaginationStoreFlowable<DATA> {
    suspend fun requestNextData(continueWhenError: Boolean = true)
}

Pagination Example

The GithubOrgsCacher + GithubOrgsFetcher or GithubReposCacher + GithubReposFetcher classes in example module implement pagination.

Two-Way pagination support

This library also includes two-way pagination support.

Inherit TwoWayPaginationCacher<PARAM, DATA> & TwoWayPaginationFetcher<PARAM, DATA> instead of Cacher<PARAM, DATA> & Fetcher<PARAM, DATA>.

Request next & previous 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)
}

Two-Way pagination Example

The GithubTwoWayReposCacher + GithubTwoWayReposFetcher classes in example module implement two-way pagination.

License

This project is licensed under the Apache-2.0 License - see the LICENSE file for details.

About

Repository pattern support library for Kotlin with Coroutines & Flow.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages