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.