DEV Community

Kai's brain dump
Kai's brain dump

Posted on • Updated on • Originally published at braindumpk.substack.com

How to auto-refresh Realm inside Android WorkManager

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() }
  }
}
Enter fullscreen mode Exit fullscreen mode

Code explanation

  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() }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. 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)
        }
      }
    }
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode
  1. 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()
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Extend RealmCoroutineWorker to get a WorkManager Worker that enjoys synchronous access to an auto-refreshing Realm instance.

Top comments (0)