はじめに
前回はアプリ全体の設計方針や構成について紹介しました。
今回は、開発を支える主要ライブラリ「Jetpack Compose」「Room」「Hilt」「Coroutines / Flow」などを中心に、
なぜこれらを選定したのか、そして実際にどのように実装しているのかをコード例付きで紹介します。
Jetpack Compose:UIを状態で動かす
習慣アプリでは「毎日更新されるデータ(達成状態)」を扱うため、UIと状態を密接に連動させる必要があります。
Composeは宣言的UIの仕組みによって、状態(StateFlowなど)の変化に応じて自動的にUIを再描画してくれます。
@Composable
fun HabitCard(
habit: HabitView,
onCheckedChange: (Boolean) -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = habit.title,
modifier = Modifier.weight(1f)
)
Checkbox(
checked = habit.isCompleted,
onCheckedChange = { onCheckedChange(it) }
)
}
}
}
状態の変化に応じて Checkbox
のON/OFFが即時反映されるため、View更新のためのnotifyDataSetChanged()
やAdapter処理は不要になります。
採用理由
- 状態変化をUIに自動反映できる
- 小規模〜中規模アプリに最適なシンプル設計が可能
Room:型安全なローカルデータベース
習慣データ(例:タイトル、カテゴリ、達成状態)を永続化するためにRoomを採用しました。
SQL文を書くことなく、データクラスとDAOで安全にCRUDを実装できます。
@Dao
interface HabitDao {
@Query("SELECT * FROM habits")
fun getAllHabits(): Flow<List<HabitEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(habit: HabitEntity)
@Update
suspend fun update(habit: HabitEntity)
@Delete
suspend fun delete(habit: HabitEntity)
}
Coroutines × Flow:非同期とリアクティブUI
Roomのクエリは Flow<List<HabitEntity>>
を返すことで、データベース変更時にUIを自動で更新できるようにしています。
@HiltViewModel
class TodayViewModel @Inject constructor(
private val repository: HabitRepository
) : ViewModel() {
val habits: StateFlow<List<HabitView>> = repository
.getAllHabits()
.map { entities ->
entities.map { HabitView.fromEntity(it) }
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
fun toggleHabit(habit: HabitView) {
viewModelScope.launch {
repository.update(habit.copy(isCompleted = !habit.isCompleted))
}
}
}
ViewModelがFlowを購読し、UI (HabitCardList
) 側では collectAsState()
で自動的にUI更新が走ります。
採用理由
- コルーチンでUIスレッドをブロックせず安全に処理
- Flowを通して「DB→Repository→ViewModel→UI」がリアルタイム連携
- 非同期処理が明確かつ保守しやすい構造
ViewModelとUIのリアクティブな連携
ViewModel 側では、Flow
(または StateFlow
)でデータを公開しています。
これは「ストリーム(流れ)」のようなもので、データが変わるたびに新しい値が流れる仕組みです。たとえば、習慣一覧を取得する HabitListViewModel
は次のようになります。
@HiltViewModel
class HabitListViewModel @Inject constructor(
private val habitRepository: HabitRepository
) : ViewModel() {
// RepositoryのFlowをそのままUI層へ公開
val habitList = habitRepository.getAllHabits()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}
このhabitList は StateFlow型のため、UI 側が購読 (collect) している間、最新状態で表示できます。
UI側(Jetpack Compose)での購読
Compose 側では collectAsState()
を使うと、Flow の値を Compose の State に変換できます。
@Composable
fun HabitCardList(viewModel: HabitListViewModel = hiltViewModel()) {
val habits by viewModel.habitList.collectAsState() // ← ここでUIとFlowがつながる
LazyColumn {
items(habits) { habit ->
HabitCard(habit)
}
}
}
ここで重要なのは以下になります。
collectAsState()
が Flow を監視し、値が変わるたびに再コンポーズが走る- ViewModel → Flow → State → Compose という「リアクティブなつながり」がある
- UI 側では「再描画のトリガー」を書く必要がなくなる(宣言的UI)
仕組みのイメージ
Room(Flow)
↓ (DB更新)
Repository
↓
ViewModel(Flow→StateFlow化)
↓
collectAsState()
↓
Compose再描画(UIが自動更新)
つまり、RoomがDB更新を通知 → Flowが発火 →ViewModel経由でUIに流れる → Composeが再描画される、という自動の流れです。
なぜこの構成にしたか
以前の View + LiveData 構成だと、observe()
を書いたり、notifyDataSetChanged()
を呼んだりして、データの更新を自分で通知する必要がありました。
でも Compose × Flow にしてからは、データが変わればUIが自動で再描画されるようになります。
単位的に説明すると「データが主役でUIがついてくる」仕組みです。
collectAsState()
で Flow を購読するだけで、Room → Repository → ViewModel → UI の更新がすべてリアルタイムに繋がります。
これによって、明示的に再読み込み処理を書かなくても、DBの変更をそのままUIに反映できるようになり非常に便利です。
Coroutines × Flow:リアクティブなデータ更新とUI反映
Roomのクエリ結果を Flow
で受け取り、ViewModelが購読することで、
データベースの変更をリアルタイムにUIへ反映できるようにしています。
@HiltViewModel
class TodayHabitViewModel @Inject constructor(
private val habitRepository: HabitRepository,
) : ViewModel() {
private val _habits = MutableStateFlow<List<HabitUiModel>>(emptyList())
val habits: StateFlow<List<HabitUiModel>> = _habits.asStateFlow()
init {
viewModelScope.launch {
val today = (Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 1).toString()
// DBのFlowを購読し、UI用モデルに変換
habitRepository.getTodayHabitsList(today)
.map { list -> list.map { it.toUiModel() } }
.collect { uiList ->
_habits.value = uiList // ← 値が変わるとUIも自動更新
}
}
}
fun updateHabitEnable(habitId: Long, isChecked: Boolean) {
viewModelScope.launch {
habitRepository.updateHabitEnable(habitId, isChecked)
}
}
}
UI側では collectAsState() で自動更新
@Composable
fun TodayHabitScreen(viewModel: TodayHabitViewModel = hiltViewModel()) {
val habits by viewModel.habits.collectAsState()
LazyColumn {
items(habits) { habit ->
HabitCard(habit)
}
}
}
データ更新の流れ
Room(Flow)
↓
Repository
↓
ViewModel (collect → MutableStateFlow)
↓
UI (collectAsState → 自動再描画)
採用理由
Room + Flow
により、DB変更を即座にUIへ反映できるCoroutine
により、UIスレッドをブロックせず安全に非同期処理StateFlow
でUIが常に最新状態を保持ViewModel
内でUIモデル変換を行うことで、UI層をシンプルに保てる
Hilt:依存関係の自動注入
Hiltを使うことで、ViewModelやRepositoryの依存関係を自動的に解決できます。
特にRoomやRepositoryなど、複数画面で使うクラスを共通管理する際に便利です。
RepositoryとDI設定例
class HabitRepository @Inject constructor(
private val habitDao: HabitDao
) {
fun getAllHabits() = habitDao.getAllHabits()
suspend fun update(habit: HabitView) = habitDao.update(habit.toEntity())
}
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase =
Room.databaseBuilder(context, AppDatabase::class.java, "habit_db").build()
@Provides
fun provideHabitDao(db: AppDatabase): HabitDao = db.habitDao()
}
採用理由
ViewModel
やRepository
の依存を明示的に分離- テストや拡張(Mock化)に対応しやすい
Applicationスコープ
でのインスタンス管理が簡単
ライブラリ選定まとめ
ライブラリ | 役割 | 採用理由 |
---|---|---|
Jetpack Compose | UI構築 | 状態駆動で再描画を自動化。XML不要でシンプル |
Room | 永続化 | 型安全で、Flow連携可能なDB |
Hilt | 依存注入 | RepositoryやDaoの管理を自動化 |
Coroutines / Flow | 非同期処理 | UIとDBをリアルタイムで同期 |
Timber | ロギング | 軽量で読みやすいログ出力 |
まとめ
今回紹介した技術スタックは、どれも「モダンAndroid開発の基盤」として幅広く使用されています。
特に Compose × Flow × Hilt
の組み合わせは、UIとデータを自然に結びつけることができ、開発体験が非常にスムーズです。
今後は、この基盤の上に「習慣達成ログ」や「統計グラフ」などの機能を追加し、アプリとしての完成度を高めていく予定です。