LoginSignup
74
48

More than 1 year has passed since last update.

Kotlin Coroutines Flow とは。

Last updated at Posted at 2023-02-10

この記事は

  • Kotlin CoroutinesのFlowが何かよくわからないので基本を知りたい人向け
    • この記事1つでFlowの基本を網羅できればという強い気持ちによって少々長くなってしまったので流し読みでも:ok_woman: :ok_hand:
  • 私自身も勉強中で、この記事は百戦錬磨の実践から学んだものというよりかは、コツコツ座学的に勉強したことを自分用にまとめたものです

Android開発におけるFlowを使うメリット

  • Coroutinesの中には非同期処理を実行するのにワンショット系のデータを処理する場合とワンショットではない系(ストリーム系)のデータを処理する場合の大きく分けて二つあります

    • ワンショット系の場合はCoroutine scopeの中でsuspend funを実行します
    • ストリームタイプのデータの場合はFlowを使うと良いです
      • 例えば、最近Twitterでとても人気のあるツイートを見ている時、画面をリロード/pull-to-refreshをしなくても自動的にいいね数やリツイート数が更新され続けませんか?こういった連続的にデータが変化し、それをUIに反映させたい場合はFlowを用いるのが良さそうです
    • 参考:Android での Kotlin Flow
  • RoomのDAOはFlowを返すクエリメソッドを定義できます

    • Android開発ではRoomを使うシーンも多いのでFlowを覚えておくとRoomも使いやすくなるかもしれません
    @Query("SELECT * FROM EventDto WHERE isFavorite = 1")
    fun getFavoriteEvents(): Flow<List<EventDto>>
    
  • AndroidXとして提供されているライブラリのインターフェースで、ある値を受け取る方法がFlowというケースもあります

  • RepositoryでFlowを公開すると、どこか一ヶ所からFlowの中身を更新すれば、どこからでも最新
    の値を取得できるようになります

  • (使いこなせれば)かなりの行数の処理でもFlowでは数行でかけたりします

    • ⚠️ ただ、若干わかりにくい書き方になってしまうこともあります

Flowのイメージ

具体的なFlowの使い方を学習する前に、図解でFlowのイメージを掴んでいきましょう。
kotlin.coroutines.flowにはcollectというメソッドが生えていて、このメソッドを使ってflowを収集・実行します

fun soumen(): Flow<String> = flowOf("1", "2", "3")

fun main() {
    CoroutineScope(Dispatchers.IO).launch() {
    	soumen().collect { soumen ->
        	print("soumen:$soumen ")
    	}
    }
}

collectを実行するのにlaunchしているのは、collectがsuspend関数だからです。

サンプルコードを実行したい場合はこちらから

このサンプルコードを元にしたFlowのイメージが次の図です。
スクリーンショット 2023-02-05 163446.jpg
Flowは、何となく流しそうめんの感覚と似ています。

流しそうめんの竹の中をそうめん(データ)が流れていて、最後のお椀(collectのラムダの中)に流しそうめんが流れると、処理が実行されます。

collectする前は流しそうめんの竹の出口に蓋がされていて、そうめん(データ)が流れなくなっています。collectを開始すると蓋が取れて、竹筒の中をそうめん(データ)が流れ出します。これがFlowのイメージです。Flowという単語が「流れ」を意味するので、納得感があると思います。

蓋がとれることでお椀の中にそうめん(データ)が収集されて、お椀の中(collectのラムダの中)の処理が実行されます。サンプルコードでいうところのprint("soumen:$soumen ")がラムダの中の処理です。これがcollectのイメージです。

我々エンジニアがFlowを扱う姿、それはさながら流しそうめん職人です。

前述の例ではシンプルなコードを扱いましたが、もう少し複雑な処理をFlowに対して実行する場合、
分岐する竹を組んだり(hot flow)加熱してにゅうめんにしたり(map)そうめんを取り上げてうどんを流したり(flatMapLatest)します。

🚨hot flowの概念に関しては後ほど説明します🚨

よく使うFlowの書き方

ニュースアプリを例に一般的なFlowの使い方を見ていきましょう。

Flowからの収集

前述の章での説明にもあった通りFlowの収集はcollectを使いますが、様々な書き方があるので紹介していきます。
参考:Android での Kotlin Flow:Flow からの収集

launchを使う場合

class NewsRepository {
    val news: Flow<List<Article>> = ...
}

class NewsViewModel(
    private val newsRepository: NewsRepository,
) {
    init {
        viewModelScope.launch {
            newsRepository.news.collect { news ->
                Log.d("collected news: $news")
            }
        }
    }
}

単純にlaunchとcollectの挙動を確かめたい場合はこちらから

launchInを使う場合

launchInlaunchと同じ処理をより簡潔に書くことが可能です。
launchではscope.launch {flow.collect()}と書いていましたが、これを省略してlaunchInだけでかけます。つまりlaunchInにはlaunch{}collect{}の両方が含まれているということです。
参考:kotlinx.coroutines.launchIn

実際に上記のlaunchで書いたコードをlaunchInで書き換えてみましょう。

class NewsRepository {
    val news: Flow<List<Article>> = ....
}

class NewsViewModel(
    private val newsRepository: NewsRepository,
) {
    init {
        newsRepository.news.onEach { news ->
            Log.d("collected news: $news")
        }.launchIn(viewModelScope)
    }
}

launchの場合のサンプルコードと比べるとネストが一段浅くなっていますね。

また、launchの場合のサンプルコードと比較して、onEachという新しいオペレーターが登場しました。
これは、竹筒を流れている途中のそうめん(データ)を何もせずただ一つ一つ見てくれるようなイメージです。

なぜ今回これが必要になったのか簡単に説明します。
launchInlaunchcollectが含まれた結果、collect{}に付いていた流れてくる値を見るラムダがなくなってしまったので、代わりにonEachを使って流れてくる値をみましょうということです。

単純にlaunchInとonEachの挙動を確かめたい場合はこちらから

ActivityやFragmentで使用する際の注意

このようにFlowからの収集ではlaunchや、さらに便利なlaunchInを使用しますが、ActivityやFragmentで使用する際には注意してください。そのActivity/Fragmentが生きているまでの間ずっと収集してしまうので、無駄にリソースを使ってしまう可能性があります。例えば、Activityが裏に回ってる時などもデータを収集してしまいますが、一般的にそのような場合はデータを収集する必要はありません。このような場合はrepeatOnLifecycleを使って回避することができます。ViewModelScopeの時は、そのような配慮は必要ありません。

Flowの収集:よくある間違い

よくある間違いの中でも、あるViewModelの中で二種類のFlowを収集したい場合を取り上げて解説します。

NG例

まずはNG例のサンプルコードです。

class NewsRepository {
    val animeNews: Flow<List<Article>> = ....
    val techNews: Flow<List<Article>> = ....
}

class ViewModel(
    private val newsRepository: NewsRepository,
) {
    init {
        viewModelScope.launch {
            newsRepository.animeNews.collect { news ->
                Log.d("collected news: $news")
            }
       
            newsRepository.techNews.collect { news ->
                Log.d("collected news: $news")
            }
        }
    }
}

1つのlaunchの中で2つFlowの処理を書いています。
この場合だと、animeNewsのFlowの処理が終わらない限り(Flowをcancelしない限り)、techNewscollectは始まりません。

NG例の修正

この問題は、Flowごとにlaunchを分けることで解決できます。

class NewsRepository {
    val animeNews: Flow<List<Article>> = ....
    val techNews: Flow<List<Article>> = ....
}

class ViewModel(
    private val newsRepository: NewsRepository,
) {
    init {
        viewModelScope.launch {
            newsRepository.animeNews.collect { news ->
                Log.d("collected news: $news")
            }
        }

        viewModelScope.launch {
            newsRepository.techNews.collect { news ->
                Log.d("collected news: $news")
            }
        }
    }
}

Flowの作り方

FlowはFlowビルダーを使って作成できます。
先ほどからいくつかサンプルコードを見て、「Flowの作り方なんて今更かよ」と思うかもしれませんが、Flowの作り方は多岐に渡ります。
今までのサンプルコードではflowOf()というFlowビルダーを使ってFlowを作成していましたが、flowOf以外にもたくさんのFlowビルダーがありますので、それらのうちのいくつかを紹介していきます。

Flow builders
There are the following basic ways to create a flow:
- flowOf(...) functions to create a flow from a fixed set of values.
- asFlow() extension functions on various types to convert them into flows.
- flow { ... } builder function to construct arbitrary flows from sequential calls to emit function.
- channelFlow { ... } builder function to construct arbitrary flows from potentially concurrent calls to the send function.
- MutableStateFlow and MutableSharedFlow define the corresponding constructor functions to create a hot flow that can be directly updated.

参考:kotlinx-coroutines-core/kotlinx.coroutines.flow/Flow

flowOf

  • flowOf(1, 2, 3)のように、直接値を入れてFlowを作成することができる
    • こういう場合はサンプルコードや、自分でFlowの動きを学びたい時に使うことが多い
  • 値を返すsuspend funの結果をFlowとして受け取りたい場合にも使える
     // ログインをしている場合はそのユーザーのおすすめコンテンツを表示し、ログインしていない場合は固定の無料コンテンツを表示させるロジック
    val recommendation = userRepository.isLoggedIn.flatMapLatest { inLoggedIn ->
        if(isLoggedIn) {
            userRecommendationRepository.contents // これはFlow
        } else {
            flowOf(
                freeContentRepository.getFreeContents() // こっちはsuspend fun
            )
        }
    }
    
    • elseの方はsuspend funですが、flowOfによってFlow型に変換する事でflatMapLatestの返り値として使用できます

flow {}

  • flow {}の中でemit{}を呼ぶと値を流せる

    • suspend funも呼べるのでdelayと組み合わせて定期的に値を取得して流すのに便利
    // ExoPlayerによるダウンロード処理が行われているかどうかを提供する
    // exoDownloadManager.value.currentDownloadsの変化をコールバックなどで受け取る方法が無いので
    // 1secごとにexoDownloadManager.value.currentDownloadsの数を調べている
    val hasActiveDownload: Flow<Boolean> = flow {
        while (currentCoroutineContext().isActive) { // collectされている間だけ動く
            emit(exoDownloadManager.value.currentDownloads.isNotEmpty())
            delay(1000L)
        }
    }
    
    • このサンプルコードではBooleanですが数値にするなどすればプログレスバーの実装にも使えます

    flow{}とemitで作ったFlowの挙動を確かめたい場合はこちら

MutableStateFlow/MutableSharedFlow

sealed class LatestNewsUiState {
    data class Success(val news: List<ArticleHeadline>): LatestNewsUiState()
    data class Error(val exception: Throwable): LatestNewsUiState()
}

class LatestNewsViewModel(
    private val newsRepository: NewsRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
    val uiState: StateFlow<LatestNewsUiState> = _uiState

    init {
        viewModelScope.launch {
            newsRepository.favoriteLatestNews
                .collect { favoriteNews ->
                    _uiState.value = LatestNewsUiState.Success(favoriteNews)
                }
        }
    }
}

LiveDataとの違い

  • LiveDataはAndroid専用のものですが、MutableStateFlow/MutableSharedFlowは汎用的
    • なので、Androidのライフサイクルには気をつけなければならない
    • 逆にLiveDataはobserveする側のライフサイクルに応じて値の監視を止めてくれる
  • LiveDataはnullableだけどMutableStateFlowは必ず1つ値を保持している
  • 一方MutableSharedFlowは値を保持しない
    • ある値がMutableSharedFlowにemitされた時、collectしている場所があればそこに値が届く
    • どこもcollectしていない場合はemitしてもその値は虚無に消えてしまう
    • ただし、MutableSharedFlow(replay: Int = 10)などとすると値を10個まで保持できる

MutableStateFlow と MutableSharedFlow の違い

  • 初期値が必須かどうか
    • MutableStateFlowは必須!
  • MutableStateFlowはUIの状態を変更したり保持したりするのに向いている
    • 常に最新のデータを保持できるため
  • イベント通知や画面遷移などワンショット系のデータの取り扱いはMutableSharedFlowが向いている
    • ただし、例えばMutableSharedFlowを誰もcollectしていない時にイベント通知などをおこなうと、誰もこれを処理せずにイベント通知が無かったことになってしまう
    • そういう場合はMutableStateFlowを使ってイベント通知を実装し、通知後にnullで値を更新してイベントを消すのが良い
    data class LatestNewsUiState(
        val userMessage: String? = null
    )
    
    class LatestNewsViewModel(/* ... */) : ViewModel() {
        private val _uiState = MutableStateFlow(LatestNewsUiState(isLoading = true))
        val uiState: StateFlow<LatestNewsUiState> = _uiState
    
        ....
    
        // TIPS: イベント通知を行うUiStateの値をnullで更新
        fun userMessageShown() {
            _uiState.update { currentUiState ->
                currentUiState.copy(userMessage = null)
            }
        }
    }
    
    class LatestNewsActivity : AppCompatActivity() {
        private val viewModel: LatestNewsViewModel by viewModels()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            lifecycleScope.launch {
                viewModel.uiState.collect { uiState ->
                    // TIPS: ViewModelのメソッドを呼び出してイベントを消去
                    uiState.userMessage?.let {
                        viewModel.userMessageShown()
                    }
                    ....
                }
            }
        }
    }
    

hot flow と cold flow

Flowの勉強をしているとhot flowとcold flowの話はよく耳にするのではないでしょうか。
これらはFlowの特性を表す言葉です。前の章で説明した通りFlowビルダーを使えばFlowが作成できますが、そうやって作成されたFlowは全てhotかcoldのどちらかに分類されます。

cold flowとは

  • flowOf()flow { } ビルダーで作ったflowはcold flow
  • mapfilterなどのオペレーターがつくとcold flowになる
  • cold flowの特徴
    • 誰かがcollectするまでflowの処理は動かない
    • 複数の人がcollectすると、それぞれの人ごとにflowの処理が動く

hot flowとは

  • shareInstateInでhot flowに変換出来る
  • MutableStateFlow/MutableSharedFlowはhot flow
  • hot flowの特徴
    • collectがなくても勝手にflowの処理(オペレーターの中の処理)が動く
      • 内部実装を見るとcollectしているのがわかります
      • 内部的にcollectしているため、わざわざcollectしなくてもshareIn/stateInを呼び出した時点で自動的にflowの処理が動きます
    • 複数の人がcollectしても、flowの処理は一人分しか動かない
      • 例えば、Repositoryは複数箇所からのアクセスがあるのが一般的なので、cold flowでそれぞれのcollectごとに処理が走るのはリソース的に無駄なのでhot flowにする方が良い(Single Source of Truth的にも良い)

shareIn/stateInでhot flowに変換する場合の注意点

shareInやstateInでcold flowをhot flowに変換する場合は、started: SharingStartedで指定できます。

fun <T> Flow<T>.stateIn(
    scope: CoroutineScope, 
    started: SharingStarted, 
    initialValue: T
): StateFlow<T>

参考:StateIn

SharingStartedEagerly, Lazily, WhileSubscribedの3つの要素を持っています。(※SharingStartedの内部実装)

  • Eagerly
    • shareIn or stateIn が実行された瞬間から、アプリが死ぬまで止ま
      らない
  • Lazily
    • shareIn or stateIn で作成したFlowを誰かがcollectした時からア
      プリが死ぬまで止まらない
  • WhileSubscribed
    • shareIn or stateIn で作成したFlowを誰かがcollectしている間だけ実行す
    • だれもcollectしなくなったら、処理を止める
    • だれもcollectしなくなってから何msec経過したら...と指定することは可能

まとめ

ここまでFlowとは何かというイメージから実際の活用例、Flowの作り方、hot flow/cold flowと、Flowの基礎に関して全体的に学習していきましたが、いかがでしたでしょうか。筆者も日々の座学でFlowに関しての理解は深まったものの、実務で効果的に使えるかどうかと言われるとまだまだ未熟です。これから経験を積んでいき、より実践的にFlowを扱えるようになり、ゆくゆくは流しそうめん職人になりたいと思います。

74
48
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
74
48