Introducing enableMultiInstanceInvalidation()

Room now has an enableMultiInstanceInvalidation() function that you can call on RoomDatabase.Builder when you are setting up the database. This tells Room that you want to use it across processes. Room will then set up a MultiInstanceInvalidationService in your primary (default) process. RoomDatabase objects in other processes will connect to that service, and Room will use IPC to allow database modification information to flow between the processes. The net effect is that each InvalidationTracker finds out about modifications happening in any of the app’s processes.

The CrossProcess module of the book’s primary sample project has a database that uses enableMultiInstanceInvalidation():

package com.commonsware.room.process

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters

private const val DB_NAME = "random.db"

@Database(entities = [RandomEntity::class], version = 1)
@TypeConverters(TypeTransmogrifier::class)
abstract class RandomDatabase : RoomDatabase() {
  abstract fun randomStore(): RandomStore

  companion object {
    fun newInstance(context: Context) =
      Room.databaseBuilder(context, RandomDatabase::class.java, DB_NAME)
        .enableMultiInstanceInvalidation()
        .build()
  }
}

For that to be useful, though, we need more than one process, and we need for each process to be working with the same underlying SQLite database via the same Room-generated classes.

In One Process, an Activity

In the main application process, MainActivity has a really big “Populate Sample Data” button that, when clicked, calls a populate() function on MainViewModel. That in turn calls populate() on the RandomRepository. That generates a random number of RandomEntity instances and inserts them:

package com.commonsware.room.process

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
import kotlin.random.Random

class RandomRepository(
  private val db: RandomDatabase,
  private val appScope: CoroutineScope
) {
  private val dispatcher =
    Executors.newSingleThreadExecutor().asCoroutineDispatcher()

  fun summarize() = db.randomStore().summarize()

  suspend fun populate() {
    withContext(dispatcher + appScope.coroutineContext) {
      val count = Random.nextInt(100) + 1

      db.randomStore().insert((1..count).map { RandomEntity(0) })
    }
  }
}

RandomRepository also has a summarize() function that exposes a corresponding function on our DAO:

package com.commonsware.room.process

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
import java.time.Instant

data class Summary(
  val count: Int,
  val oldestTimestamp: Instant? = null
)

@Dao
interface RandomStore {
  @Insert
  suspend fun insert(entities: List<RandomEntity>)

  @Query("SELECT COUNT(*) as count, MIN(timestamp) as oldestTimestamp FROM randomStuff")
  fun summarize(): Flow<Summary>
}

summarize() gets the count of entities and the oldest timestamp and emits them via a Flow. That Flow will emit new results as the database is modified and so long as something is observing the Flow. MainActivity gets that data via MainViewModel and shows the count and date on the screen.

In Another Process, a Service

Our manifest has a <service> entry for SomeService, placing it into another process via android:process:

    <service android:name=".SomeService" android:process=":something" />

SomeService also uses summarize() on RandomRepository, dumping whatever it receives to Logcat:

package com.commonsware.room.process

import android.content.Intent
import android.os.Process
import android.util.Log
import androidx.lifecycle.LifecycleService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.core.qualifier.named

class SomeService : LifecycleService() {
  private val repo: RandomRepository by inject()
  private val appScope: CoroutineScope by inject(named("appScope"))

  override fun onCreate() {
    super.onCreate()

    appScope.launch {
      repo.summarize().collect {
        Log.d("SomeService", "PID: ${Process.myPid()} summary: $it")
      }
    }
  }
}

MainActivity starts that service when it is in the foreground via onStart() and stops that service when the UI returns to the background in onStop(). This is not a wise use of a service; the point behind a service is to run when the UI is not in the foreground. But, it helps illustrate the effects of enableMultiInstanceInvalidation().

Results, Before and After

If we lacked enableMultiInstanceInvalidation() — such as if you comment out that line in RandomDatabase and run the app — you will find that SomeService logs the initial state of the database, but that is it. You can push the big button as much as you want, and the activity will display the current count of entities, but the service will not log new data. That is because our separate process does not know that the database changed, so Room does not emit a new result on that process’ summarize() Flow.

But, if we use enableMultiInstanceInvalidation(), now clicking the button causes both the activity and the service to get details of the updated database, so we see both the UI update and the SomeService Logcat entry.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.