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.
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
@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
val noteFlow by viewModel
.noteFlow.collectAsStateWithLifecycle()
UI Controls
@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
@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))
}
}
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)
}
Custom Remember List State
@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
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.
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.



Comments
Post a Comment