The problem with Realm and background threads
Realm’s live objects only work when your Realm instance is operating from a Looper
thread, namely your Activity
and Fragment
classes (the UI thread). However, typical use cases of Realm may require access from 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
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:
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 explanation
- Create a companion
HandlerThread
and attach aLooper
and aHandler
to it. Let’s call this classRealmHandlerThread
. Create a Realm instance on thisLooper
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() }
}
}
- Use your
RealmHandlerThread
‘sHandler::post
to perform all your Realm operations exclusively. Since your Realm instance lives on aLooper
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)
}
}
}
}
...
}
- Finally, hook your
RealmHandlerThread
to aCoroutineWorker
.
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()
}
}
}
}
- Extend
RealmCoroutineWorker
to get a WorkManager Worker that enjoys synchronous access to an auto-refreshing Realm instance.
Top comments (0)