LoginSignup
42
29

More than 3 years have passed since last update.

DroidKaigi 2020 アプリでの学び【Kotlin Coroutines Flow 編】

Last updated at Posted at 2020-03-07

この記事なに?

DroidKaigi 2020 アプリに commit したので、そこで得た学びや感想を書いた記事です。
いろいろ書きたいことがあったので、分割して書きます。
僕のモチベが続く限り続きます。
ちなみに初めての OSS 活動でした 😎

僕が取り組んだ Pull Request はこの2つになります。

Kotlin Coroutines Flow

DroidKaigi 2020 アプリの大きな特徴の1つとして、 Kotlin Coroutines Flow (以下 Flow) の採用があります。
Flow は RxJava を始めとする Reactive Stream の概念にインスパイアされており、Kotlin の標準機能だけでリアクティブプログラミングができるようになります。
Asynchronous Flow - Kotlin Programming Language

僕は業務で RxJava を利用しているため、Flow には以前から興味がありました。
今回取り組んだ PR の中で Flow をがっつり触ったので、RxJava と比較した感想を書いていきます。

Flow ってなに?

Kotlin は非同期処理の方法としてコルーチンという機能を提供しています。
コルーチンは suspend キーワードがついた中断可能な関数を定義し、処理の中断や待ち合わせを書きやすくする仕組みです。
suspend 関数は中断可能であること以外、通常の関数と同じなので、一度の関数呼び出しで一度しか値を得られません。
よって、複数の値を非同期で取得するためにFlow が誕生しました。

Flow は RxJava と同じで publish / subscribe パターンで設計されています。
Flow という Cold なストリーム (RxJava でいう Observable) に非同期で値を流せます。
また、値を使う側は collect を使って値を購読してよしなに使います。
AndroidX の LiveData とも似てますね。

DroidKaigi 2020 アプリでは、ViewModel - Repository 間や Repository - DB 間の接続で Flow が用いられています。
データ層からデータを購読し、Flow が提供するオペレータでデータを加工して ViewModel へ伝えます。
conference-app-2020/arch1.png at master · DroidKaigi/conference-app-2020

具体的な例だと、いいね機能では Firestore に格納されている各セッションのいいね数を監視する部分に使用しています。
conference-app-2020/FirestoreImpl.kt · DroidKaigi/conference-app-2020

Flow の良いところ: 購読の管理が RxJava より楽

RxJava の dispose 忘れ問題

業務で RxJava を使っていて大変だなと思うところは、Disposable の管理です。
Android のライフサイクルに合わせた dispose 処理をしないと、必要なときにストリームから値が流れてこなかったり、不要になったストリームが動き続けたりします。
そして、dispose の有無はコンパイラではチェックされないので、めちゃ忘れやすいです。

CompositeDisposable という複数の dispose 処理をまとめて呼ぶ仕組みもありますが、結局一度はCompositeDisposable.dispose() をどこかで呼ばないとダメなので同じです。

fun retrieveDataFromDB(): Observable<Data> {
    // DB を監視してデータを取ってくる処理
}

// disposable.dispose() を呼ばないと、ずっと subscribe が動き続ける
val disposable = retrieveDataFromDB()
    subscribe { data ->
        println(data)
    }

Flow の Structured concurrency によるストリームの購読管理

なんのこっちゃって感じかもしれませんが、要は CoroutineScope でストリームの購読が動作する範囲を制御できる 点が良いなと思いました。

コルーチンの一部である Flow は、Structured concurrency という概念のもとに設計されています。
Structured concurrency とは雑に説明すると、並行に実行されるコードブロック (コルーチン) を管理する仕組みのことです。
そして、コルーチンの動作する範囲が CoroutineScope です。

CoroutineScope.cancel() を呼ぶと Scope 内のコルーチンをすべてキャンセルできます。
つまり、CoroutineScope.cancel()CompositeDispossable.dispose() と同じような役割を果たします。

じゃあ RxJava と一緒じゃんと思いきや、Android Architecture Components には ViewModelScopeLifecycleScope など便利な Scope があります。
これらの Scope を使うと、ViewModel や Activity/Fragment のライフサイクルに合わせてコルーチンをいい感じにキャンセルしてくれます。

すべてのコルーチンは CoroutineScope の中で動作させる必要があるため、Flow の購読を始める際は必ず Scope を意識します。
さらに、Android Architecture Components の提供する Scope を使えばライフサイクルを意識しなくて良くなり、購読の管理が楽になります。

と、Dropbox のエンジニアも言ってました😊
Store grand re-opening: loading Android data with coroutines | Dropbox Tech Blog

Flow の良いところ: RxJava 並にオペレータが潤沢

RxJava の強みは豊富なオペレータだと感じています。
オペレータを組み合わせて、様々なストリームの加工ができます。
便利ですね。
(ただ、オペレータが豊富すぎてなかなか使いこなせず、非同期処理のためだけに RxJava を使うのはオーバースペックだの、学習コストが高いだのと言われがちなんですがね。。。)
ReactiveX - Operators

Flow もオペレータはめちゃあります。
map, flatMap (Flow では flatMapLatest) , filter など基本的なものから、debounce, distinctUntilChanged など UI を作るのに便利なオペレータまであります。
Flow - kotlinx-coroutines-core

LiveData はオペレータが少なすぎて、 RxJava と同じことをしようとするとオペレータを自作する必要がありました。
Flow は RxJava でやっていたストリーム変換が大体同じようにできました。
さらに、transform オペレータを使えば複雑なストリーム変換処理の自作も柔軟にできそうです。
(僕は DroidKaigi アプリでは使ってませんが)

fun Flow<Int>.skipOddAndDuplicateEven(): Flow<Int> = transform { value ->
    if (value % 2 == 0) { // Emit only even values, but twice
        emit(value)
        emit(value)
    } // Do nothing if odd
}

transform - kotlinx-coroutines-core

余談ですが、Flow と RxJava で決定的に違うところは、 null をストリームに流せるというところです。
RxJava では null を流すとエラーが起きます。
どうやら、Reactive Stream で null は流してはいけないと決まっているらしいです。
Flow には、filterNotNull など nullable を unwrap するようなオペレータもあるので、null 安全な Kotlin らしく書けて大変便利です。

Flow の良いところ: Kotlin Multiplatform Project への可能性

Kotlin Multiplatform Project (Kotlin MPP) は Kotlin だけで iOS や Windows などのネイティブコード、Javascript まで書いちゃおう!という技術です。
DroidKaigi も Kotlin MPP を採用しており、iOS アプリで Kotlin のモジュールを使っています。

しかし、Kotlin で書かれたコードを他のプラットフォームで使おうとすると、JVM の資産は使えません。
つまり、Java で書かれた RxJava を使っていては Kotlin MPP 化できません。

残念ながら、コルーチンや Flow もまだ Kotlin MPP には完全に対応しておらず、iOS の Framework や他のネイティブコードへはコンパイルできません。
そのため、DroidKaigi では Swift との接点で Flow は使っていませんでした (たぶん)。
callback 形式のインターフェースで API のレスポンスを返却し、RxSwift で Observable(Single) 化する方法が取られていました。

SessionDataProvider.swift
final class SessionDataProvider {
    func fetchSessions() -> Single<[Session]> {
        return Single.create { observer -> Disposable in
            ApiComponentKt.generateDroidKaigiApi().getSessions(callback: { response in
                let model = ResponseToModelMapperKt.toModel(__: response)
                observer(.success(model.sessions))
            }, onError: { error in
                observer(.error(KotlinError(localizedDescription: error.description())))
            })
            return Disposables.create()
        }
    }

conference-app-2020/SessionDataProvider.swift · DroidKaigi/conference-app-2020

また、JetBrains が作成した Kotlin Conf のアプリでは、CFlow という iOS 用に Flow をラップしたクラスが利用されています。
CFlowは callback 形式よりもう少し進んでおり、Swift でも擬似的に Flow の値を購読できます。

FlowUtils.kt
fun <T> Flow<T>.wrap(): CFlow<T> = CFlow(this)

class CFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
    fun watch(block: (T) -> Unit): Closeable {
        val job = Job(/*ConferenceService.coroutineContext[Job]*/)

        onEach {
            block(it)
        }.launchIn(CoroutineScope(dispatcher() + job))

        return object : Closeable {
            override fun close() {
                job.cancel()
            }
        }
    }
}

kotlinconf-app/FlowUtils.kt at master · JetBrains/kotlinconf-app

使い方↓

HomeController.swift
Conference.votes.watch { votes in
    let count = Float(votes?.count ?? 0)
    let progress = min(1.0, count / Float(Conference.votesCountRequired()))

    let hidden = progress != 1.0
    ...
}

kotlinconf-app/HomeController.swift at master · JetBrains/kotlinconf-app

Flow はまだ完全に Kotlin MPP で利用できるとは言えませんが、RxJava の代用にはなりそうです。
何より、サードパーティライブラリではなく 言語の標準機能でリアクティブプログラミングが実現できるという安心感があります。

Flow の悩み?どころ: CoroutineScope はどこから貰う良いのか

Flow を動作させるのに CoroutineScope が必要なことはメリットだと言ったばかりですが、めんどくさいときもありました。

Activity/Fragment や ViewModel などプレゼンテーション層に近い部分では、ViewModelScopeLifecycleScope が簡単に取得できます。
ですが、Repository より下のデータ層でこれらの Scope を使うベストプラクティスは何なのかよくわかりませんでした。

単純に必要なクラスに DI するのが良いんでしょうか?
それとも CoroutineScope 新たに定義するのがいいんでしょうか?
このあたりはまだコルーチンのわかっていない部分だなあ。

まとめ

RxJava と比較しながら Flow について思ったことをつらつら書きました。
Flow はまだ Experimental な API も多く、まだまだ開発中な機能です。
ですが、想像していたよりずっと RxJava と似ていたので Rx を知っていればそんなに難しくは無いと思います。

まだ分かっていないこととしては、Observer に相当する FlowCollector や、Subject 相当の Channel の使い方です。
単純に Channel = Flow (Observable) + FlowCollector (Observer) ではない気がしているので、この辺を探求したいですね💪

42
29
4

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
42
29