はじめに
前回(#4)では、Habit・Category などのエンティティ構成を整理しました。
今回はそれを実際に 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動作イメージ
- ON/OFFスイッチをタップ
- ViewModelで
enable更新 - Repository → DAO → RoomでDB更新
- Flowが新しいデータを発行
collectAsState()がUI再描画をトリガー
一度この流れができると、アプリ全体が非常にシンプルになります。
どこか1箇所で状態が変われば、全てのUIが最新状態に保たれます。
なぜこの構成にしたか
従来のLiveData + Adapter構成だと、UIの更新イベントを自分で書く必要がありました。
しかしCompose + Flow構成では、データに対してUIが結果として動くという仕組みになります。
アプリ開発でありがちな「UIの再描画忘れ」や「更新漏れ」が自然と減ります。
実際、私自身開発中に「再描画処理いらないの?」と驚くぐらいです。
今後の予定
次回は実際の画面構成についてお話いたします。
テーマは「今日の習慣一覧画」のUI/UX の工夫や、カードデザインのポイントについて解説していきます。

