LoginSignup
5
8

Kotlin Flowを使ったポーリングの実装

Last updated at Posted at 2024-02-10

はじめに

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は停止します。

ViewModel
    val users: StateFlow<List<UserData>> = usersFlow()
        .stateIn(
            viewModelScope,
            SharingStarted.WhileSubscribed(), // 必要な時だけアクティブにする
            emptyList() // 初期値は空のリスト
        )

Lifecycleを考慮したStateFlowの収集

StateFlowをComposableの中で使うには、Stateに変換します。普通はcollectAsState()を使いますが、ここではcollectAsStateWithLifecycle()を使います。こうすることで、アプリがバックグラウンドになったとき、値の収集が止まり、上流のFlowの動作が停止します。

Composable
    val users by viewModel.users.collectAsStateWithLifecycle()
    LazyColumn {
        items(users) { user ->
            Text(user.name)
        }
    }

Flowの再起動

ユーザーが明示的に更新処理を行ったとき、実行中の定期処理のタイミングに関わらず更新を行い、その後、新しい定期処理を行うにはどうしたらいいでしょうか。

flatMapLatest()を使ってFlowを再起動する方法を考えてみました。

ViewModel
    // 更新処理の制御用
    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を再起動

参考

5
8
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
5
8