In this tutorial, I teach you how to overcome one of the biggest pain points in Realm for Android, i.e. how to use Realm in a background thread.
What is Realm?
Realm is an embedded database with an SQL-like query syntax used by Android apps.
Its main feature vs other databases is that Realm objects in your mobile app code are live. When you modify a Realm object, all its copies across your app are instantly updated, and you can reactively observe any object’s changes via Realm change listeners, Flow, RxJava, etc.
The problem with Realm and background threads
Realm’s mechanism of live objects only works when your Realm instance is operating from a Looper thread, namely your Activity and Fragment classes (the UI thread). However, you might need to access Realm from non-UI background threads too, and those instances won’t be auto refreshed, creating memory leak and large file size problems.
How to use Realm in a WorkManager background thread
A typical use case for Realm in a background thread is within an Android WorkManager worker thread.
To solve this issue, I have used the following technique to obtain auto-refreshing Realm instances within WorkManager workers. Using a combination of Handler.post + suspendCoroutine + CoroutineWorker, I am able to achieve synchronous access to a Realm instance and ensure my Realm objects insider a WorkManager worker remain as live objects:
1. Create a companion HandlerThread and attach a Looper and a Handler to it. Let’s call this class RealmHandlerThread. Create a Realm instance on this Looper thread.
class RealmHandlerThread(name: String) : HandlerThread(name) {
@Volatile private var handler: Handler? = null
@Volatile private var realm: Realm? = null
fun startAndWaitUntilReady() {
start()
// HandlerThread's getLooper() blocks until it has a value
handler = Handler(looper)
handler?.post { realm = Realm.getDefaultInstance() }
}
private fun beforeQuit(onFinished: () -> Any): Boolean {
if (looper == null) {
return false
}
handler?.post {
realm?.close()
onFinished()
}
return true
}
override fun quit(): Boolean {
return beforeQuit { super.quit() }
}
override fun quitSafely(): Boolean {
return beforeQuit { super.quitSafely() }
}
}
Code language: Kotlin (kotlin)
2. Use your RealmHandlerThread‘s handler.post to perform all your Realm operations exclusively. Since your Realm instance lives on a Looper thread, all its objects become live and auto-refreshing objects.
handler.post is asynchronous, so to be able to use your Realm synchronously inside your WorkManager Worker code, bridge the async and sync worlds with a suspendCoroutine.
class RealmHandlerThread(name: String) : HandlerThread(name) {
...
suspend fun <T> executeWithRealm(realmFun: (Realm) -> T): T {
return suspendCoroutine { continuation ->
handler!!.post {
try {
continuation.resume(realmFun(realm!!))
} catch (err: Exception) {
continuation.resumeWithException(err)
}
}
}
}
...
}
Code language: Kotlin (kotlin)
3. Finally, hook your RealmHandlerThread to a CoroutineWorker.
abstract class RealmCoroutineWorker(
name: String,
context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
private val realmThread = RealmHandlerThread(name)
abstract fun doWork(realm: Realm): Result
final override suspend fun doWork(): Result {
return withContext(Dispatchers.IO) {
try {
realmThread.startAndWaitUntilReady()
realmThread.executeWithRealm { realm -> doWork(realm) }
} catch (err: Exception) {
Result.failure()
} finally {
realmThread.quit()
}
}
}
}
Code language: Kotlin (kotlin)
4. Extend RealmCoroutineWorker to get a WorkManager Worker that enjoys synchronous access to an auto-refreshing Realm instance.
Full source code
import android.content.Context
import android.os.Handler
import android.os.HandlerThread
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import io.realm.Realm
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
abstract class RealmCoroutineWorker(
name: String,
context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
private val realmThread = RealmHandlerThread(name)
abstract fun doWork(realm: Realm): Result
final override suspend fun doWork(): Result {
return withContext(Dispatchers.IO) {
try {
realmThread.startAndWaitUntilReady()
realmThread.executeWithRealm { realm -> doWork(realm) }
} catch (err: Exception) {
Result.failure()
} finally {
realmThread.quit()
}
}
}
}
class RealmHandlerThread(name: String) : HandlerThread(name) {
@Volatile private var handler: Handler? = null
@Volatile private var realm: Realm? = null
fun startAndWaitUntilReady() {
start()
// HandlerThread's getLooper() blocks until it has a value
handler = Handler(looper)
handler?.post { realm = Realm.getDefaultInstance() }
}
suspend fun <T> executeWithRealm(realmFun: (Realm) -> T): T {
return suspendCoroutine { continuation ->
handler!!.post {
try {
continuation.resume(realmFun(realm!!))
} catch (err: Exception) {
continuation.resumeWithException(err)
}
}
}
}
private fun beforeQuit(onFinished: () -> Any): Boolean {
if (looper == null) {
return false
}
handler?.post {
realm?.close()
onFinished()
}
return true
}
override fun quit(): Boolean {
return beforeQuit { super.quit() }
}
override fun quitSafely(): Boolean {
return beforeQuit { super.quitSafely() }
}
}
Code language: Kotlin (kotlin)