Safe-Integration of Realm in Android production code, Part-1 with MVP
Realm 1.1.x is here and this post covers,
- Why Realm?
- Safe integration v/s Deep integration
- Steps to follow, when doing safe integration of Realm in production code, in combination with MVP(Model View Presenter) design pattern, Dagger, Rx-Java, Retrofit & Annotations.
- Unit Testing
Persistent storage in Android applications took a handful of turns like SQLite & ORM (Object Relational Mapping) before we were introduced to Realm.
Realm is fast, secure, free, easy to learn and integrate. More info here.
A detailed explanation on Realm internals, by Christian Melchior is here.
Bringing a new library to your production code or replacing a library with another, can be tricky and Realm is no exception. If you are planning to bring Realm to your code base or to replace SQLite or ORM with Realm, then consider the following best practices.
Safe Integration v/s Deep Integration
Just like any other major library, Realm has its own list of limitations. We can not pass Realm, RealmObject or RealmResult across threads. This makes it difficult to integrate Realm in a well tested production code.
We will talk about 2 different approaches of Realm integration, Safe integration and Deep integration.
Safe integration is where we completely isolate Realm from rest of the codebase. This approach is useful for production codes with a lot of code.
- It is easy to integrate Realm in production code
- It makes Realm a pluggable module, with complete isolation
- It is easy to test and debug Realm code
- Existing code and code structure remains relatively as it is
- Minimal refactoring needed
- Less lines of code needed
- We miss out on Auto-update feature of Realm
- We have to use our own boilerplate code for auto-update feature
- We create a copy of the data, every time we make a Realm query (which is opposite of zero copy object store idea of Realm)
- Realm queries are relatively slower since we are making a copy
What is Auto-Update feature of Realm?
In Realm all RealmObjects and RealmResults are LIVE objects which means until we close the Realm instance, we keep getting a callback if the queried data is modified.
Let's say we have a view with list of data, if user makes changes on those data in some other view, usually we have to manually update the view with updated data, with Realm’s Auto-update feature we will get a callback and we can easily update the view without any boilerplate code.
Deep integration is where we heavily depend on Realm and use all of Realm’s features. This approach is useful if we are creating a new codebase or if we are willing to do heavy refactoring on our production code.
- We will be able to use Auto-update feature of Realm, which is a big plus
- Less boilerplate code
- All the queries we make are much faster than safe integration because we don’t make a copy
- All Realm queries needs less memory because of zero copy object store idea of Realm
- We create heavy dependency on Realm
- We have to do major refactoring on production code, which will involve, DAO, presenter and data layer.
- We have to pay extra attention to thread management
Steps for Deep Integration
Steps for Safe Integration
1. Isolation of model classes
There are two ways we can achieve isolation of model classes.
Use the same model class for realm+json+view
As discussed earlier RealmObject/RealmResult can not be passed across threads, which would be a problem if we use the same model class for view building and Realm storage, here is how,
- We make a database query in the background thread (to avoid ANR, Realm is really fast, but in this approach we need to make a copy, which slows down Realm query)
- We need to use the queried results on main thread to update the view
- Exception, because a. realm instance is closed and b. we are trying to access the result(s) from a different thread.
Solution : Realm provides a function called copyFromRealm to make an in memory deep copy of the result(s). Once we make a copy of the result(s), we can close the Realm instance and also pass the result(s) across the threads.
A sample model class (realm+json+view) looks like this,
And a sample query method looks like this,
Use a separate copy of model for realm+(json+view)
The other way would be to make a separate model copy for Realm, which is not recommended, because we need to maintain two separate copy of the same model class, but if we decide to go for a pure isolation, this is how it can be done.
The problem of not being able to pass RealmObject/RealmResults across the thread is still valid in this approach as well.
So, as a first step, we need to create a corresponding Realm model for each Json model. Here is how it looks like:
Next, add constructor in Realm model that takes Json model and convert to Realm model and vice-versa.
A sample Json model looks like this,
and a corresponding Realm model looks like this,
2. Dependency injection and Realm
When it comes to database, it is always advised to inject a singleton instance of database with application scope. Realm is an exception. Realm instance can not be passed across threads & we should pay extra attention to the life of a Realm instance. That is why we should handle lifecycle of each Realm instance in each DAO class itself.
Realm is quite secure and easy to configure using RealmConfiguration.
By default Realm stores data at /data/data/package_name/files/default.realm
To make the default.realm even more secure we can create a 64 byte encryption key and encrypt the data.
We should be very careful when we use an encryption key in production code. If we change the encryption key, we should clear the Realm and recreate Realm data, otherwise Realm would not be accessible with updated encryption key.
4. Isolation of Realm interaction from rest of the code base
Create a DAO class for each of the Realm model which handles all Realm interactions to isolate Realm from rest of the code base.
Since we do not want RealmObjects to be accessible outside of DAO, DAO class should provide methods to convert Realm to Json and Json to Realm. This means DAO class accepts Json model and provides Json model.
A sample DAO class looks like this,
5. Write operations in Realm
All write operations (add/edit/remove) in Realm must be wrapped in write transactions. This means that realm.beginTransaction() must be called before the operation and if the operation is successful realm.commitTransaction() must be called, if operation fails realm.cancelTransaction() must be called.
- We can handle realm transaction states manually, which looks like this,
Problem with manually handling the transaction is, if there is an exception during the write operation, we would have to catch it and manually call realm.cancelTransaction().
2. A better way of doing this is using executeTransaction(), which looks like this,
Realm implementation of executeTransaction() looks like this,
1. Realm model unit testing
Realm model & Json model testing is pretty straight forward.
A sample Json model test class looks like this,
and a corresponding Realm model test class looks like this,
2. Realm DAO unit testing
Testing with Realm is not very clear, requires a lot of boiler plate code and still under development.
Realm testing requires JUnit4, Robolectric, Mockito and PowerMock.
Here we are testing Realm query and clear operations,
Key takeaway with Safe integration approach is Isolation. Isolation of Realm(or any other library) keeps the code modular, readable, easy to test, upgrade & debug.
To achieve Realm isolation, keep a separate copy of Realm models, create a DAO for each Realm model & keep all Realm calculations in DAO files only.
Complete source code
Complete source code is available on GitHub.
A support project with ORM instead of Realm is available here.