Room3 Alpha - Example (compatible with Room 2)

Introduction

Now Room3 is available in Alpha, it may be a good time to check it out and make sure it works with your current database. If you have never used a Room database before, this blog should give you a working start point to see it in action.

This is for people interested in testing Room3 and for people interested in Room in general. Beginners may want to give this a try just so they can see a database in action.

I will not go into too much detail about Room itself in this post, but you can see how to quickly get something up and running.

More advanced programmers may just want to check out the dependencies as a reference, but you may be interested in how I access my Database and pass it into the ViewModel.

Goal

As well as setting up a Room database that will work with both Room2 and Room3 (depending on which dependencies you use), we will also add a ViewModel and Basic UI.

Getting Started - The Boring Part

We can start by adding our dependencies for Room3. We will also add Room2 dependencies but will leave them commented out. You can switch over easily if you need to.

Note: Clicking on the listing will shrink it down to make things easier to skim.

Plugins: build.gradle.kts (Project: ProjectName)
plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.kotlin.compose) apply false

    // Added - Room2/3 (shared)
    alias(libs.plugins.ksp) apply false
}
Plugins: build.gradle.kts (Module :app)
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)

// Added - Room2/3 (shared)
alias(libs.plugins.ksp)
}
Deps: build.gradle.kts (Module :app)
dependencies {
...
// Added - ViewModel
implementation(libs.androidx.lifecycle.viewmodel.compose)

// Added - Room2 Database
// implementation(libs.androidx.room2.ktx)
// ksp(libs.androidx.room2.compiler)

// Added - Room3 Database
implementation(libs.androidx.room3.runtime)
ksp(libs.androidx.room3.compiler)
}

Toml

For the toml file, I feel it is better just to put everything onto a Github gist, but I'll leave the highlights here. I would recommend copying the data from the GitHub Gist - libs.versions.toml - rather than below.

Important: Copying and pasting the text below will fail due to the line breaks.

libs.versions.toml
[versions]
...

# Added - ViewModel
lifecycle = "2.10.0"

# Added - Room
#room2 = "2.8.4"
room3 = "3.0.0-alpha02"
ksp = "2.3.6"

[libraries]
...

# Added - ViewModel
androidx-lifecycle-viewmodel-compose = { 
    group = "androidx.lifecycle", 
    name = "lifecycle-viewmodel-compose", 
    version.ref = "lifecycle" 
}

# Added - Room2
#androidx-room2-ktx = { 
#    group = "androidx.room", 
#    name = "room-ktx", 
#    version.ref = "room2" 
#}
#androidx-room2-compiler = { 
#    module = "androidx.room:room-compiler", 
#    version.ref = "room2" 
#}

# Added - Room3
androidx-room3-runtime = { 
    group = "androidx.room3", 
    name = "room3-runtime",  
    version.ref = "room3" 
}
androidx-room3-compiler = { 
    group = "androidx.room3", 
    name = "room3-compiler", 
    version.ref = "room3" 
}

[plugins]
...

# Added - Room2/3 (shared)
ksp = { 
    id = "com.google.devtools.ksp", 
    version.ref = "ksp" 
}

After setting up all your dependencies, sync and build your project so you know everything is working correctly.

The Database - The Interesting Part!

Now we have that out of the way, we can start on the actual code.

Data Structure

First off, we need our data structure for our database.

@Entity(tableName = "room_note")
data class RoomTextNote(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val note : String = "Empty Note",
val username : String = "Default User",
)

Note we need to mark this as an @Entity and give it a tableName. Unfortunately, this name is plain text so we need to be careful to keep every reference to it in sync.

The key is automatically generated by the database so we can ignore this when creating items.

Data Access Object

Next, we need our DAO - our Data Access Object.

@Dao
interface RoomTextNoteDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(note: RoomTextNote)

@Query("SELECT * FROM room_note" +
" ORDER BY id DESC") // Or 'value', or 'timestamp'
fun getAllAsFlowReverseSortedById(): Flow<List<RoomTextNote>>

@Query("DELETE FROM room_note " +
"WHERE id = :id")
suspend fun deleteById(id: Int)

@Query("DELETE FROM room_note")
suspend fun deleteAll()

@Query("DELETE FROM sqlite_sequence " +
"WHERE name = 'room_note'")
suspend fun resetSequence()

@Transaction
suspend fun clearAndReset() {
deleteAll()
resetSequence() // resets id counter to 0
}
}

I've cut some of the functions here (available in full code listing), but this should give you a good idea of what we are doing.

We have insert, delete (by id), and clear. We also have a way to stream all the data to our viewModel then to our UI via the Flow.

Database Singleton

We access our database via a Singleton. The database needs to be built before it can be accessed. There are a number of ways to do this, but we'll automate this when we access our viewModel. That way, we don't need to think about it.

For heavier databases, you will likely want to build things in the background before you need to access it.

@Database(entities = [RoomTextNote::class], version = 1)
abstract class RoomTextNoteDatabaseSingleton : RoomDatabase() {
abstract fun roomTextNoteDao(): RoomTextNoteDao

companion object {
@Volatile
private var INSTANCE: RoomTextNoteDatabaseSingleton? = null

fun getInstance(context: Context):
RoomTextNoteDatabaseSingleton {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDatabase(context).also {
INSTANCE = it
}
}
}

private fun buildDatabase(context: Context):
RoomTextNoteDatabaseSingleton {
return Room.databaseBuilder(
context.applicationContext,
RoomTextNoteDatabaseSingleton::class.java,
"room_note_db",
).fallbackToDestructiveMigration(
true
).build()
}
}
}

Our ViewModel

We'll keep this quick. Basically, we're connecting our ViewModel to our Database via the the DAO. I'll skip some of the code here (either cut completely or abbreviated with ...) so check the GitHub Gist for full details.

class NoteViewModel(
private val dao: RoomTextNoteDao
): ViewModel() {
val noteFlow: StateFlow<
List<RoomNoteDatabase.RoomTextNote>> = dao
.getAllAsFlowReverseSortedById()
.stateIn(
scope = viewModelScope,
started = SharingStarted
.WhileSubscribed(5000),
initialValue = emptyList()
)

private val usernameList = listOf(...)

private val noteContentList = listOf(...)

fun addScoreExample(
note: String = noteContentList.random(),
username: String = usernameList.random()
) {
viewModelScope.launch {
dao.insert(
RoomNoteDatabase.RoomTextNote(
note = note,
username = username
)
)
}
}

fun deleteById(id: Int) {
viewModelScope.launch {
dao.deleteById(id)
}
}

private suspend fun clearDatabaseMaintainIds() = dao
.deleteAll()
private suspend fun clearAndResetDatabase() = dao
.clearAndReset()

fun clearDatabase() { ... }

companion object {
@VisibleForTesting
internal fun factory() { ... }

@Composable
fun get(context: Context): NoteViewModel {
return viewModel(
factory = factory(
RoomTextNoteDatabaseSingleton
.getInstance(context)
)
)
}

@Composable
fun get(): NoteViewModel {
val context = LocalContext.current
return get(context)
}
}
}

For learning about Room, focus on the noteFlow, addScoreExample, deleteById, and clear functions. Each time, we are using a Coroutine (or Flow) to safely use the database without blocking the UI thread.

The noteFlow is how we share the data with the UI. Using Compose, it is super simple to use.

For more advanced programmers, you may notice I am using a unique get() in a Composable to access the ViewModel. I wrote a blog post previously about this subject. We're basically making it very convenient to access our ViewModel and automatically build our database. We'll cover the access in the UI section next.

Full code listing.

The UI

Finally, the UI. Let's look at our empty database display first:

I guess that looks okay, but let's see how we make it and get it working with our data.

ViewModel Access

First, we need to access our ViewModel:
@Composable
private fun SimpleWriteAndReadTest() {
val viewModel = NoteViewModel.get()

That's it... We're using our custom .get extension so we don't need to think about anything else. This also handles connecting our ViewModel to our Database. One less worry.

Collecting the Flow

We can now connect to our flow via the viewModel:
val noteFlow by viewModel
.noteFlow.collectAsStateWithLifecycle()

UI Controls

We control add / clear with the following:
@Composable
private fun AddClearControls(
onAdd: () -> Unit,
onClear: () -> Unit,
) {
Spacer(Modifier.height(8.dp))
Row(
Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround
) {
Button(onAdd) {
Text("Add")
}

Button(onClear) {
Text("Clear")
}
}
}

Which is accessed via our viewModel:

AddClearControls(
onAdd = viewModel::addScoreExample,
onClear = viewModel::clearDatabase
)

Note Display

And, finally, we have our note display sections.
@Composable
private fun NoteDisplay(note: RoomNoteDatabase.RoomTextNote) {
Text(
modifier = Modifier.padding(start = 16.dp, top = 8.dp),
text = note.id.toString(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.inversePrimary,
)
Column(
Modifier
.sizeIn(minWidth = 256.dp)
.padding(4.dp),
horizontalAlignment =
Alignment.CenterHorizontally
) {
Text(note.note)
Text(" - ${note.username}")
Spacer(Modifier.height(16.dp))
}
}
The display itself is fairly simple. Each item is displayed on a card (see below) and in a column. We have an inner column which centers most of the data aside from the ID number (which we keep at the left).

The card:

OutlinedCard(
modifier = Modifier.clickable() {
onDelete(it.id)
},
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
border = BorderStroke(
1.dp,
Color.White.copy(alpha = 0.5f)
)
) {
NoteDisplay(it)
}
All of this is wrapped in a LazyColumn.

Custom Remember List State

For more advanced programmers, you may be interested in my LazyListState. I am using a custom remember function to track new entries and jump to the top of the list to give a better experience for the user.
@Composable
private fun rememberTopJumpListState(
notes: List<RoomNoteDatabase.RoomTextNote>
): LazyListState {
val listState = rememberLazyListState()
var previousCount by remember {
mutableIntStateOf(notes.size)
}

LaunchedEffect(notes.size) {
if (notes.size > previousCount) {
listState.animateScrollToItem(0)
}
previousCount = notes.size
}

return listState
}

Populated Version

Here is how it looks after you add some data.

I quite like the color scheme, card design, and emoji use in this simple example. I would prefer the controls were at the bottom (easier to use on a phone) but this is fine for now.

Full code listing.

Summary

We've looked at creating a basic Room Database, ViewModel, and UI and the latest Room3 alpha dependencies.

I hope you have picked up something interesting along the way. Be sure to check the links and full code listings.

GitHub Gists

Related Blog Posts

Official Links


Comments

Popular Posts