HomeLearnArticleRealm Kotlin 0.4.1 announcement

Realm Kotlin 0.4.1 announcement

Updated: Jul 16, 2021 |

Published: Jul 16, 2021

  • Realm
  • Mobile
  • Kotlin
  • ...

By Christian Melchior

Rate this article

In this blogpost we are announcing v0.4.1 of the Realm Kotlin Multiplatform SDK. This release contains a significant architectural departure from previous releases of Realm Kotlin as well as other Realm SDK’s, making it much more compatible with modern reactive frameworks like Kotlin Flows. We believe this change will hugely benefit users in the Kotlin ecosystem.

#Some background

The Realm Java and Kotlin SDK’s have historically exposed a model of interacting with data we call Live Objects. Its primary design revolves around database objects acting as Live Views into the underlying database.

This was a pretty novel approach when Realm Java was first released 7 years ago. It had excellent performance characteristics and made it possible to avoid a wide range of nasty bugs normally found in concurrent systems.

However, it came with one noticeable drawback: Thread Confinement.

Thread-confinement was not just an annoying restriction. This was what guaranteed that users of the API would always see a consistent view of the data, even across decoupled queries. Which was also the reason that Kotlin Native adopted a similar memory model

But it also meant that you manually had to open and close realms on each thread where you needed data, and it was impossible to pass objects between threads without additional boilerplate.

Both of which put a huge burden on developers.

More importantly, this approach conflicts with another model for working with concurrent systems, namely Functional Reactive Programming (FRP). In the Android ecosystem this was popularized by the RxJava framework and also underpins Kotlin Flows.

In this mode, you see changes to data as immutable events in a stream, allowing complex mapping and transformations. Consistency is then guaranteed by the semantics of the stream; each operation is carried out in sequence so no two threads operate on the same object at the same time. In this model, however, it isn’t uncommon for different operations to happen on different threads, breaking the thread-confinement restrictions of Realm.

Looking at the plethora of frameworks that support this model (React JS, RxJava, Java Streams, Apple Combine Framework and Kotlin Flows) It becomes clear that this way of reasoning about concurrency is here to stay.

For that reason we decided to change our API to work much better in this context.

#The new API

So today we are introducing a new architecture, which we internally have called the Frozen Architecture. It looks similar to the old API, but works in a fundamentally different way.

Realm instances are now thread-safe, meaning that you can use the same instance across the entire application, making it easier to pass around with e.g. dependency injection.

All query results and objects from the database are frozen or immutable by default. They can now be passed freely between threads. This also means that they no longer automatically are kept up to date. Instead you must register change listeners in order to be notified about any change.

All modifications to data must happen by using a special instance of a MutableRealm, which is only available inside write transactions. Objects inside a write transaction are still live.

#Opening a Realm

Opening a realm now only needs to happen once. It can either be stored in a global variable or made available via dependency injection.

1// Global App variable
2class MyApp: Application() {
3 companion object {
4 private val config = RealmConfiguration(schema = setOf(Person::class))
5 public val REALM = Realm(config)
6 }
7}
8
9
10
11// Using dependency injection
12val koinModule = module {
13 single { RealmConfiguration(schema = setOf(Person::class)) }
14 single { Realm(get()) }
15}
16
17// Realms are now thread safe
18val realm = Realm(config)
19val t1 = Thread {
20 realm.writeBlocking { /* ... */ }
21}
22val t2 = Thread {
23 val queryResult = realm.objects(Person::class)
24}

You can now safely keep your realm instance open for the lifetime of the application. You only need to close your realm when interacting with the realm file itself, such as when deleting the file or compacting it.

1// Close Realm to free native resources
2realm.close()

#Creating Data

You can only write within write closures, called write and writeBlocking. Writes happen through a MutableRealm which is a receiver of the writeBlocking and write lambdas.

Blocking:

1val jane = realm.writeBlocking {
2 val unmanaged = Person("Jane")
3 copyToRealm(unmanaged)
4}

Or run as a suspend function. Realm automatically dispatch writes to a write dispatcher backed by a background thread, so launching this from a scope on the UI thread like viewModelScope is safe:

1CoroutineScope(Dispatchers.Main).launch {
2
3 // Write automatically happens on a background dispatcher
4 val jane = realm.write {
5 val unmanaged = Person("Jane")
6 // Add unmanaged objects
7 copyToRealm(unmanaged)
8 }
9
10 // Objects returned from writes are automatically frozen
11 jane.isFrozen() // == true
12
13 // Access any property.
14 // All properties are still lazy-loaded.
15 jane.name // == "Jane"
16}

#Updating data

Since everything is frozen by default, you need to retrieve a live version of the object that you want to update, then write to that live object to update the underlying data in the realm.

1CoroutineScope(Dispatchers.Main).launch {
2 // Create initial object
3 val jane = realm.write {
4 copyToRealm(Person("Jane"))
5 }
6
7 realm.write {
8 // Find latest version and update it
9 // Note, this always involves a null-check
10 // as another thread might have deleted the
11 // object.
12 // This also works on objects without
13 // primary keys.
14 findLatest(jane)?.apply {
15 name = "Jane Doe"
16 }
17 }
18}

#Observing Changes

Changes to all Realm classes are supported through Flows. Standard change listener API support is coming in a future release.

1val jane = getJane()
2CoroutineScope(Dispatchers.Main).launch {
3 // Updates are observed using Kotlin Flow
4 val flow: Flow<Person> = jane.observe()
5 flow.collect {
6 // Listen to changes to the object
7 println(it.name)
8 }
9}

As all Realm objects are now frozen by default, it is now possible to pass objects between different dispatcher threads without any additional boilerplate:

1val jane = getJane()
2CoroutineScope(Dispatchers.Main).launch {
3
4 // Run mapping/transform logic in the background
5 val flow: Flow<String> = jane.observe()
6 .filter { it.name.startsWith("Jane") }
7 .flowOn(Dispatchers.Unconfined)
8
9 // Before collecting on the UI thread
10 flow.collect {
11 println(it.name)
12 }
13}

#Pitfalls

With the change to frozen architecture, there are some new pitfalls to be aware of:

Unrelated queries are no longer guaranteed to run on the same version.

1// A write can now happen between two queries
2val results1: RealmResults<Person> = realm.objects(Person::class)
3val results2: RealmResults<Person> = realm.objects(Person::class)
4
5// Resulting in subsequent queries not returning the same result
6results1.version() != results2.version()
7results1.size != results2.size

We will introduce API’s in the future that can guarantee that all operations within a certain scope are guaranteed to run on the same version. Making it easier to combine the results of multiple queries.

Depending on the schema, it is also possible to navigate the entire object graph for a single object. It is only unrelated queries that risk this behaviour.

Storing objects for extended periods of time can lead to Version Pinning. This results in an increased realm file size. It is thus not advisable to store Realm Objects in global variables unless they are unmanaged.

1// BAD: Store a global managed object
2MyApp.GLOBAL_OBJECT = realm.objects(Person::class).first()
3
4// BETTER: Copy data out into an unmanaged object
5val person = realm.objects(Person::class).first()
6MyApp.GLOBAL_OBJECT = Person(person.name)

We will monitor how big an issue this is in practise and will introduce future API’s that can work around this if needed. It is currently possible to detect this happening by setting RealmConfiguration.Builder.maxNumberOfActiveVersions()

Ultimately we believe that these drawbacks are acceptable given the advantages we otherwise get from this architecture, but we’ll keep a close eye on these as the API develops further.

#Conclusion

We are really excited about this change as we believe it will fundamentally make it a lot easier to use Realm Kotlin in Android and will also enable you to use Realm in Kotlin Multilplatform projects.

You can read more about how to get started at https://docs.mongodb.com/realm/sdk/kotlin-multiplatform/. We encourage you to try out this new version and leave any feedback at https://github.com/realm/realm-kotlin/issues/new. Sample projects can be found here.

The SDK is still in alpha and as such none of the API’s are considered stable, but it is possible to follow our progress at https://github.com/realm/realm-kotlin.

If you are interested about learning more about how this works under the hood, you can also read more here

Happy hacking!

Rate this article
MongoDB Icon
  • Developer Hub
  • Documentation
  • University
  • Community Forums

© MongoDB, Inc.