はじめに
APIから一定間隔で自動的にデータを取得(ポーリング)して、UIを最新の状態に保ちたいことがあります。タイマーを使う方法もありますが、Flowを使うと安全で効率的な処理が綺麗に書けます。
ポーリング
今回、必要な条件は以下のように考えました。
- 一定間隔でAPIに問い合わせて画面を更新する
- UIコンポーネントが画面に表示されていない時は、更新処理を行わない
- ユーザーが任意のタイミングで更新することもできる
Flow
Flowは非同期なデータの流れです。このケースでは、定期的にAPIに問い合わせた結果が流れていく場所がFlowになります。
Flowは「コールド」です。コールドとは、値を収集することではじめて動作するという意味です。つまり、UIが表示を行うためにFlowの値を収集することによって、Flowの処理が実行されます。
Flowの作成
flow
ビルダーを使ってFlowを作成します。
リポジトリからデータを取得して、emit
でFlowにデータを出力。これを一定間隔で繰り返します。
while(true)
は無限ループのように見えますが、Flowはコルーチンの一種であり、必要なくなればキャンセルされるものなので問題ありません。
fun usersFlow(): Flow<List<UserData>> = flow {
while (true) {
val users = usersRepository.fetchUsers()
emit(users) // 新しい値をFlowに流す
delay(5.seconds) // 5秒間待機
}
}
StateFlow
UIが最新のデータを監視して更新できるようにするためにはStateFlowを使います。
stateIn
を使ってFlowをStateFlowに変換します。ここでのポイントはWhileSubscribed
を指定して必要な時だけ上流のFlowをアクティブにすることです。言い換えると、このStateFlowの値を収集していない時は元のFlowは停止します。
val users: StateFlow<List<UserData>> = usersFlow()
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(), // 必要な時だけアクティブにする
emptyList() // 初期値は空のリスト
)
Lifecycleを考慮したStateFlowの収集
StateFlowをComposableの中で使うには、Stateに変換します。普通はcollectAsState()
を使いますが、ここではcollectAsStateWithLifecycle()
を使います。こうすることで、アプリがバックグラウンドになったとき、値の収集が止まり、上流のFlowの動作が停止します。
val users by viewModel.users.collectAsStateWithLifecycle()
LazyColumn {
items(users) { user ->
Text(user.name)
}
}
Flowの再起動
ユーザーが明示的に更新処理を行ったとき、実行中の定期処理のタイミングに関わらず更新を行い、その後、新しい定期処理を行うにはどうしたらいいでしょうか。
flatMapLatest()
を使ってFlowを再起動する方法を考えてみました。
// 更新処理の制御用
val refreshCount = MutableStateFlow<Int>(0)
val users: StateFlow<List<UserData>> = refreshCount
.flatMapLatest { usersFlow() }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
fun refresh() {
// refreshCountを変更して、新しいFlowを開始させる
refreshCount.update { it + 1 }
}
更新処理を制御するためのStateFlowを用意します。
flatMapXXXはFlowで得られた値を使って別のFlowを作成するとき、それらをひとつのFlowとして扱うためのものです。3種類あり、次のような違いがあります。
-
flatMapConcat
前のFlowが終わってから次のFlowを収集する -
flatMapMerge
すべてのFlowを並列に収集してマージする -
flatMapLatest
新しい値が流れてきたら、前のFlowをキャンセルして、新しいFlowを収集する
ここでは、refreshCountの値そのものに意味はなく、flatMapLatest
の「前のFlowをキャンセルして新しいFlowを開始するという」性質を利用して、Flowの再起動を行なっています。
まとめ
Flowを使ったポーリングの実装を紹介しました。タイマーを使う方法だとタイマーの開始・終了処理が複雑になり、タイマーを止め忘れると無駄な処理が動き続けることになりかねません。
「値を収集している時だけ動作する」というFlowの性質を使うと、定期実行処理の開始・終了が簡単になります。
ポイントをまとめると以下のようになります。
-
flow
で定期更新処理を作成 -
WhileSubscribed
なStateFlowに変換 -
collectAsStateWithLifecycle
で収集 -
flatMapLatest
でFlowを再起動
参考