1. はじめに
この記事では、Android開発におけるFlow・StateFlow・Stateを使った状態管理を、実際のTodoアプリを例にまとめています。間違いなどありましたらコメントでご指摘いただければ幸いです。
2. 各要素の概要
Flow(データ監視)
@Query("SELECT * FROM tasks WHERE isDone = 0")
fun getUnfinishedTasks(): Flow<List<Task>>
Flowは非同期データストリームを扱うためのライブラリであり、複数のデータを非同期に出力することができます。すなわち、データの受け渡しをメインスレッドをブロックすることなく行うことが出来ます。また、Flowはコールドストリームとして設計されており、collect が呼び出されたときに初めてデータの取得が開始されます。
用途: データベースの変更監視やネットワークからのデータ取得
StateFlow(画面状態管理)
private val _uiState = MutableStateFlow<AddTaskUiState>(AddTaskUiState.Input())
val uiState: StateFlow<AddTaskUiState> = _uiState
StateFlowはFlowの一種であり、常に最新の値1つを保持するという特徴を持っています。
常に最新の値を保持できる点やコンポーザブル関数側でStateに変換することで状態を監視し、変化に応じてリアクティブにデータの変更をUIに反映できる点などから、ViewModelでの状態管理に用いられています。
また、StateFlowは通常のFlowとは異なり、常に値を持ち続け、collectしたときにはすぐにその最新の値を受け取れるホットストリームという性質を持っています。
用途: ViewModelでの状態管理
State(ローカルUI状態)
var showDatePicker by remember { mutableStateOf(false) }
Stateは、JetpackCompose専用の状態管理の仕組みです。Composeにおいて、UIの再描画のトリガーとしての役割を果たします。このStateである値が変更されると、その値を使用しているComposable関数が自動的にUIの再描画を行います。
用途: Compose内での一時的なUI状態の保持
3. 実装パターンと活用方法
サンプルアプリの概要
まず、本記事で例として示すアプリの内容について簡単に説明しておきます。
今回使用するアプリは、
- タスク追加画面でのタスクの追加
- ホーム画面でのタスク一覧表示
- タスク編集画面での既存のタスクの編集
といった機能を備えた簡易的なTodoアプリになっています。
3.1 Repository層:Flowでデータ監視
@Query("SELECT * FROM tasks WHERE isDone = 0")
fun getUnfinishedTasks(): Flow<List<Task>>
上記のコードはタスクを保存するRoomデータベースを操作するためのDAOで定義されたメソッドの一つです。
Flowを返すDAOメソッドであるgetUnfinishedTasks()
はRoomデータベースの変更(データの追加、削除、更新等)を検知し、クエリ結果に変化がある場合はFlow内で新しいListをemit(再送信)します。
3.2 ViewModel層:StateFlowで状態管理
パターン1: 単一画面の状態管理
sealed interface AddTaskUiState {
// タスクの入力状態
data class Input(
val title: String = "",
val deadline: String = "",
val importance: TaskPriority = TaskPriority.MEDIUM,
) : AddTaskUiState
// タスクの保存中状態
data object Saving : AddTaskUiState
// タスクの保存成功状態
data object Success : AddTaskUiState
}
@HiltViewModel
class AddTaskViewModel @Inject constructor(
private val taskRepository: TasksRepository
) :
ViewModel() {
// 内部保存用
private val _uiState = MutableStateFlow<AddTaskUiState>(AddTaskUiState.Input())
// 外部公開用(外部からは変更できない)
val uiState: StateFlow<AddTaskUiState> = _uiState
....
}
AddTaskUiState
はUIの状態を表現するためのsealed interfaceです。Inputはユーザーがタスクを入力している状態、Savingはタスク保存処理中の状態、Successは保存完了を示す状態です。InputにのみUI表示に必要な情報(タスクの名前、締め切り、重要度)を定義しています。
AddTaskViewModel
ではこのAddTaskUiState
の値を内部で更新する用の_uiState
と外部に公開する用のuiState
で2種類保持しています。_uiState
はMutableStateFlow
として保持され、privateであるためにViewModelの外部からは更新することが出来ません。uiState
はStateFlowとして保持され、_uiState
により更新されます。これにより、UI側からは状態の監視のみ可能にして不変性を保っています。
パターン2: 複数Flowの組み合わせ
sealed interface HomeUiState {
// タスクの読み込み中状態
data object Loading : HomeUiState
// タスクの読み込み成功状態
data class Success(
val finishedTasks: List<Task>,
val unfinishedTasks: List<Task>,
val isEmpty: Boolean = false
) : HomeUiState
}
@HiltViewModel
class HomeViewModel @Inject constructor(
private val tasksRepository: TasksRepository
) : ViewModel() {
val uiState: StateFlow<HomeUiState> = combine(
tasksRepository.getFinishedTasks(), // 2つのFlowのどちらかに変更があったら新しい値を生成
tasksRepository.getUnfinishedTasks()
) { finishedTasks, unfinishedTasks ->
// ホーム画面のUI状態を変更
HomeUiState.Success(
finishedTasks = finishedTasks,
unfinishedTasks = unfinishedTasks,
isEmpty = (finishedTasks.isEmpty() && unfinishedTasks.isEmpty())
)
}.stateIn( // FlowをStateFlowに変換する
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = HomeUiState.Loading
)
....
}
上記のコードは、先ほどとは違って複数のFlowを組み合わせてUI状態を生成しています。
HomeUiState
では、UIの状態として、タスクの読み込み中であるLoading
とタスクの読み込みが成功した状態であるSuccessを定義しており、Success
では完了済みタスクと未完了のタスクがプロパティとして必要になります。
このように複数のFlowを組み合わせる場合はcombine
メソッドを利用します。このcombine
メソッドを用いることで複数のFlowを同時に監視し、そのうちいずれかのFlowでデータが更新されるたびにまとめて新しい値を生成します。
ここでは、完了したタスクのリストと未完了のタスクのリストのうちいずれかに変更があった場合にHomeUiState.Success
を用いて最新のUI状態が生成されます。
combine
メソッドで作成されたFlowはコールドストリームであるため、UI側で監視しやすくなるようstateIn
メソッドを用いてStateFlowに変換します。
3.3 Compose層:StateでUI制御
@Composable
fun AddTaskScreen(viewModel: AddTaskViewModel = hiltViewModel()) {
val uiState = viewModel.uiState.collectAsState()
var showDatePicker by remember { mutableStateOf(false) }
when (val state = uiState.value) {
is AddTaskUiState.Input -> { /* 入力画面 */ }
is AddTaskUiState.Saving -> CircularProgressIndicator()
}
}
Composable関数側では、ViewModelがStateFlowとして持っているUI状態をcollectAsState()
関数を用いてStateに変換して保持します。
これにより、UIはStateの変化に自動的に反応して再コンポーズされ、常に最新の状態を表示することが可能になります。
まとめ
この記事では、Flow,StateFlow,Stateを用いた状態管理についてまとめました。
このような状態管理の方法をとることにより、状態管理の複雑さを軽減したり、自動的なUIの更新を行ったりすることが出来ます。