Providing offline capabilities to your Android app using NetworkBoundResource

Liviu Iuga
Zipper Studios
Published in
5 min readAug 5, 2019

Last time I wrote an article, my boss told me I made it look too easy. Guilty as charged, I’ve decided to kick it up a notch and dive even deeper into the mysterious mechanisms that help us build an awesome app that can function both online and offline, switching seamlessly between those two states.

This time I approached the same topic from my previous article, but in a different manner. This time, I went down the road of Android Architecture Components.

NetworkBoundResource — what it is and what it does

If you love a well-written class, you’ll love NetworkBoundResource. Although it is not a part of Android’s official components, you will find it in Google’s Github Samples. This little gem of code handles a lot of the work that we were supposed to do until now. Not anymore!

Remember all the edge cases we had to cover in order to make an app function properly? If you took a close look at the Gist above, you should have noticed that using this helper class only requires us to overwrite 4 functions:

  • saveCallResult(item: RequestType) — This is where we will use Room’s DAO to save locally whatever data we just fetched from the server
  • shouldFetch(data: ResultType?): Boolean — This determines whether the app should fetch new data from the server and update the data already stored on our local device database.
  • loadFromDb(): LiveData<ResultType> — This method loads the data saved locally in Room
  • createCall(): LiveData<ApiResponse<Response>> — Here we will call a function that will fetch data from the server

Note: there are two parameters used (ResultType and RequestType). This is very helpful since the data type returned from the API might not match the data type used locally.

The Repository class can become pretty crowded in a big app so in order to maintain a clean and sustainable code, instead of having multiple functions (one for fetching data from API, one for loading data from local DB, one for making the decision of data source that will be used to display data to UI, so on and so on..), now we can write just one function as below:

fun loadRepos(): LiveData<Resource<List<Task>>> {
return object : NetworkBoundResource<List<Task>, Response>(appExecutors) {
override fun saveCallResult(item: Response) {
item.taskList?.let {
db.toDoDao().insertTasks(it)
}
}
override fun shouldFetch(data: List<Task>?): Boolean {
return true
}
override fun loadFromDb(): LiveData<List<Task>> {
return db.toDoDao().getAllTasks()
}
override fun createCall(): LiveData<ApiResponse<Response>> {
return getBooks()
}
}.asLiveData()
}

This method will be called from the activity’s ViewModel. Since it returns a LiveData object, it can be observed in the activity and the UI will be updated.

If you worked with Firebase before, you probably know about their ValueEventListeners. However, those methods work asynchronously, which is not very helpful in our case. That’s why in order to keep the same firebase database and avoid making big changes to the already existing project, I opted for Firebase’s REST API.

Basically, you can use any Firebase Database URL as a REST endpoint. All you need to do is append “.json” to the end of the URL. This way you can use Retrofit (or your favorite HTTP client) to create those needed API calls. On the left, you can see the structure of my Firebase database. This means my API URL will look something like “/ToDoList.json”.

Let’s run the app and see what happens. I already have some values in my database so they should be displayed when first starting the app.

Success!

Now, if I insert another task, my value will be inserted in Firebase, but my app won’t update the tasks list even though I set the shouldFetch method to always return true. Why is that? Well, let’s take a closer look at NetworkBoundResource. We’re looking for shouldFetch…And there it is:

init {
result.value = Resource.loading(null)
@Suppress("LeakingThis")
val dbSource = loadFromDb()
result.addSource(dbSource) { data ->
result.removeSource(dbSource)
if (shouldFetch(data)) {
fetchFromNetwork(dbSource)
} else {
result.addSource(dbSource) { newData ->
setValue(Resource.success(newData))
}
}
}
}

Using the debugger provided by Android Studio I noticed the value returned by the shouldFetch method is checked only on the init of the class and it happens only once when I start my app. Why is that? It should happen on every onResume of the MainActivity. The code in my View Model looks like below:

var itemsList: LiveData<Resource<List<Task>>> = repository.loadRepos()

fun onResume(){
itemsList = repository.loadRepos()
}

Confused as to why my code doesn't work as expected, I reached out to Vlad Negrea who came up with an interesting idea: Transformations. This is a class for LiveData and provides 2 static methods that make sure the information is carried across the observer's lifecycle. In my case, the LiveData object _refreshItems works as a trigger and the function triggered is repository.loadRepos().

private val _refreshItems = MutableLiveData<Unit>()

var itemsList: LiveData<Resource<List<Task>>> = Transformations.switchMap(_refreshItems) { repository.loadRepos() }
fun onResume() {
_refreshItems.value = Unit
}

Running this project again, this time it works as expected. Whenever MainActivity is resumed, the task list is updated and displayed. And yeah, that's how we roll at Zipper Studios, we got each other's backs.

You can take a better look at this project here. And don't forget to clap if you found something valuable in this article.

Zipper Studios is a group of passionate engineers helping startups and well-established companies build their mobile products. Our clients are leaders in the fields of health and fitness, AI, and Machine Learning. We love to talk to likeminded people who want to innovate in the world of mobile so drop us a line here.

--

--