onCreate
As many of us might know, Realm has already introduced freezing objects. Personally, I have been waiting for long time for such feature. So, what actual problem does this solve?
A lot of us might have faced this issue:
Realm access from incorrect thread. Realm objects can only be accessed on the thread they were created.
I believe the error speaks for itself.
Realm < 7.0:
When you call Realm.getDefaultInstance()
you are not creating a new object every time you call this method. However, having it on every method is somehow ugly. In cases when you need the object in another thread, it is impossible to use it without a DTO helper data class.
Let’s consider the following case:
private lateinit var realm: Realm
...
override var onCreate(){
...
realm = Realm.getDefaultInstance() // Main thread
}
suspend fun doSomething() = withContext(Dispatchers.IO){
val result = realm.where<MyRealmObject>().findFirst() //Crash
result?.let {
Timber.d("${it.id}")
}
}
Every time we have to start a realm interaction, we have to call Realm.getDefaultInstance()
before, every time you are in a new thread. Otherwise, it would bring the error mentioned above. If you want a sinlge, app-scope Realm
instance, you have to make some workaround.
Same would have happened if you were to read an RealmObject
or a RealmResults<T>
:
private lateinit var realm: Realm
private var myObject: MyRealmObject? = null
...
override var onCreate(){
...
realm = Realm.getDefaultInstance() // Main thread
myObject = realm.where<MyRealmObject>().findFirst()
}
suspend fun doSomething() = withContext(Dispatchers.IO){
result?.let {
Timber.d("${it.id}") // Crash
}
}
Realm >= 7.0:
Since this would be a huge obstacle, especially to reactive programming, the Realm team is bringing a new actor in the game: The frozen Realm. Just a new object which can be used across threads, attached to the only instantiated Realm object.
private lateinit var liveRealm: Realm
private lateinit var frozenRealm: Realm
override var onCreate(){
...
realm = Realm.getDefaultInstance() // Main thread
frozenRealm = realm.freeze()
}
suspend fun doSomething() = withContext(Dispatchers.IO){
val result = frozenRealm.where<MyRealmObject>().findFirst()
result?.let {
Timber.d("${it.id}") // Works
}
}
You can do the above solution, or you can directly freeze the RealmResults<T>
or directly freeze the object (must be a RealmObject
). This would be even easier:
private lateinit var realm: Realm
private var myObject: RealmResults? = null
override var onCreate(){
...
realm = Realm.getDefaultInstance() // Main thread
myObject = realm.where<MyRealmObject>().findAll().freeze()
}
suspend fun doSomething() = withContext(Dispatchers.IO){
myObject?.let {
Timber.d("${it.id}") // Works
}
}
There are a few more additions to this new feature, but the docs are already perfect for that.
Are coroutines really necessary for this example?
Not at all, but coroutines give a nice presentation of thread switch with withContext
block. Realm has its own thread usage:
val result = realm.where<MyRealmObject>().findAllAsync()
The findAllAsync()
eliminate the necessity to have other kinds of thread pools. However, combining realm with Flow
(particularly callbackFlow
) would be one good choice if you want to go more reactive. Let us consider the following case: Select some data from the database and every time data changes, a new toast would appear.
The imperative way:
// In a ViewModel
fun getData(){
val result = realm.where<MyRealmObject>().findAllAsync()
result.addChangeListener{ result ->
_toastLiveData.value = ShowToastFlag
}
}
As an example is more than enough to eliminate usage of coroutines. But let’s consider this following case: Fetch a list of objects locally, and make a POST request on the network after serializing it to JSON.
suspend fun sendListToNetwork() = withContext(Dispatchers.IO){
val result = frozenRealm.where<MyRealmObject>().findAll()
// We are in another thread because of the Network Request and not because of Realm
result.addChangeListener{ mResult ->
val jsonStringResultObject = serialize(result.toList())
val response = request().postRequest(jsonStringResult).execute()
when(response){
is Success -> doSomething()
else -> doSomethingElse()
}
}
If the case would have been longer, we would have had a more difficult code to read.
Let’s try to make this more Reactive
Let’s make a generic method for all Realm objects that are going to be selected as a list:
inline fun observeData(): Flow> = callbackFlow{
val result = frozenRealm.where<T>().findAllAsync()
result.addChangeListener{ mResult ->
offer(result.toList())
}
awaitClose {result.removeAllChangeListeners()}
}
It’s time to observe the magic of this new feature with combination to coroutines:
// inside the ViewModel
suspend fun sendDataToNetwork(){
observeData().flowOn(Dispatchers.IO)
.map {
return@map serialize(it)
}.flowOn(Dispatchers.Default) // same Realm object, different threads
.map {
return@map postRequest(it).execute()
}.flowOn(Dispatchers.IO)
.collect { networkResult ->
handleResult(networkResult)
}
}
And that’s it.
onDestroy
Without the Realm new feature, this would have been impossible. Instead, we would have wasted DTOs in order to hold reference to each selection that we could get. Allowing to share object across thread is a very convenient and necessary feature to have. Saving time and code is not the only factor that is being touched. This new feature opened a new way to reactive programming also.