【習慣トラッカー開発記 #5】Room × Flow × ViewModelでリアクティブなデータ更新を実装

はじめに

前回(#4)では、HabitCategory などのエンティティ構成を整理しました。

今回はそれを実際に UIへリアルタイム反映させる仕組み を実装していきます。

具体的には次のような流れを作ります:

Room(DB更新)
 → Repository
 → ViewModel(Flowを購読)
 → Compose(collectAsStateでUI更新)

いわゆる「リアクティブUI」の仕組みです。特別なイベント通知を使わず、データの変化がそのままUI更新につながるようにします。

実装の全体構成

Room(HabitDao)
   ↓ Flowでデータを返す
Repository(HabitRepository)
   ↓
ViewModel(HabitViewModel)
   ↓
UI(HabitCardListScreen)

DAO:Flowを返すクエリ

Roomでは、Flow<List<T>>を返すクエリを定義するだけで、データの変更を自動的に監視し、DB更新をUIへそのまま反映してくれます。

@Dao
interface HabitDao {

    @Query("SELECT * FROM habit ORDER BY updatedAt DESC")
    fun getAllHabits(): Flow<List<Habit>>

    @Update
    suspend fun update(habit: Habit)

    @Insert
    suspend fun insert(habit: Habit)

    @Delete
    suspend fun delete(habit: Habit)
}

Roomで Flow<List<T>> を返すだけで、データ変更時に自動で新しいリストがFlowとして発行されます。

Repository:Flowをそのまま返す

Repositoryでは、DAOからのFlowをそのまま返します。

これによりViewModel側は中間層を意識せずに購読できます。

class HabitRepository @Inject constructor(
    private val habitDao: HabitDao
) {
    fun getAllHabits(): Flow<List<Habit>> = habitDao.getAllHabits()

    suspend fun updateHabit(habit: Habit) = habitDao.update(habit)
    suspend fun insertHabit(habit: Habit) = habitDao.insert(habit)
    suspend fun deleteHabit(habit: Habit) = habitDao.delete(habit)
}

ViewModelでは、RepositoryのFlowをStateFlowに変換します。

collectAsState()と組み合わせるためです。

@HiltViewModel
class HabitViewModel @Inject constructor(
    private val repository: HabitRepository
) : ViewModel() {

    val habitList: StateFlow<List<Habit>> = repository.getAllHabits()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )

    fun toggleHabitEnable(habit: Habit) {
        viewModelScope.launch {
            repository.updateHabit(habit.copy(enable = !habit.enable))
        }
    }
}

UI(Compose):collectAsState()でリアクティブ更新

最後に、UI側です。

HabitCardListScreen でViewModelのhabitListを購読します。

@Composable
fun HabitCardListScreen(viewModel: HabitViewModel = hiltViewModel()) {
    val habits by viewModel.habitList.collectAsState()

    LazyColumn {
        items(habits) { habit ->
            HabitCard(
                habit = habit,
                onToggle = { viewModel.toggleHabitEnable(habit) }
            )
        }
    }
}

collectAsState() を使うだけで、DBでデータが変われば即座にUIも更新されます。

つまり「状態を手動で再読込する」必要がありません。

実際のUI動作イメージ

  1. ON/OFFスイッチをタップ
  2. ViewModelでenable更新
  3. Repository → DAO → RoomでDB更新
  4. Flowが新しいデータを発行
  5. collectAsState()がUI再描画をトリガー

一度この流れができると、アプリ全体が非常にシンプルになります。

どこか1箇所で状態が変われば、全てのUIが最新状態に保たれます。

なぜこの構成にしたか

従来のLiveData + Adapter構成だと、UIの更新イベントを自分で書く必要がありました。

しかしCompose + Flow構成では、データに対してUIが結果として動くという仕組みになります。

アプリ開発でありがちな「UIの再描画忘れ」や「更新漏れ」が自然と減ります。

実際、私自身開発中に「再描画処理いらないの?」と驚くぐらいです。

今後の予定

次回は実際の画面構成についてお話いたします。

テーマは「今日の習慣一覧画」のUI/UX の工夫や、カードデザインのポイントについて解説していきます。

タイトルとURLをコピーしました