In my previous article “Simple Android MVVM” I showcased how you can easily implement a MVVM architecture using RX and Kotlin.
There, I used a simple repository which was delivering data from the cache if available, and then fetching it from the API.
This works perfectly but it does not persist our data in a database, just in memory. In many cases when developing mobile applications, you are also required to provide offline access to the data. Imagine you are developing a news-reader app, and you’ll also want that your users can access the data while they are riding the tube, travelling by plane, or they are in any area without internet access. There are also cases when you want to display the stored data, while new data is loaded from the API. If this is not implemented or taken into account in your architecture from the beginning of the application, this can become a complex task, and a lot of changes might be required, which will impact the UI layer as well, leaving space for new potential bugs in your code. Luckily, the repository pattern solves exactly this problem, and we implemented it from the beginning in our app.
Why the Repository Pattern ?
- decouples the application from the data sources
- provides data from multiple sources (DB, API) without clients being concerned about this
- isolates the data layer
- single place, centralized, consistent access to data
- testable business logic via Unit Tests
- easily add new sources
So our repository now talks to the API data source and with the cache data source. We would now want to add another source for our data, a database source.
On Android, we have several options here :
- using pure SQLite (too much boilerplate)
- Realm ( too complex for our use case, we don’t need most of it’s features)
- GreenDao ( a good ORM, but I think they will focus the development on objectbox in the future)
- Room ( the newly introduced ORM from Google, good support for RXJava 2 )
I will be using for my example Room, the new library introduced by Google.
I know in my previous article I mentioned that the new Architecture Components seem complex, require too much boilerplate and are still in alpha and I would not use them. The good part about them is that they are split into modules, and you can easy use just one module (eg : use just Room, without using LiveData and Lifecycles).
What I like about Room is that it is small, focused, has a clean API and can easily be added to your project. It also has support for RxJava 2 (see this article), and hopefully in the future it will be made open source, and everybody can contribute to the rx-bindings and the library itself.
Let’s see how we can use Room in our project, with Kotlin and RX. First we need to add the Koltin annotation processor plugin, because Room uses annotation processing to generate the boilerplate code for accessing the DB.
apply plugin: ‘kotlin-kapt’
Next add the Room dependencies and the kapt dependency :
Next we just need to annotate our User data class, with room annotations :
Now we must declare our Database and our UserDao. For simplicity I kept them in the same file here :
To create our Database and get a reference to our UserDao we must use the Room(for simplicity I kept this in the Application class, but usually they are provided by a DI framework such as Dagger ) :
So as I mentioned earlier, it was very easy to add Room to our project. We just annotated our model class with @Entity, without the need to extend from any class, declared a interfaces for our DAO and that’s it.
Now let’s see how we can change our UserRepository, and add our Database source, the UserDao. For this example, we will drop our cache source, as it no longer makes sense to also keep the data in the memory cache, as the database access is fast enough. This is how our UserRepository look like now :
Pretty simple. We just use Observable.concatArray() and pass our 2 sources, the DB and the API Observables. This will first deliver to the subscribers the data from the DB, and secondly the data from the API. When the data is received from the API, we also trigger inside doOnNext() an async operation to store the data in our DB.
So this was really easy to implement and required no changes in the View. Just imagine having data access code, such as API access in your View, and now having to modify your View to also store and fetch data from a database. Also, imagine having to do this without RX, maybe using AsyncTasks and in a project that is already implemented 90%. It will not be fun, it could easily take weeks, and introduce tons of bugs.
Bonus : the beauty of RX
To highlight the beauty of RX, let’s imagine the next scenario: let’s suppose that if your app has connection, and the API response is fast enough (say under 400ms), you don’t want to display the data from the DB, just display the fresh data from the API. This would make sense, because if the API call is executed fast, maybe you don’t want to display the DB items, and then quickly change them with the new items. So how would you do this without RX ?… the code would be complex, hard to read, and error prone.
But with RX, all this can be achieved using the debounce() operator. We just apply the debounce() operator on the stream, inside our ViewModel, and this will not deliver our DB items, if the API can respond fast enough. Here is how our ViewModel will look :
You maybe noticed that we also added an extra statement onErrorReturn(), where we return an empty list , an error message and the error that occurred. This way if there was a NoConnectioException, maybe the View will have to display a warning or message, that data might no be up to date, and it is displayed from the DB / cache. Don’t forget, you can get the source code from GitHub.
Implementing a Repository Pattern for your data access, is a great idea, even if you are not using a MVVM architecture, and even if you don’t need data caching from the beginning. Implementing the repository from the beginning of your app / architecture can be really simple, especially using RX, it will not add extra development time or overhead and it can really pay off in the long run. If/when you need to add extra data sources, this can be done with minimum effort, and minimum impact on the rest of your app.