【習慣トラッカー開発記 #3】使用ライブラリの選定理由(Jetpack Compose / Room / Hilt / Coroutineなど)

はじめに

前回はアプリ全体の設計方針や構成について紹介しました。

今回は、開発を支える主要ライブラリ「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()
}

採用理由

  • ViewModelRepositoryの依存を明示的に分離
  • テストや拡張(Mock化)に対応しやすい
  • Applicationスコープでのインスタンス管理が簡単

ライブラリ選定まとめ

ライブラリ役割採用理由
Jetpack ComposeUI構築状態駆動で再描画を自動化。XML不要でシンプル
Room永続化型安全で、Flow連携可能なDB
Hilt依存注入RepositoryやDaoの管理を自動化
Coroutines / Flow非同期処理UIとDBをリアルタイムで同期
Timberロギング軽量で読みやすいログ出力

まとめ

今回紹介した技術スタックは、どれも「モダンAndroid開発の基盤」として幅広く使用されています。

特に Compose × Flow × Hilt の組み合わせは、UIとデータを自然に結びつけることができ、開発体験が非常にスムーズです。

今後は、この基盤の上に「習慣達成ログ」や「統計グラフ」などの機能を追加し、アプリとしての完成度を高めていく予定です。

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