Kotlin Flow "First Offline" approach

Viewed 933

I am developing a brand new android app and i wanted to implement an Offline-First strategy using Kotlin Flow just like we do it using RxJava. i use below code for the offline-first functionality when i use Rx.

private fun getOfflineFirst(param: Param): Observable<T> =
    Observable.concatArrayEagerDelayError(
        getOffline(param), getRemote(param)

getOffline and getRemote functions will return an observable object. and i use below code to achieve same result using Flow.

private suspend fun getOfflineFirst(param: Param) = flow {
    getLocal(param)
        .onCompletion {
            getRemote(param).collect {
                emit(it)
            }
        }.collect { emit(it) }
}

getLocal and getRemote will return a Flow object. also i use another logic in one of my playground projects like below:

    suspend fun getResult(param: PARAM, strategy: QueryStrategy): Flow<ResultResponse> = flow {
    if (strategy.isRemote()) {
        emit(getRemoteResult(param))
    } else {
        emit(getLocalResult(param))
        emit(getRemoteResult(param))
    }
}

In the "else" section it's going to emit the local result then the remote, that's how i handled first-offline in this scenario.

But i am not sure if i am using the best approaches available.

Can someone suggest me some better approaches?

3 Answers

Use this abstract class to handle data fetching and storing.

  /**
  * A repository which provides resource from local database as well as remote 
   endpoint.
  *
  * [RESULT] represents the type for database.
  * [REQUEST] represents the type for network.
  */
  @ExperimentalCoroutinesApi
  abstract class NetworkBoundRepository<RESULT, REQUEST> {

   fun asFlow() = flow<State<RESULT>> {

    // Emit Loading State
    emit(State.loading())

    try {
        // Emit Database content first
        emit(State.success(fetchFromLocal().first()))

        // Fetch latest posts from remote
        val apiResponse = fetchFromRemote()

        // Parse body
        val remotePosts = apiResponse.body()

        // Check for response validation
        if (apiResponse.isSuccessful && remotePosts != null) {
            // Save posts into the persistence storage
            saveRemoteData(remotePosts)
        } else {
            // Something went wrong! Emit Error state.
            emit(State.error(apiResponse.message()))
        }
    } catch (e: Exception) {
        // Exception occurred! Emit error
        emit(State.error("Network error! Can't get latest data."))
        e.printStackTrace()
    }

    // Retrieve posts from persistence storage and emit
    emitAll(fetchFromLocal().map {
        State.success<RESULT>(it)
    })
}

/**
 * Saves retrieved from remote into the persistence storage.
 */
@WorkerThread
protected abstract suspend fun saveRemoteData(response: REQUEST)

/**
 * Retrieves all data from persistence storage.
 */
@MainThread
protected abstract fun fetchFromLocal(): Flow<RESULT>

/**
 * Fetches [Response] from the remote end point.
 */
@MainThread
protected abstract suspend fun fetchFromRemote(): Response<REQUEST>
}

and in your repo class pass your api interface and database repo

class Repository(private val api: ApiInterface, private val db: DBRepository) {

/**
 * Fetched the posts from network and stored it in database. At the end, data from 
 persistence
 * storage is fetched and emitted.
 */
fun getAllArticles(): Flow<State<List<Article>>> {
    return object : NetworkBoundRepository<List<Article>, ArticlesResponse>() {

        override suspend fun saveRemoteData(response: ArticlesResponse) =
                db.getNewsDao().insertALLItems(response.article!!)

        override fun fetchFromLocal(): Flow<List<Article>> = 
      db.getNewsDao().getItems()

        override suspend fun fetchFromRemote(): Response<ArticlesResponse> = 
        api.getArticles()

      }.asFlow().flowOn(Dispatchers.IO)
    }
 }

First, it depends how much variability you would like to have. Second, there is Dropbox's Store solution and it is more universal (and complex) solution for these issues.

I do not understand much your first function getOfflineFirst, it could be done similarly as in your second example in else branch.

I would not suggest that approach. First it would not be a good choice to collect() from another flow in another flow. Second, you have to decide between Rx and Flow you don't need both of them.

You can always use the map() method for that. I would do something like this:

getData() = getLocal(param)
   .map{ whatGetLocalReturns ->
     return@map getRemote(param)
    }
   .map{ whatGetRemoteReturns ->
     return@map decideResult(whatGetRemoteReturns, strategy)
   }

And then you can do:

getData().collect{ result ->
  // there you go :) 
}

I have skipped some details but I believe you can figure out what I exactly mean. Don't forget the threading and everything of course.

Related