2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FlowとStateFlowを用いた状態管理

Posted at

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アプリになっています。

Githubリポジトリはこちら

3.1 Repository層:Flowでデータ監視

TaskDao.kt
@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: 単一画面の状態管理

AddTaskViewModel.kt
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種類保持しています。_uiStateMutableStateFlowとして保持され、privateであるためにViewModelの外部からは更新することが出来ません。uiStateはStateFlowとして保持され、_uiStateにより更新されます。これにより、UI側からは状態の監視のみ可能にして不変性を保っています。

パターン2: 複数Flowの組み合わせ

HomeViewModel.kt
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制御

AddTaskScreen.kt
@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の更新を行ったりすることが出来ます。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?