Offline First Architecture
There may be many meanings tied to the "offline-first" architecture. The definition here means the app always loads from the cache "first" for the fast performance. Once the data from the cache is displayed, it updates again the difference (if any) using the data from the server.
Result:
Even if getting information from the server is slow, the app instantly loads the screen from the cache instead of showing the "loading" screen. Also, if the device is offline, the app will still work as long as there is a cache data (the page has been loaded before).
Folder Structure
I have grouped Fragment & ViewModel into one folder but separated the Repository into different folders.
Reason:
In most cases you will have 1-to-1 ViewModel per Fragment. However, sometimes data(repository) can be shared between different View/ViewModels.
For example, both User List and User Detail screens share the same User data.
ViewModel <-> Repository Relationship
With the typical MVVM architecture, the Repository offers the suspend function and the ViewModel will have the LiveData calling for the data.
Flow
Flow is similar to RxJava. The learning curve, in my opinion, is lower than the RxJava. Like RxJava, Flow also offers very useful operators like zip, combine, flatMapConcat, flatMapMerge, debounce, onStart, onComplete, etc. The fact that Flow can be directly converted into LiveData makes it really easy to adapt if your project already uses the MVVM architecture with LiveData.
Two-tier Cache
I use two-tier cache:
1. Memory Cache
2. Local Cache (DB or SharedPreference)
There are advantage and disadvantage of using two-tier cache:
Advantage:
- Speed: Even reading the local DB takes time. It requires you to switch the worker thread. Coroutine has made the thread switch much faster than the original way but it can still cause the UI flickering issue during the page load. The flickering issue is more evident when the placeholder UI is not empty and the height is different from the UI with data. This is caused by the thread switch: UI (show placeholder UI / trigger event) -> Worker (read data) -> UI (display result). Reading from the memory can be done on UI thread without switching the threads.
Disadvantage:
- Complexity: It adds an extra complexity to maintain two caches up-to-date with the latest data.
- Out of memory error: If you're working with the large size data, having all of them to be saved onto the memory cache does not make sense. They will eventually add up and the app will run out of memory. So you have to define the "scope" of how much and how long data should be saved on to memory. For example, you can create the memory cache as a LRU memory: only saving the last used data up-to the cache size pre-defined. You can also set a time limit. Some data is no longer valid after a certain time. You can wipe the cache data after a certain time so that the new data from the server has to be called.
2 Types Of Read Call
There are two types read call:
- Cache-and-server: First read from the cache (either memory or local) and return the result. Then read from the server and update the difference.
- Cache-or-server: If data from the cache exists, then return the cache data. If cache has no data, then return the data from the server.
Cache-and-server
Following is a simple example cache-and-server read using Flow.
override fun getUsers(): Flow<ArrayList<User>?> {
return flow {
// To show the data quickly as possible, first show data from cache, then from server
var hasCacheData = false
// 1. Emit first from either from memory or local
// Memory
if (currentUserList != null) {
hasCacheData = true
emit(currentUserList!!)
} else {
// Local
val localCache = myDao.getUserList()
if (localCache.isNotEmpty()) {
val users =
ServiceLocator.getGson()
.fromJson(localCache[0].payload, UserList::class.java)
if (users?.value != null) {
// Save to memory
currentUserList = users.value!!
hasCacheData = true
emit(users.value!!)
}
}
}
// 2. Emit again from the network (for the latest update)
// Network
val apiResult = ServiceLocator.provideJsonHolderService().getUsers()
// Saving to DB takes time. Create another thread to save to DB.
GlobalScope.launch(Dispatchers.IO) {
saveToLocal(apiResult)
}
// Do not overwrite cache value with null network value (offline case)
// If cache has no data, then it's ok to return null.
if (apiResult != null || !hasCacheData)
emit(apiResult)
}.flowOn(Dispatchers.IO)
}
Cache-and-server method is useful for the first time launch. The app can display the screen almost instantly with the data in cache and then apply the difference soon after (if any). Also, since it displays data from cache first, the app displays the result fast even if the device is offline or in a low connectivity state.
Cache-or-server
Following is a simple example cache-or-server read using Flow.
override fun getUsersAllowCache(): Flow<ArrayList<User>?> {
return flow {
// If cache is found, don't try to download from server
// 1. Memory
if (currentUserList != null) {
emit(currentUserList!!)
} else {
// 2. Local
var hasLocalData = false
val localCache = myDao.getUserList()
if (localCache.isNotEmpty()) {
val users =
ServiceLocator.getGson()
.fromJson(localCache[0].payload, UserList::class.java)
if (users?.value != null) {
// Save to memory
currentUserList = users.value!!
hasLocalData = true
emit(users.value!!)
}
}
if (!hasLocalData) {
// 3. Network
val apiResult = ServiceLocator.provideJsonHolderService().getUsers()
// Saving to DB takes time. Create another thread to save to DB.
GlobalScope.launch(Dispatchers.IO) {
saveToLocal(apiResult)
}
emit(apiResult)
}
}
}.flowOn(Dispatchers.IO)
}
Cache-or-server method is useful for the screen re-entry or when you know you don't need an updated value from the server.
Continuous Update From Server
Examples like current stock price or a number of likes in a post requires to be frequently auto refreshed. This can be done using the while loop and the delay:
override fun getUsers(): Flow<ArrayList<User>?> {
return flow {
// To show the data quickly as possible, first show data from cache, then from server
var hasCacheData = false
// 1. Emit first from either from memory or local
// Memory
if (currentUserList != null) {
hasCacheData = true
emit(currentUserList!!)
} else {
// Local
val localCache = myDao.getUserList()
if (localCache.isNotEmpty()) {
val users =
ServiceLocator.getGson()
.fromJson(localCache[0].payload, UserList::class.java)
if (users?.value != null) {
// Save to memory
currentUserList = users.value!!
hasCacheData = true
emit(users.value!!)
}
}
}
// 2. Emit again from the network (for the latest update)
// If API call is like a current stock price, we should auto refresh every 1 seconds. Then we use "while" loop with "delay(1000)"
while (true) {
// Network
val apiResult = ServiceLocator.provideJsonHolderService().getUsers()
// Saving to DB takes time. Create another thread to save to DB.
GlobalScope.launch(Dispatchers.IO) {
saveToLocal(apiResult)
}
// Do not overwrite cache value with null network value (offline case)
// If cache has no data, then it's ok to return null.
if (apiResult != null || !hasCacheData)
emit(apiResult)
delay(1000)
}
}.flowOn(Dispatchers.IO)
}
The while loop will stop when there is no more observer (i.e. screen change).
ViewModel
The ViewModel code can be as simple as following:
fun getUserList(): LiveData<ArrayList<User>?> = userRepo.getUsers().asLiveData()
Use .asLiveData() to convert a Flow into a LiveData.
Using .onStart(), .onEach(), and .onComplete(), the viewModel can display the spinner:
fun getUserList(): LiveData<ArrayList<User>?> = userRepo.getUsers()
.onStart { _spinner.value = true }
.onEach { _spinner.value = false }
.catch { e -> Timber.e(e) }
.asLiveData()
Also, the data can be transform into another type using .map() inside the viewModel:
fun getUserDetail(id: String): LiveData<User?> =
userRepo.getUsersAllowCache()
.map {
if (it != null) {
for (user in it) {
if (user.id == id) {
return@map user
}
}
}
return@map null
}.asLiveData()
Full Source Code
Following link is the source code of the Offline-first Architecture using MVVM with Flow Sample app.
https://github.com/jclova/MvvmWithFlow



android development company</a
ReplyDelete