Step 12 of 15
Room database (Entity, DAO, Database), DataStore for preferences
Local storage — Room สำหรับ database, DataStore สำหรับ preferences
| Tool | Use for |
|---|---|
| Room | Structured data you query — lists of objects, relations, search |
| DataStore | Simple key-value pairs — settings, preferences, a single token |
Current versions: Room 2.7.0 (with KSP), DataStore 1.1.4.
// build.gradle.kts
plugins {
alias(libs.plugins.google.devtools.ksp)
}
dependencies {
implementation("androidx.room:room-runtime:2.7.0")
implementation("androidx.room:room-ktx:2.7.0")
ksp("androidx.room:room-compiler:2.7.0")
}
Room uses KSP to generate the implementation at compile time. The KSP version must match the Kotlin version (for example 2.4.20-1.0.31).
An entity is a table. Each @Entity class maps to one table.
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "tasks")
data class TaskEntity(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String,
val done: Boolean = false
)
A DAO (Data Access Object) defines the queries. Use suspend for write operations and Flow for observable reads.
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface TaskDao {
@Query("SELECT * FROM tasks ORDER BY id DESC")
fun observeAll(): Flow<List<TaskEntity>>
@Insert
suspend fun insert(task: TaskEntity): Long
@Update
suspend fun update(task: TaskEntity)
@Delete
suspend fun delete(task: TaskEntity)
@Query("DELETE FROM tasks WHERE id = :id")
suspend fun deleteById(id: Int)
}
The database holds the entities and DAOs. Make it a singleton.
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [TaskEntity::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
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,
"app.db"
).build().also { INSTANCE = it }
}
}
}
}
class RoomTaskRepository(private val dao: TaskDao) : TaskRepository {
override fun observeAll(): Flow<List<Task>> =
dao.observeAll().map { entities ->
entities.map { it.toDomain() }
}
override suspend fun add(title: String) {
dao.insert(TaskEntity(title = title))
}
private fun TaskEntity.toDomain() =
Task(id = id, title = title, done = done)
}
Because observeAll() returns a Flow, the UI updates automatically when the database changes.
For settings, use Preferences DataStore. It replaces the old SharedPreferences and is safe to call from coroutines.
// build.gradle.kts
// implementation("androidx.datastore:datastore-preferences:1.1.4")
import android.content.Context
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private val Context.dataStore by preferencesDataStore(name = "settings")
class SettingsRepository(private val context: Context) {
private object Keys {
val DARK_MODE = booleanPreferencesKey("dark_mode")
val USERNAME = stringPreferencesKey("username")
}
val darkMode: Flow<Boolean> = context.dataStore.data.map { it[Keys.DARK_MODE] ?: false }
suspend fun setDarkMode(enabled: Boolean) {
context.dataStore.edit { it[Keys.DARK_MODE] = enabled }
}
val username: Flow<String> = context.dataStore.data.map { it[Keys.USERNAME] ?: "" }
suspend fun setUsername(name: String) {
context.dataStore.edit { it[Keys.USERNAME] = name }
}
}
@Composable
fun SettingsScreen(repo: SettingsRepository) {
val darkMode by repo.darkMode.collectAsStateWithLifecycle(initialValue = false)
Switch(
checked = darkMode,
onCheckedChange = { scope.launch { repo.setDarkMode(it) } }
)
}
Add a Room NoteEntity and NoteDao with insert and observeAll. Build a NoteViewModel that exposes StateFlow<List<Note>> and an addNote(title) function. Add a DataStore key last_opened (Long) and update it each time the screen opens.