Room์ผ๋ก ๋ก์ปฌ DB์ ์ ์ฅํ๊ธฐ
์ง๋ ๊ธ์์ to-do ์ฑ์ ViewModel + StateFlow๋ก ์ ๋ฆฌํ๋ค. ๊ทผ๋ฐ todoList๋ ์ฌ์ ํ ์ฑ์ ์คํํ ๋๋ง๋ค ์ฝ๋ ์์ ๊ณ ์ ๊ฐ(์ฅ๋ณด๊ธฐ, ์ด๋ํ๊ธฐ, ์ฑ
์ฝ๊ธฐ)์ผ๋ก ๋ค์ ์ฑ์์ง๋ค. ์ฑ์ ๊ป๋ค ์ผ๋ ํ ์ผ์ด ๋จ์์๊ฒ ํ๋ ค๋ฉด, ๋ฉ๋ชจ๋ฆฌ๊ฐ ์๋๋ผ ๋์คํฌ์ ์ ์ฅํด์ผ ํ๋ค. Room์ ์ฐ๋ฉด ๋๋ค.
์์กด์ฑ ์ถ๊ฐ
Room์ Kotlin Symbol Processing(KSP)์ผ๋ก ์ฝ๋๋ฅผ ์๋ ์์ฑํ๋ค. ๋จผ์ ํ๋ก์ ํธ ์ต์์ build.gradle.kts์ KSP ํ๋ฌ๊ทธ์ธ์ ์ถ๊ฐํ๋ค.
// ์ต์์ build.gradle.kts
plugins {
id("com.google.devtools.ksp") version "2.3.4" apply false
}๊ทธ๋ค์ ์ฑ ๋ชจ๋ build.gradle.kts์ ํ๋ฌ๊ทธ์ธ ์ ์ฉ๊ณผ ์์กด์ฑ์ ์ถ๊ฐํ๋ค.
// app/build.gradle.kts
plugins {
id("com.google.devtools.ksp")
}
dependencies {
val roomVersion = "2.8.5"
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
ksp("androidx.room:room-compiler:$roomVersion")
}(KSP ํ๋ฌ๊ทธ์ธ ๋ฒ์ ์ ์ฐ๋ Kotlin ๋ฒ์ ๊ณผ ๋ง์ถฐ์ผ ํด์, ์ค์ ๋ก ์ ์ฉํ ๋๋ KSP ๋ฆด๋ฆฌ์ฆ ํ์ด์ง์์ ํ์ฌ Kotlin ๋ฒ์ ์ ๋ง๋ ๊ฑธ ํ์ธํด์ผ ํ๋ค.)
Todo๋ฅผ ํ ์ด๋ธ๋ก ์ ์ธํ๊ธฐ
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "todo")
data class Todo(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String,
val isDone: Boolean = false
)์ง๊ธ๊น์ง๋ nextId๋ฅผ ์ง์ ๊ด๋ฆฌํ๋ฉด์ id๋ฅผ ํ๋์ฉ ๋งค๊ฒผ๋๋ฐ, @PrimaryKey(autoGenerate = true)๋ฅผ ์ฐ๋ฉด Room์ด ์ ํ์ ๋ฃ์ ๋๋ง๋ค ์์์ ๋ฒํธ๋ฅผ ๋งค๊ฒจ์ค๋ค. ๊ทธ๋์ ์๋ก ๋ง๋ค ๋๋ id = 0์ผ๋ก ๋๋ฉด ๋๋ค(Room์ด ์๋ ์์ฑ ๋์์ด๋ผ๋ ๊ฑธ ์์๋ณด๋ ๊ฐ์ด๋ค).
Dao - ๋๋น์ ํ ์ง๋ฌธ๊ณผ ์์ฒญ์ ์ ์ํ๋ค
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface TodoDao {
@Query("SELECT * FROM todo ORDER BY id")
fun getAll(): Flow<List<Todo>>
@Insert
suspend fun insert(todo: Todo)
@Update
suspend fun update(todo: Todo)
@Delete
suspend fun delete(todo: Todo)
}getAll()์ด Flow<List<Todo>>๋ฅผ ๋๋ ค์ค๋ค๋ ๊ฒ ์ค์ํ๋ค. ํ ๋ฒ ์กฐํํด์ ๋๋๋ ๊ฒ ์๋๋ผ, ํ
์ด๋ธ ๋ด์ฉ์ด ๋ฐ๋ ๋๋ง๋ค Room์ด ์์์ ์ ๋ฆฌ์คํธ๋ฅผ ํ๋ ค๋ณด๋ด ์ค๋ค. insert, update, delete๋ ๋์คํฌ์ ์ฐ๋ ์์
์ด๋ผ ์๊ฐ์ด ๊ฑธ๋ฆด ์ ์์ด์ suspend๋ก ์ ์ธํ๋ค.
Database
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(entities = [Todo::class], version = 1)
abstract class TodoDatabase : RoomDatabase() {
abstract fun todoDao(): TodoDao
}ViewModel์ Room์ ์ฐ๊ฒฐํ๊ธฐ
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import androidx.room.Room
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class TodoViewModel(application: Application) : AndroidViewModel(application) {
private val dao = Room.databaseBuilder(application, TodoDatabase::class.java, "todo.db")
.build()
.todoDao()
val uiState: StateFlow<TodoUiState> = dao.getAll()
.map { todoList -> TodoUiState(todoList = todoList) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = TodoUiState(isLoading = true)
)
fun addTodo(title: String) {
if (title.isBlank()) return
viewModelScope.launch { dao.insert(Todo(title = title)) }
}
fun toggleDone(todo: Todo) {
viewModelScope.launch { dao.update(todo.copy(isDone = !todo.isDone)) }
}
fun deleteTodo(todo: Todo) {
viewModelScope.launch { dao.delete(todo) }
}
}[์ด๋ฏธ์ง: ํ ์ผ์ ์ถ๊ฐํ ๋ค ์ฑ์ ์์ ํ ์ข ๋ฃํ๊ณ ๋ค์ ์ผ๋ ๋ชฉ๋ก์ด ๋จ์์๋ ๋ชจ์ต]
ViewModel์ด AndroidViewModel๋ก ๋ฐ๋์๋ค. Room.databaseBuilder๋ก DB๋ฅผ ๋ง๋ค๋ ค๋ฉด Context๊ฐ ํ์ํ๋ฐ, AndroidViewModel์ application์ ํตํด ๊ทธ Context๋ฅผ ์ ๊ณตํด์ค๋ค. viewModel() ์ปดํฌ์ ๋ธ ํจ์๋ ์ด๋ฐ AndroidViewModel๋ ์์์ ๋ง๋ค์ด์ฃผ๊ธฐ ๋๋ฌธ์, ํธ์ถํ๋ ์ชฝ ์ฝ๋๋ ๊ทธ๋๋ก viewModel()๋ง ์ฐ๋ฉด ๋๋ค.
dao.getAll()์ด ์ฃผ๋ Flow๋ฅผ .map { ... }์ผ๋ก TodoUiState๋ก ๊ฐ์ธ๊ณ , .stateIn(...)์ผ๋ก StateFlow๋ก ๋ฐ๊ฟจ๋ค. SharingStarted.WhileSubscribed(5000)์ "๊ตฌ๋
ํ๋ ํ๋ฉด์ด ์๋ ๋์, ๊ทธ๋ฆฌ๊ณ ๋ง์ง๋ง ๊ตฌ๋
์๊ฐ ์ฌ๋ผ์ง ๋ค 5์ด ๋์์ ๊ณ์ ๋๋น๋ฅผ ๋ณด๊ณ ์๋ค๊ฐ, ๊ทธ ๋ค๋ก ๋ณด๋ ์ฌ๋์ด ์์ผ๋ฉด ๋ฉ์ถ๋ค"๋ ๋ป์ด๋ค. ํ๋ฉด์ด ์ ๊น ๊บผ์ก๋ค๊ฐ ๋ฐ๋ก ์ผ์ง๋ ๊ฒฝ์ฐ์ ๋งค๋ฒ ์ฒ์๋ถํฐ ๋ค์ ์กฐํํ์ง ์๊ฒ ํด์ค๋ค.
๊ทผ๋ฐ toggleDone, deleteTodo๊ฐ id ๋์ todo ๊ฐ์ฒด ์ ์ฒด๋ฅผ ๋ฐ๋๋ค
fun toggleDone(todo: Todo) { ... } // id: Int๊ฐ ์๋๋ผ todo: Todo6๋ฒ ๊ธ์์๋ ํ๋ฉด ์ด๋ํ ๋ "๊ฐ์ฒด๋ฅผ ํต์งธ๋ก ๋๊ธฐ์ง ๋ง๊ณ id๋ง ๋๊ฒจ๋ผ"๊ณ ํ๋๋ฐ, ์ง๊ธ์ ๋ฐ๋๋ก id๊ฐ ์๋๋ผ ๊ฐ์ฒด ์ ์ฒด๋ฅผ ๋ฐ๊ณ ์๋ค. ์ด์ ๊ฐ ๋ค๋ฅด๋ค. @Update, @Delete๋ ๋๊ฒจ๋ฐ์ ๊ฐ์ฒด์ ๊ธฐ๋ณธํค(id)๋ก "์ด๋ค ํ์ธ์ง"๋ฅผ ์ฐพ๊ธด ํ์ง๋ง, ๋๋จธ์ง ์นผ๋ผ ๊ฐ๋ ๊ทธ ๊ฐ์ฒด์ ์๋ ๊ฐ์ผ๋ก ํต์งธ๋ก ๋ฎ์ด์ด๋ค. id๋ง ๋์ ธ์ฃผ๋ฉด Room์ ์ ๋ชฉ์ด ๋ญ์๋์ง, ์๋ฃ ์ํ๊ฐ ๋ญ์๋์ง ์ ๋ฐฉ๋ฒ์ด ์๋ค. ๊ทธ๋์ toggle ๊ฐ์ "์ผ๋ถ๋ง ๋ฐ๊พธ๋" ์์
๋, ๊ฒฐ๊ตญ ์ ์ฒด ๊ฐ์ฒด๋ฅผ ๊ฐ์ ธ์์ ๊ทธ ์ค ํ ํ๋๋ง copy()๋ก ๋ฐ๊พผ ๋ค์ ํต์งธ๋ก ๋ค์ ๋๊ฒจ์ผ ํ๋ค.
๋ฐ๋ฉด ํ๋ฉด ์ด๋(navigate)์ ๊ทธ ํ๋ฉด์ด ์์์ ์ต์ ๋ฐ์ดํฐ๋ฅผ ๋ค์ ์กฐํํ๊ฒ ๋ง๋๋ ๊ฒ ๋ชฉ์ ์ด๋ผ id๋ง ๋๊ธฐ๋ ๊ฒ ๋ง์๋ค. ์ํฉ์ ๋ฐ๋ผ "์ด์ ๋ง ๋๊ธฐ๋ ๊ฒ ๋ง์ ๋"์ "์ ์ฒด๋ฅผ ๋๊ฒจ์ผ ํ ๋"๊ฐ ๋ค๋ฅด๋ค๋ ๊ฒ๋ง ๊ธฐ์ตํ๋ฉด ๋๋ค.
์ด ๋ณ๊ฒฝ์ ๋ง์ถฐ TodoListScreen, TodoDetailScreen์ onToggleDone, onDelete ์ฝ๋ฐฑ ํ์
๋ (Int) -> Unit์์ (Todo) -> Unit์ผ๋ก, ํธ์ถ๋ถ๋ onToggleDone(todo.id)์์ onToggleDone(todo)๋ก ๋ฐ๋๋ค. ๋๋จธ์ง ์ฝ๋ ๊ตฌ์กฐ๋ ๊ทธ๋๋ก๋ค.
์ ๋ฆฌ
Flow<List<Todo>>๋ฅผ ๋๋ ค์ฃผ๋Dao๋ฅผ ๋ง๋ค๋ฉด, ํ ์ด๋ธ์ด ๋ฐ๋ ๋๋ง๋ค ์๋์ผ๋ก ์ ๋ฆฌ์คํธ๊ฐ ํ๋ฌ๋์จ๋ค.stateIn()์ผ๋ก ์ดFlow๋ฅผStateFlow๋ก ๋ฐ๊ฟ์ViewModel์ ๋ ธ์ถํ๋ค@Update/@Delete๋ ๊ฐ์ฒด ์ ์ฒด๊ฐ ํ์ํด์, ์ฝ๋ฐฑ๋id๋์Todo๊ฐ์ฒด๋ฅผ ๋ฐ๋ ์ชฝ์ผ๋ก ๋ฐ๋๋ค
๋ค์ ๊ธ์์๋ ๋คํฌ ๋ชจ๋ ๊ฐ์ "๊ฐ ํ๋์ง๋ฆฌ ์ค์ "์ ์ ์ฅํ ๋ Room๋ณด๋ค ๋ ๊ฐ๋ฒผ์ด DataStore๋ฅผ ๋ค๋ค๋ณด๊ฒ ๋ค.
