Why Realm is a great persistence solution for beginners in Android development

Gabor Varadi
5 min readJan 8, 2018

--

If you haven’t heard about Realm, here’s a quick recap:

  • Realm is a database, which isn’t SQLite-based (it brings its own core)
  • it’s generally called an “object database”: your objects are mapped directly, and many-relations are mapped directly as a list, instead of with JOINs across multiple tables
  • Realm not only handles storing your data, but it also keeps queried data up to date, and calls any registered change listeners when your data has been modified, allowing you to keep the UI up to date with minimal effort
  • Most importantly, due to lazy evaluation of RealmResults’ elements, you don’t need to implement pagination logic — just get a RealmResults, throw it in a RealmRecyclerViewAdapter, and it’s good to go — in that case, even the listener that keeps the RecyclerView updated is managed automatically.

But what does this look like in practice?

Setting things up

To use Realm, you need to add the Realm gradle plugin to your dependencies:

buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.realm:realm-gradle-plugin:4.3.1"
}
}

And apply the plugin:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android'

Add a useful helper:

dependencies {
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
implementation
'io.realm:android-adapters:2.1.1'
}

Create an application class where Realm is initialized…

class CustomApplication: Application() {
override fun onCreate() {
super.onCreate()
Realm.init(this)
Realm.setDefaultConfiguration(
RealmConfiguration.Builder()
.deleteIfMigrationNeeded()
.build())
}
}
<application android:name=".CustomApplication"

Then you can create a class that will contain the schema for how you want to save the data.

Once we have that, we can store data, pretty much as one would expect:

We can now query this data, and even add change listener to it

And that’s pretty much the basics for using Realm!

What are some other cool things you can do with Realm?

You can do additional filtering on existing RealmResults (query results) or RealmList.

val person: Person? = realm.where<Person>()
.equalTo(PersonFields.NAME, "John")
.findFirst()
person?.dogs?.let { dogs ->
val filteredDogs = dogs.where()
.greaterThan(DogFields.AGE, 4)
.findAll()
}

Using realm-android-adapters, we can use RealmRecyclerViewAdapter to keep the UI up to date even when the database is written to on a background thread, without any additional effort.

Internally, it calls adapter.notifyItem* methods instead of adapter.notifyDataSetChanged(), so we don’t need to handle DiffUtil and doing the change evaluation on a background thread — Realm does that automatically, thanks to its fine-grained listeners.

implementation 'io.realm:android-adapters:2.1.1'class PersonAdapter(val results: RealmResults<Person>): 
RealmRecyclerViewAdapter<Person, PersonViewHolder>(results, true) {
...
}

What to look out for?

When using Realm, it helps to know some caveats so that you know what are things you cannot do.

  • RealmObjects cannot extend any classes that are not specifically RealmObject — which means you can’t inherit fields, and your RealmObject classes cannot inherit from other RealmObjects.
  • You can only store the field types that are allowed (…, boolean, String, Date, Long, Double, ByteArray, specific RealmObject types, RealmList)
    Using @Ignore lets you add unsupported fields to the class, but that is not stored, of course.
  • Your RealmObject classes define the underlying schema, so if you modify the fields (or their attributes), unless you drop/recreate with deleteIfMigrationNeeded(), you’ll also need to track these changes with schema version bump, and with migration
    (which is like ALTER TABLE statements in onUpgrade() using SQLite, except it’s done with the DynamicRealm API, for example realmSchema.get("Dog").addIndex("age"))
  • Cascade behavior is done by-hand (manual)
  • Realm instances (and anything obtained from it — queries, objects, query results) are thread-local, so when you do the background write, you need a Realm instance for that (which you close via use {.) When you set up the auto-refreshing query, you need a Realm instance for that, which you obtained on the UI thread.
    So you can’t just use a singleton Realm instance for the whole app, and you can’t toss RealmResults and managed objects between threads.

And most importantly:

  • You should never ever leave a Realm instance open once you’re done with it on a background thread. Realm.getInstance() sounds like it gives you a single instance, but it also increases a reference count. So you should always make sure Realm.getInstance() is paired with close() somewhere.
    You can also use .use { (or try(Realm realm = … in Java), like in the above example. This is written down quite well in the docs, though.

Why would I use Realm instead of SQLite/DbFlow/Requery/SQLDelight/Room?

Because Realm requires less knowledge of how relational databases are supposed to be designed, and how to manage your relations.

Even with configuration-based helpers like in Room, relation management can be tricky compared to links in Realm. Here’s a join table in Room (based on this article):

@Entity
data class Person(@PrimaryKey val id: Int) {
}
@Entity(foreignKeys = @ForeignKey(entity = Person.class,
parentColumns = "id",
childColumns = "personId",
onDelete = ForeignKey.CASCADE))
data class Dog(@PrimaryKey val id: Int) {
}
@Entity(tableName = "user_dog_join",
primaryKeys = ["personId", "dogId"],
foreignKeys = [
@ForeignKey(entity = Person.class,
parentColumns = "id",
childColumns = "personId"),
@ForeignKey(entity = Dog.class,
parentColumns = "id",
childColumns = "dogId")
])
data class PersonDogJoinTable(val userId: Int, val dogId: Int) {
}

Or here’s how you get data through relations, with Room’s @Relation and @Embedded annotation (note the new class to describe this data):

class PersonWithDogs {
@Embedded var person: Person

@Relation(parentColumn = "id",
entityColumn = "personId") var
dogList: List<Dog>
}

— — — — — — — — — — — —

In the Realm sample above, all this magic (excluding automatic cascade on deletion), was just:

open class Person : RealmObject() {
var dogs: RealmList<Dog>? = null
}

And the optional inverse relation from dog was:

open class Dog: RealmObject() {
@LinkingObjects("dogs")
val owners: RealmResults<Person>? = null
}

So that’s a bit less configuration for similar behavior.

Also, you don’t really need to think about “how will I obtain my query results on a background thread and then pass it to the UI thread”, because findAllAsync() and addChangeListener()/removeChangeListener() are generally sufficient for this.

Conclusion

Using Realm simplifies certain aspects of data persistence, and managing when to update our views to reflect the latest data.

While RealmObjects themselves can store only a limited number of data types, and there is no out-of-the-box support for type adapters (other than your own custom getters/setters)— for many simpler use-cases, especially where one needs to download and display data (and keep the UI updated when that happens), Realm is a solution that makes this kind of “queryable cache” behavior near-trivial.

In cases where setting up relations and join tables might be overkill, Realm can be a solution — and the way it expects and somewhat enforces the usage of change listeners to keep your UI in sync, eventually makes you end up with a reactive local data source in your data layer, making the disk the single source of truth — which is exactly what the Android Architecture Components guide describes as the recommendation for any data repository.

With that in mind, I think if you obey the simple rules — close Realm instances on background thread, open/close the Realm per thread, and use addChangeListener()— then Realm is a great persistence solution for beginners in Android development.

--

--

Gabor Varadi
Gabor Varadi

Written by Gabor Varadi

Android dev. Zhuinden, or EpicPandaForce @ SO. Extension function fan #Kotlin, dislikes multiple Activities/Fragment backstack.

Responses (1)