Nguyễn Đình Cường
0 comment
09 Nov, 2026
Ứng dụng ghi chú, danh sách công việc hay bộ nhớ đệm tin tức đều cần lưu dữ liệu trên máy. Room là thư viện chính thức giúp làm việc với SQLite trong Android một cách an toàn và gọn gàng, với kiểm tra câu truy vấn ngay lúc biên dịch.
Thêm các phụ thuộc Room và trình xử lý chú thích (KSP) vào file Gradle theo phiên bản được khuyến nghị trong tài liệu chính thức. Sau đó khai báo như sau.
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
val content: String,
val createdAt: Long = System.currentTimeMillis()
)
@Dao
interface NoteDao {
@Query("SELECT * FROM notes ORDER BY createdAt DESC")
fun observeAll(): Flow<List<Note>>
@Query("SELECT * FROM notes WHERE id = :id")
suspend fun getById(id: Long): Note?
@Insert
suspend fun insert(note: Note): Long
@Update
suspend fun update(note: Note)
@Delete
suspend fun delete(note: Note)
}
Hàm trả về Flow sẽ tự phát danh sách mới mỗi khi bảng thay đổi, rất hợp với giao diện phản ứng. Hàm suspend được Room chạy ngoài luồng chính.
@Database(entities = [Note::class], version = 1, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
companion object {
@Volatile private var instance: AppDatabase? = null
fun get(context: Context): AppDatabase =
instance ?: synchronized(this) {
instance ?: Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app.db"
).build().also { instance = it }
}
}
}
Chỉ nên có một thể hiện cơ sở dữ liệu cho toàn ứng dụng, thường được quản lý qua thư viện tiêm phụ thuộc hoặc mẫu singleton như trên.
class NotesViewModel(private val dao: NoteDao) : ViewModel() {
val notes: StateFlow<List<Note>> = dao.observeAll()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
fun add(title: String, content: String) {
viewModelScope.launch {
dao.insert(Note(title = title, content = content))
}
}
}
Khi thêm cột hoặc bảng mới ở phiên bản sau, bạn tăng version và cung cấp Migration để giữ dữ liệu của người dùng cũ:
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE notes ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0")
}
}
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.addMigrations(MIGRATION_1_2)
.build()
Tránh dùng tính năng xóa và tạo lại toàn bộ cơ sở dữ liệu trong bản phát hành thật, vì người dùng sẽ mất sạch dữ liệu.
Room giúp bạn quản lý dữ liệu cục bộ với ít mã lặp, ít lỗi và dễ kiểm thử. Kết hợp với Flow và ViewModel, giao diện luôn phản ánh đúng dữ liệu mà bạn không cần cập nhật thủ công.
Khi dữ liệu có quan hệ, chẳng hạn một người dùng có nhiều ghi chú, hãy dùng khóa ngoại và lớp quan hệ của Room để lấy dữ liệu liên quan trong một truy vấn thay vì gọi nhiều lần. Khai báo ràng buộc ForeignKey với hành vi khi xóa (ví dụ xóa dây chuyền) để cơ sở dữ liệu tự đảm bảo tính nhất quán. Với trường kiểu đặc biệt như ngày giờ hay danh sách, dùng TypeConverter để Room biết cách lưu và đọc.
Về kiểm thử, hãy viết bài kiểm thử cho DAO bằng cơ sở dữ liệu trong bộ nhớ: thêm vài bản ghi, truy vấn và kiểm tra kết quả. Kiểm thử cả migration bằng công cụ hỗ trợ của Room để chắc rằng người dùng cũ nâng cấp không mất dữ liệu. Ngoài ra, bật xuất lược đồ (exportSchema) và lưu các tệp JSON vào kho mã để theo dõi lịch sử thay đổi cấu trúc bảng giữa các phiên bản. Cách làm này giúp việc rà soát và phát hiện lỗi migration dễ dàng hơn rất nhiều.
Lỗi hay gặp nhất là quên tăng version khi đổi cấu trúc bảng, dẫn đến ứng dụng sập với thông báo không khớp lược đồ. Lỗi thứ hai là truy vấn trên luồng chính, bị Room chặn bằng ngoại lệ. Lỗi thứ ba là dùng entity làm mô hình giao diện cho mọi thứ; hãy tách mô hình hiển thị khi giao diện cần cấu trúc khác với bảng dữ liệu. Cuối cùng, đừng quên thêm index cho cột thường lọc, vì truy vấn chậm trên bảng lớn thường chỉ do thiếu chỉ mục.
Khi sẵn sàng làm ứng dụng Android hoàn chỉnh có lưu trữ cục bộ và đồng bộ, hãy khám phá thêm tại codebem.com.
Nguyễn Đình Cường
0 comment