跪拜 Guibai
← Back to the summary

Room in Practice: Compile-Time SQL Checks and a Full CRUD Notes Database for Android

Getting Started with Room — Entities / DAO / Database Setup and CRUD

Hello Android developers, welcome to Part 5 of the "Jetpack Core in Action" series. In the first four parts, we completed the setup of ViewModel, LiveData, coroutines, and a Repository-based layered architecture, and the project already has a clear skeleton. The one shortcoming is that all data is stored in memory; when the app exits, the data is gone. This part officially introduces the Room database, Google's recommended SQLite abstraction layer. It turns tedious database operations into a clean annotation-driven API, provides compile-time SQL validation, and makes database development safe and efficient. We will build a notes database from scratch and complete full CRUD functionality. All code follows Kotlin conventions and Version Catalog dependency management, and can be compiled and run directly.

1. What is Room? Why give up native SQLite?

Native SQLite is Android's built-in database capability, but the development experience is poor. Room retains all the capabilities of SQLite while making many engineering optimizations and fitting perfectly into the Jetpack architecture.

1.1 Core pain points of native SQLite

Pain point Explanation
Lots of boilerplate Requires manually managing database helper classes, cursor parsing, and SQL concatenation; a simple query takes dozens of lines
No compile-time validation SQL syntax errors and wrong table or field names only crash at runtime, with very high troubleshooting cost
No thread-safety guarantee Main-thread access, multithreaded concurrency, and transaction management must be handled manually; any slip can cause ANR or data corruption
No reactive support The UI must be manually refreshed after data changes; cannot work seamlessly with LiveData/Flow

1.2 Room's core advantages

Room is an annotation-based ORM (object-relational mapping) framework. Its core advantages directly solve the pain points of native SQLite:

Advantage Explanation
Annotation-driven development, minimal code Annotations define tables and interfaces define operations; no need to hand-write SQL helper classes or cursor parsing
Compile-time SQL validation All SQL statements are checked for syntax, table names, and field names during compilation, catching problems early and preventing runtime crashes
Native coroutine/LiveData support DAO methods directly support suspend functions and can return LiveData/Flow, integrating seamlessly with the Jetpack architecture
Thread-safety encapsulation Automatically handles thread switching for database operations, avoids main-thread access exceptions, and simplifies transaction management

1.3 Room's three core components

Room's architecture consists of three core parts, each with its own responsibility, jointly completing the mapping from objects to database tables:

Component Annotation Core responsibility
Entity class @Entity Corresponds to a table in the database; the class's properties correspond to table fields
Data access object DAO @Dao Defines the database operation interface (CRUD); the only entry point for the business layer to operate the database
Database class @Database The holder of the database; manages the database version and all DAO instances

The dependency relationship among the three: Database contains DAO, DAO operates on Entity, and the business layer obtains the DAO through Database, then uses the DAO to operate entity data. This fully matches the design philosophy of a layered architecture.

2. Prerequisite dependency configuration

2.1 Version Catalog configuration

Check whether gradle/libs.versions.toml contains the following configuration:

[versions]
room = "2.6.1"

[libraries]
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }

[plugins]
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }

2.2 Module-level build.gradle.kts configuration

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.kapt)   // enable kapt for annotation processing
}

android {
    // ... other configuration

    kapt {
        correctErrorTypes = true      // correct error types to improve Room stability
    }
}

dependencies {
    // Room
    implementation(libs.androidx.room.runtime)
    implementation(libs.androidx.room.ktx)     // coroutine + Flow support
    kapt(libs.androidx.room.compiler)          // annotation processor
}

Note: kapt is Kotlin's annotation processing tool. Room uses it to generate code at compile time. Without it, the code for @Dao and @Database will not be generated, and you will get an error at runtime saying that the implementation class cannot be found.

3. @Entity: entity definition and common configuration

An entity class is the mapping of a database table; one data class corresponds to one table. Through annotations, you can flexibly configure table names, primary keys, fields, ignored items, and more.

3.1 Basic entity definition

The most basic entity class only needs the @Entity annotation, with @PrimaryKey defining the primary key:

package com.example.jetpacknote.data.entity

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "notes")    // tableName sets a custom table name; the class name is used by default if omitted
 data class Note(
    @PrimaryKey(autoGenerate = true)  // auto-increment primary key
    val id: Long = 0,

    val content: String,

    val createTime: Long = System.currentTimeMillis(),

    val updateTime: Long = System.currentTimeMillis()
)

3.2 Common field configuration

(1) Custom field name: @ColumnInfo

If a table field name must differ from the class property name, use @ColumnInfo to map it. You can also configure a default value:

@ColumnInfo(name = "note_content", defaultValue = "")
val content: String

@ColumnInfo(name = "create_time")
val createTime: Long

(2) Ignoring fields: @Ignore

If a property is used only for business logic and does not need to be stored in the database, add the @Ignore annotation:

@Ignore
val isSelected: Boolean = false

(3) Nested objects: @Embedded

If several fields in an entity can be grouped into one object, use @Embedded to nest the entity. The table still has independent fields, but the code structure is clearer.

data class TimeInfo(
    val createTime: Long,
    val updateTime: Long
)

@Entity(tableName = "notes")
data class Note(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,

    val content: String,

    @Embedded
    val timeInfo: TimeInfo          // the table will have two fields: createTime and updateTime
)

Note: Field names in a nested object cannot duplicate the outer entity's fields. You can use the prefix property on @Embedded to add a prefix to nested fields:

@Embedded(prefix = "time_")         // table fields become time_createTime and time_updateTime
val timeInfo: TimeInfo

3.3 Summary of entity class definition conventions

Convention Explanation Example
Use data class Automatically generates equals/hashCode/toString data class Note(...)
Primary key must have a default value Especially when combined with autoGenerate = true val id: Long = 0
Use val to keep fields immutable Data classes are recommended to be immutable val content: String
Add @Ignore to non-stored fields Prevents Room from trying to map them @Ignore val temp: String?
Use @ColumnInfo for explicit naming Avoids field-name confusion during refactoring @ColumnInfo(name = "create_time")

4. @Dao: the four core operations

DAO is short for Data Access Object, the interface layer for database operations. The business layer only interacts with the DAO and does not need to care about the underlying SQL implementation. Room generates the DAO interface implementation class at compile time.

4.1 Basic DAO definition

@Dao
interface NoteDao {
    // database operation methods are defined here
}

4.2 @Insert: inserting data

Mark an insert method directly, and Room automatically generates the insert SQL. Single objects and list objects are both supported for batch insertion:

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertNote(note: Note): Long          // returns the auto-increment ID after insertion

@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertNotes(notes: List<Note>)        // batch insert

Conflict strategies (OnConflictStrategy):

Strategy Behavior
REPLACE Replaces old data when the primary key is duplicated
IGNORE Ignores the new data when the primary key is duplicated
ABORT Aborts the operation and throws an exception when the primary key is duplicated (default)

4.3 @Update: updating data

Updates the corresponding data row by primary key:

@Update
suspend fun updateNote(note: Note): Int          // returns the number of affected rows

4.4 @Delete: deleting data

Deletes the corresponding data row by primary key:

@Delete
suspend fun deleteNote(note: Note): Int          // returns the number of affected rows

4.5 @Query: custom queries

@Query is the most flexible operation annotation. It supports custom SQL statements and all SQLite syntax. SQL correctness is validated at compile time:

@Dao
interface NoteDao {

    // Query all notes (ordered by time descending)
    @Query("SELECT * FROM notes ORDER BY createTime DESC")
    suspend fun getAllNotes(): List<Note>

    // Query a single note by ID
    @Query("SELECT * FROM notes WHERE id = :noteId")
    suspend fun getNoteById(noteId: Long): Note?

    // Fuzzy search for notes
    @Query("SELECT * FROM notes WHERE content LIKE '%' || :keyword || '%'")
    suspend fun searchNotes(keyword: String): List<Note>

    // Delete all notes
    @Query("DELETE FROM notes")
    suspend fun clearAllNotes()
}

Important convention: Methods in a DAO should all be declared as suspend functions. Room automatically executes database operations on a background thread, with no manual thread switching, perfectly matching Kotlin coroutines.

5. @TypeConverter: handling complex types

SQLite only supports basic data types such as integers, strings, and floats. If an entity contains complex types such as Date, List, or custom objects, you need @TypeConverter for type conversion.

5.1 Date to Long converter

package com.example.jetpacknote.data.converter

import androidx.room.TypeConverter
import java.util.Date

class DateConverter {

    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? = date?.time

    @TypeConverter
    fun timestampToDate(timestamp: Long?): Date? = timestamp?.let { Date(it) }
}

5.2 List to JSON converter (using Gson)

If you need to store collection types such as List, you can use Gson to serialize them into a JSON string:

package com.example.jetpacknote.data.converter

import androidx.room.TypeConverter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken

class StringListConverter {

    private val gson = Gson()

    @TypeConverter
    fun listToString(list: List<String>?): String? {
        return list?.let { gson.toJson(it) }
    }

    @TypeConverter
    fun stringToList(str: String?): List<String>? {
        return str?.let {
            val type = object : TypeToken<List<String>>() {}.type
            gson.fromJson(it, type)
        }
    }
}

5.3 Registering type converters

After writing the converters, register them on the Database class with the @TypeConverters annotation:

@Database(entities = [Note::class], version = 1)
@TypeConverters(DateConverter::class, StringListConverter::class)
abstract class AppDatabase : RoomDatabase() {
    // ...
}

6. @Database: defining the database class

The @Database annotation defines the database class. It is Room's entry point and uses the singleton pattern to ensure there is only one database instance globally.

package com.example.jetpacknote.data.database

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.example.jetpacknote.data.converter.DateConverter
import com.example.jetpacknote.data.dao.NoteDao
import com.example.jetpacknote.data.entity.Note

@Database(
    entities = [Note::class],
    version = 1,
    exportSchema = false
)
@TypeConverters(DateConverter::class)
abstract class AppDatabase : RoomDatabase() {

    abstract fun noteDao(): NoteDao

    companion object {
        private const val DATABASE_NAME = "jetpack_note_db"

        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                INSTANCE ?: Room.databaseBuilder(
                    context.applicationContext,           // must use Application Context
                    AppDatabase::class.java,
                    DATABASE_NAME
                ).build().also { INSTANCE = it }
            }
        }
    }
}

Key points:

7. Hands-on: complete notes CRUD

Now combine all the concepts into the notes project and walk through the full flow from creating the database to CRUD.

Step 1: Create the Note entity class

package com.example.jetpacknote.data.entity

import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.Date

@Entity(tableName = "notes")
data class Note(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,

    val content: String,

    val createTime: Date = Date(),

    val updateTime: Date = Date()
)

Step 2: Create NoteDao

package com.example.jetpacknote.data.dao

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.jetpacknote.data.entity.Note

@Dao
interface NoteDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertNote(note: Note): Long

    @Update
    suspend fun updateNote(note: Note): Int

    @Delete
    suspend fun deleteNote(note: Note): Int

    @Query("SELECT * FROM notes ORDER BY createTime DESC")
    suspend fun getAllNotes(): List<Note>

    @Query("SELECT * FROM notes WHERE id = :noteId")
    suspend fun getNoteById(noteId: Long): Note?

    @Query("DELETE FROM notes")
    suspend fun clearAllNotes()
}

Step 3: Create AppDatabase (with converter)

package com.example.jetpacknote.data.database

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.example.jetpacknote.data.converter.DateConverter
import com.example.jetpacknote.data.dao.NoteDao
import com.example.jetpacknote.data.entity.Note

@Database(
    entities = [Note::class],
    version = 1,
    exportSchema = false
)
@TypeConverters(DateConverter::class)
abstract class AppDatabase : RoomDatabase() {

    abstract fun noteDao(): NoteDao

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

        fun getInstance(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                INSTANCE ?: Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "jetpack_note_db"
                ).build().also { INSTANCE = it }
            }
        }
    }
}

Step 4: Create DateConverter

package com.example.jetpacknote.data.converter

import androidx.room.TypeConverter
import java.util.Date

class DateConverter {

    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? = date?.time

    @TypeConverter
    fun timestampToDate(timestamp: Long?): Date? = timestamp?.let { Date(it) }
}

Step 5: Refactor the Repository (from memory to database)

This is where the advantage of the layered architecture shows: the Repository switches internally from mock data to the Room database, its external method signatures remain completely unchanged, and the ViewModel and UI layers do not need any modifications.

package com.example.jetpacknote.data.repository

import android.content.Context
import com.example.jetpacknote.data.database.AppDatabase
import com.example.jetpacknote.data.entity.Note
import java.util.Date

class NoteRepository private constructor(context: Context) {

    private val noteDao = AppDatabase.getInstance(context).noteDao()

    suspend fun getAllNotes(): Result<List<Note>> {
        return try {
            Result.success(noteDao.getAllNotes())
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    suspend fun getNoteById(id: Long): Result<Note?> {
        return try {
            Result.success(noteDao.getNoteById(id))
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    suspend fun addNote(content: String): Result<Boolean> {
        return try {
            val note = Note(content = content)
            noteDao.insertNote(note)
            Result.success(true)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    suspend fun updateNote(note: Note): Result<Boolean> {
        return try {
            val updated = note.copy(updateTime = Date())
            noteDao.updateNote(updated)
            Result.success(true)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    suspend fun deleteNote(note: Note): Result<Boolean> {
        return try {
            noteDao.deleteNote(note)
            Result.success(true)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    suspend fun clearAllNotes(): Result<Unit> {
        return try {
            noteDao.clearAllNotes()
            Result.success(Unit)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

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

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

Step 6: Update the ViewModel

package com.example.jetpacknote.ui

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.example.jetpacknote.data.repository.NoteRepository
import kotlinx.coroutines.launch

sealed class NoteUiState {
    data object Loading : NoteUiState()
    data class Success(val notes: List<String>) : NoteUiState()
    data class Error(val message: String) : NoteUiState()
}

class NoteViewModel(application: Application) : AndroidViewModel(application) {

    private val repository = NoteRepository.getInstance(application)

    private val _uiState = MutableLiveData<NoteUiState>()
    val uiState: LiveData<NoteUiState> = _uiState

    fun loadNotes() {
        viewModelScope.launch {
            _uiState.value = NoteUiState.Loading
            val result = repository.getAllNotes()
            result.onSuccess { notes ->
                _uiState.value = NoteUiState.Success(notes.map { it.content })
            }.onFailure { error ->
                _uiState.value = NoteUiState.Error(error.message ?: "Failed to load")
            }
        }
    }

    fun addNote(content: String) {
        viewModelScope.launch {
            repository.addNote(content)
            loadNotes()   // refresh the list automatically after adding
        }
    }

    fun deleteNote(content: String) {
        viewModelScope.launch {
            // simple demo: delete the first matching item by content
            val result = repository.getAllNotes()
            result.onSuccess { notes ->
                notes.firstOrNull { it.content == content }?.let {
                    repository.deleteNote(it)
                    loadNotes()
                }
            }
        }
    }
}

Why extend AndroidViewModel? Because the Repository needs Context, and AndroidViewModel holds Application Context, which is safe and does not leak memory.

Step 7: Update the Activity layout

Add an input field and an add button in activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center_horizontal"
    android:padding="32dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <EditText
            android:id="@+id/etInput"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:hint="Enter note content"
            android:inputType="textMultiLine" />

        <Button
            android:id="@+id/btnAdd"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Add" />
    </LinearLayout>

    <Button
        android:id="@+id/btnRefresh"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="Refresh list" />

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:visibility="gone" />

    <TextView
        android:id="@+id/tvContent"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:gravity="center"
        android:textSize="16sp"
        android:visibility="gone" />

    <TextView
        android:id="@+id/tvError"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:gravity="center"
        android:textColor="#D32F2F"
        android:textSize="16sp"
        android:visibility="gone" />

</LinearLayout>

Step 8: Update the Activity

package com.example.jetpacknote

import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.example.jetpacknote.databinding.ActivityMainBinding
import com.example.jetpacknote.ui.NoteUiState
import com.example.jetpacknote.ui.NoteViewModel

class MainActivity : AppCompatActivity() {

    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
    private val viewModel: NoteViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        initClickListeners()
        observeUiState()
        viewModel.loadNotes()
    }

    private fun initClickListeners() {
        binding.btnAdd.setOnClickListener {
            val content = binding.etInput.text.toString().trim()
            if (content.isEmpty()) {
                Toast.makeText(this, "Please enter note content", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }
            viewModel.addNote(content)
            binding.etInput.text?.clear()
        }

        binding.btnRefresh.setOnClickListener {
            viewModel.loadNotes()
        }
    }

    private fun observeUiState() {
        viewModel.uiState.observe(this) { state ->
            when (state) {
                is NoteUiState.Loading -> {
                    binding.progressBar.visibility = View.VISIBLE
                    binding.tvContent.visibility = View.GONE
                    binding.tvError.visibility = View.GONE
                }
                is NoteUiState.Success -> {
                    binding.progressBar.visibility = View.GONE
                    binding.tvError.visibility = View.GONE
                    binding.tvContent.visibility = View.VISIBLE
                    binding.tvContent.text = state.notes.joinToString("\n\n")
                }
                is NoteUiState.Error -> {
                    binding.progressBar.visibility = View.GONE
                    binding.tvContent.visibility = View.GONE
                    binding.tvError.visibility = View.VISIBLE
                    binding.tvError.text = state.message
                }
            }
        }
    }
}

Run verification

Operation Expected result
Enter content and click Add The list refreshes automatically and shows the new note ✅
Kill the app and reopen it The data still exists; persistence works ✅
Rotate the screen Data is not lost; the page state restores automatically ✅
Click Refresh Reloads the database data ✅

8. Advantages of Room's compile-time SQL checking

This is one of Room's biggest advantages over native SQLite and deserves separate emphasis:

Comparison dimension Native SQLite (string SQL) Room (@Query annotation)
SQL syntax errors Runtime crash ❌ Compile-time error ✅
Table name/field name typos Runtime crash ❌ Compile-time error ✅
Return type mismatch Runtime crash ❌ Compile-time error ✅
Field refactoring Manually check all SQL Automatically detected ✅
IDE support No syntax highlighting SQL syntax highlighting ✅

A real scenario: suppose you want to rename createTime to createdAt. With native SQLite, you have to manually search all SQL strings and replace them; it is easy to miss one and cause a runtime crash. With Room, once you change the entity field name, every @Query that uses that field fails at compile time, and none will be missed.

9. FAQ / Pitfall guide

Q1: Why must the Room database be a singleton?

A: There are three core reasons:

Reason Explanation
Resource overhead Database connections are heavyweight resources; frequent creation and destruction consume significant memory and performance
Data consistency Multiple instances operating concurrently can cause transaction conflicts, inconsistent reads/writes, and even database lockups
Transaction safety A singleton ensures unified management of global transactions and avoids transaction isolation failures under multiple instances

Room's official documentation also explicitly recommends creating only one database instance per app.

Q2: What are the conventions for defining an Entity class?

A: Following these conventions avoids most Room development problems:

Convention Explanation
Prefer data class Automatically generates equals, hashCode, and toString
Primary key must be explicit A Long auto-increment primary key is recommended
Field nullability must be explicit Non-null fields use non-null types; Room validates at compile time
Add @Ignore to non-stored fields Otherwise Room tries to map them as table fields and the build fails
Use @ColumnInfo for explicit naming Avoids compatibility problems caused by default class/property names

Q3: What is the advantage of Room's compile-time SQL checking?

A: This is Room's biggest engineering advantage over native SQLite:

Q4: Why does the ViewModel extend AndroidViewModel? What should be noted?

A: Because the Repository needs a Context instance, and AndroidViewModel holds Application Context, whose lifecycle matches the app and does not cause memory leaks.

Note: AndroidViewModel can only access Application Context, not Activity Context. If you need Activity-level context, such as showing a Dialog, delegate the operation to the Activity through LiveData or callbacks.

10. Migration summary: from memory to database

Stage Data storage method Characteristics
Parts 2-3 Hardcoded data in ViewModel For demonstration; no persistence
Part 4 In-memory data in Repository Complete architecture, but lost on restart
This part Room database Persistent storage; survives restart ✅

At this point, our notes data has truly been "persisted". The app closes and reopens, and the data still exists. At the same time, the Repository layered architecture keeps the data source switch completely transparent to upper layers: switching from mock data to the Room database required zero changes in the ViewModel and UI layers.

Next episode preview

This part built the Room database from scratch, mastered the three core components of entities, DAO, and Database, and implemented full CRUD persistence for the notes app, connecting seamlessly with the Repository layered architecture.

But the current queries are still one-shot. After data changes, you must manually call a refresh method; reactive automatic updates are not yet implemented. In the next part, we will move to advanced Room topics:


🌟 Like + bookmark + follow to receive updates as soon as they are published 💬 Comment section: what database pitfalls have you run into while using Room? Leave a message and chat!

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

亚雷

Deleting a note by its primary key is more reliable.