はじめに
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を再起動 
参考