この記事は
- Kotlin CoroutinesのFlowが何かよくわからないので基本を知りたい人向け
- この記事1つでFlowの基本を網羅できればという強い気持ちによって少々長くなってしまったので流し読みでも
- 私自身も勉強中で、この記事は百戦錬磨の実践から学んだものというよりかは、コツコツ座学的に勉強したことを自分用にまとめたものです
Android開発におけるFlowを使うメリット
-
Coroutinesの中には非同期処理を実行するのにワンショット系のデータを処理する場合とワンショットではない系(ストリーム系)のデータを処理する場合の大きく分けて二つあります
- ワンショット系の場合はCoroutine scopeの中で
suspend fun
を実行します - ストリームタイプのデータの場合はFlowを使うと良いです
- 例えば、最近Twitterでとても人気のあるツイートを見ている時、画面をリロード/pull-to-refreshをしなくても自動的にいいね数やリツイート数が更新され続けませんか?こういった連続的にデータが変化し、それをUIに反映させたい場合はFlowを用いるのが良さそうです
- 参考:Android での Kotlin Flow
- ワンショット系の場合はCoroutine scopeの中で
-
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のイメージが次の図です。
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を使う場合
launchIn
はlaunch
と同じ処理をより簡潔に書くことが可能です。
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
という新しいオペレーターが登場しました。
これは、竹筒を流れている途中のそうめん(データ)を何もせずただ一つ一つ見てくれるようなイメージです。
なぜ今回これが必要になったのか簡単に説明します。
launchIn
にlaunch
とcollect
が含まれた結果、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しない限り)、techNews
のcollect
は始まりません。
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
の返り値として使用できます
- elseの方はsuspend funですが、
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ですが数値にするなどすればプログレスバーの実装にも使えます
MutableStateFlow/MutableSharedFlow
- RepositoryやViewModelでFlowを提供するときに使われることが多い
-
LiveData
のように値を流すことが可能 - サンプルコードは公式ドキュメントを参照
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 -
map
やfilter
などのオペレーターがつくとcold flowになる - cold flowの特徴
- 誰かがcollectするまでflowの処理は動かない
- 複数の人がcollectすると、それぞれの人ごとにflowの処理が動く
hot flowとは
-
shareIn
やstateIn
で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
的にも良い)
- 例えば、Repositoryは複数箇所からのアクセスがあるのが一般的なので、cold flowでそれぞれのcollectごとに処理が走るのはリソース的に無駄なのでhot flowにする方が良い(
- collectがなくても勝手にflowの処理(オペレーターの中の処理)が動く
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
SharingStarted
はEagerly
, Lazily
, WhileSubscribed
の3つの要素を持っています。(※SharingStartedの内部実装)
- Eagerly
-
shareIn
orstateIn
が実行された瞬間から、アプリが死ぬまで止ま
らない
-
- Lazily
-
shareIn
orstateIn
で作成したFlowを誰かがcollectした時からア
プリが死ぬまで止まらない
-
- WhileSubscribed
-
shareIn
orstateIn
で作成したFlowを誰かがcollectしている間だけ実行す
る - だれもcollectしなくなったら、処理を止める
- だれもcollectしなくなってから何msec経過したら...と指定することは可能
-
まとめ
ここまでFlowとは何かというイメージから実際の活用例、Flowの作り方、hot flow/cold flowと、Flowの基礎に関して全体的に学習していきましたが、いかがでしたでしょうか。筆者も日々の座学でFlowに関しての理解は深まったものの、実務で効果的に使えるかどうかと言われるとまだまだ未熟です。これから経験を積んでいき、より実践的にFlowを扱えるようになり、ゆくゆくは流しそうめん職人になりたいと思います。