アイリッジ開発者ブログ

アイリッジに所属するエンジニアが技術情報を発信していきます。

Kotlin Flow 概説&チートシート [Android]

Kotlin Flow とは?

Kotlin Flowは、非同期処理を行うためのライブラリです。Kotlinのコルーチンという機能を発展させたものです。

非同期処理とは、コンピュータプログラムの処理方式の一つで、ある処理が完了するまで待たずに、他の処理を実行することができる仕組みです。従来の同期処理では、ある処理が完了するまで、次の処理を実行することができませんでしたが、非同期処理では、複数の処理を同時に実行することができます。

例えば、ウェブページの読み込みや、アプリ・サーバー間の通信などのように、長時間かかる処理がある場合、従来の同期処理ではユーザーは処理が完了するまで待たなければならず、ユーザーの操作に反応しない状態に陥ってしまいます。しかし、非同期処理を使うことで、ユーザーは待つことなく別の操作を実行することができます。

一般的に、非同期処理は、マルチスレッドプログラミング、イベント駆動プログラミング、コールバックなどの方法で実装されます。

同様の機能を果たすAndroidではRxKotlinや、Kotlin コルーチンという仕組みがありましたが、Kotlin FlowはKotlin コルーチンを発展させたものです。

「Flow」という言葉は、非同期に実行される値のストリームを表すためのもので、いわゆるObservableやPromiseに似た役割を果たします[1] 。例えば、コールバックだけで非同期処理を実装したら、コールバックが幾重にも重なったり、時間的には連続して行われる処理がコード上ではバラバラの箇所に書かれるなど、見通しが悪くなってしまうデメリットがあります。Flowを用いると、非同期に流れてくるイベントを購読しておき、実際にイベントが発生したらどのような処理をしたいかをその後に記載できます。これにより、直感的かつシンプルな記載となります。

またAndroid のjetpack composeという最新のUI構築ライブラリと連携することも当然想定されており、簡単に連携することができます。

デメリットとしては、独特の用語や概念など覚えることがあるため、学習コストがかかることです。チートシートなどを利用し、これらの用語や概念をざっと把握してしまうのがまずおすすめです。

Emitter/Subscriber について

Emitter/Subscriberという概念があります。

これは、Flowの基本的な概念で、値を生成するエミッター(Emitter)と、その値を受け取る購読者(Subscriber)から構成されます。Emitterは、コルーチンの挙動と同様に、一つのスレッドで値を生成していきます。そして、生成された値はFlowパイプラインを通じてSubscriberに渡されます。以下は、Emitter/Subscriberの関係を表しています。

          Emitter ----> [Flow Pipeline(Event)] ----> Subscriber

なおProducer/Consumerという言葉が使われることもありますが、これはEmitter/Subscriberとほぼ同様の意味です。

Flow 関連クラス について

Flow、SharedFlow、MutableSharedFlow、StateFlow、MutableStateFlow、callbackFlowといった概念があります。これらは皆エミッターですが、字面からだけでは使い分けるのが難しいため、解説します。

図1 引用元: [2]

Flow

非同期で単一の値または複数の値を生成することができるインターフェースです。エラーを発生させるか、または正常に完了させることができる標準的な実装です。他の全てのFlow関連クラスの継承元となっています。

いわゆるコールド[4]なエミッターとなっており、サブスクライバが購読を開始するまでは何も動作をしません。また、複数の購読者がいた場合、それぞれの購読者に対して別々のストリームが生成されることになります。

後に説明するSharedFlowやStateFlowはホット[4]なエミッターですが、FlowはshareIn[5], stateIn[6]オペレータを呼ぶことでこれらに変換することができます。

Flowを用いた一例を示します。小文字のflow関数を用いてFlowを生成しています。ここではいわゆるフィボナッチ数列を生成してストリームに流しています。

fun fibonacci(): Flow<BigInteger> = flow {
    var x = BigInteger.ZERO
    var y = BigInteger.ONE
    while (true) {
        emit(x)
        x = y.also {
            y += x
        }
    }
}

fibonacci().take(100).collect { println(it) }
SharedFlow

複数のコレクターが共有できる、複数の値を生成することができます。Flowを継承しています。

値はブロードキャストされ、すべてのコレクターに配信されます。いわゆるホット[4]なエミッターとなっているので、複数のコレクターから購読されても、複数のストリームが生成されるなどということはありません。常に一つのストリームしかないことになります。そのため、サーバーとの通信など、高価な処理を無駄に何回もやってしまうという事態を防ぐことができます。

任意の数のキャッシュを設定することができ、最新の値+キャッシュされた古い値を購読時に取得できます。

MutableSharedFlow

SharedFlowを継承した型です。SharedFlowと同様に、複数のコレクターが共有できる複数の値を生成することができます。 ただし、MutableSharedFlowは、emit()関数によって値を生成することができます。

emit()関数によって値を生成することができることから、外部からemit()を呼ばれて勝手な操作で思わぬ値を生成させられてしまうことを防ぐ必要があります。そのため、MutableSharedFlowはprivateな変数としておき、外部にはSharedFlowを露出させるのが良いと考えられます。

// ViewModelなどに以下を記載
// MutableSharedFlowにはprivateをつけて、外部から直接アクセスさせない
private val _items = MutableSharedFlow<List<Item>>()
val items: SharedFlow<List<Item>> = _items

// MutableSharedFlowには直接アクセスさせないため、代わりに関数を設けて外部へ公開する。
fun updateItems(newItems: List<Item>) {
    _items.tryEmit(newItems)
}

// Fragment, ActivityなどUI側に以下を記載
lifecycleScope.launch {
    viewModel.items.collect { items ->
        // アイテムを用いてUIを描画
    }
}
StateFlow

SharedFlowを継承したクラスになります。特徴としては以下が挙げられます。 - 初期値を持つ = 常に何かの値を持っていることが保証される - 古い値を蓄積しておくキャッシュがない = 常に値を一つだけ持っており、複数持つことはない。この値はvalueというプロパティ経由で簡単に取り出せる。

以上のような特徴を持つため、UIを描画するためのデータを流す場所として役に立ちます。UIは常に何かを描画しておかないといけないため、最初から最後までたった一つの値を保つことが保証されているStateFlowは都合がいいのです。

値の変更を追跡できる単一の値を生成することができます。 StateFlowは、ViewModelなどのライフサイクル感知型コンポーネントで使用することができます。 生成されたデータは、一度に1つのコレクターにのみ配信されます。

MutableStateFlow

StateFlow を継承したクラスです。emit()関数によって値を生成することができます。

使い分けの典型例
  • SharedFlowにデータレイヤーの処理を担当させ、サーバーと通信を行わせる
  • その他、アプリ全般においてデータを伝搬させるにおいても、SharedFlowを使う
  • UIへの反映はStateFlowで行う。 というのが典型的パターンになります[6]ので、コードで一例を示します。
// APIと通信を行うクラス
class APIRepository {
    val sharedFlow = MutableSharedFlow<String>()
    
    suspend fun fetchData() {
           // サーバーと通信を行い、データを取得したものとします
           var fetchedData = "HOGEHOGE"
           sharedFlow.emit(fetchedData)
    }
}

APIと通信を行うクラス APIRepositoryですがMutableSharedFlowを持っています。通信が行われるまでは何もデータがないため、StateFlowと違い初期値を持っていないMutableSharedFlowを使っています。

    private val repository = APIRepository()
    // stateIn を用いて、SharedFlowをStateFlowに変換
    val stateFlow: StateFlow<String> = repository.sharedFlow
        .stateIn(
            scope = viewModelScope,
            started = WhileUiSubscribed,
            initialValue = "Loading"
    )
    
    // 初期化時に呼ばれる
    init {
        viewModelScope.launch {
                // API読み込み開始
                repository.load()
        }
    }

ViewModel(データ通信の層とUIの層を仲立ちするためのクラス)において、SharedFlowをStateFlowに変換します。これでUI層にデータを見せる準備をしています。

@Composable
fun MainScreen() {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    Column() {
        Text(text = text)
    }
}

UI層のメイン画面です。collectAsStateWithLifecycleで、StateFlowをJetpack ComposeのStateに変換し、UIとして表示しています。

callbackFlow

こちらはクラスではなく、Flowを返す関数です。 コールバックベースの使いにくいAPIをFlowに変換することができます。 生成されたデータは、emit()関数を使用して新しい値を生成することによって、複数のコレクターに配信されます。[7]

参照

[1] Android Developers - Android での Kotlin Flow
[2]Kotlin Flows ~ an Android cheat sheet
[3]Flow Official Documentation
[4]RxのHotとColdについて
[5]shareIn
[5]stateIn
[6]KotlinのSharedFlowとStateFlowの違いを理解する
[7]callBackFlow