Upgrade to Pro — share decks privately, control downloads, hide ads and more …

KotlinCoroutinesFlowことはじめ

kr9ly
November 27, 2019

 KotlinCoroutinesFlowことはじめ

kr9ly

November 27, 2019
Tweet

More Decks by kr9ly

Other Decks in Programming

Transcript

  1. Kotlin Coroutines Flowことはじめ
    2019-11-27 dely株式会社 梅森翔(@kr9ly)

    View Slide

  2. delyはこういうの作ってる会社です

    View Slide

  3. 詳しく知りたければこのスライド見てください
    https://speakerdeck.com/tsubotax/dely

    View Slide

  4. 軽く自己紹介
    • delyのAndroidアプリチームのリーダーやってます
    • 兼Engineering Manager
    • DroidKaigi2019に登壇したりしてます
    • 今年のは落ちました(残念)
    • Twitter: @kr9ly

    View Slide

  5. アジェンダ
    • Kotlin Coroutines Flowとは
    • Flowの使い方
    • Cold Streamとは
    • Flowが導入されると何がうれしいのか?
    • RxJavaからのマイグレーション
    • まとめ

    View Slide

  6. アジェンダ
    • Kotlin Coroutines Flowとは
    • Flowの使い方
    • Cold Streamとは
    • Flowが導入されると何がうれしいのか?
    • RxJavaからのマイグレーション
    • まとめ

    View Slide

  7. Kotlin Coroutines Flowとは
    Kotlin 1.2から導入された、
    Kotlin Coroutinesで Cold Stream を取り扱うためのAPI
    まだExperimentalなAPIも多いが、実用的なレベルに達している(と思う)

    View Slide

  8. 徐々にAPIはstableになっています
    https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md#version-132

    View Slide

  9. しかし1.3.2でもまだまだExperimental
    @ExperimentalCoroutinesApi ついてるAPIも結構あります
    (というか割とまだついてる)
    使う場合は使うメソッド(かクラス)に
    @ExperimentalCoroutinesApi つける
    この発表は1.3.2時点でのFlow APIに準拠した説明をします

    View Slide

  10. ライブラリを作るとかでなければ心配無さそう
    https://github.com/Kotlin/kotlinx.coroutines/blob/1.3.2/docs/compat
    ibility.md#experimental-api

    View Slide

  11. ちなみに
    FlowはだいたいRxJavaです
    (親切にもMigration用の仕組みもあるので後でご紹介します)

    View Slide

  12. ところでCold Streamとは?
    一旦置いといてまずは使い方から見てみましょう

    View Slide

  13. アジェンダ
    • Kotlin Coroutines Flowとは
    • Flowの使い方
    • Cold Streamとは
    • Flowが導入されると何がうれしいのか?
    • RxJavaからのマイグレーション
    • まとめ

    View Slide

  14. Flowの使い方
    特にライブラリを追加で入れる必要は無いです
    dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2'

    View Slide

  15. Hello World!
    runBlocking {
    val f: Flow = flow {
    emit("Hello")
    delay(500)
    emit("Coroutines Flow World!")
    }
    f.collect { value ->
    println(value)
    }
    }
    Hello
    Coroutines Flow World!
    非同期処理をflowでラップ
    flow内で呼ぶ必要のある
    メソッドはemitのみ
    使う側はcollectを呼ぶ

    View Slide

  16. suspend functionをflowに変換
    runBlocking {
    val suspendFunc = suspend {
    delay(300)
    "this is suspend func result"
    }
    val f: Flow = flow {
    emit(suspendFunc())
    }
    f.collect { value ->
    println(value)
    }
    }
    public fun flow(@BuilderInference block: suspend FlowCollector.() -> Unit): Flow = SafeFlow(block)
    flowに渡すfunctionはsuspend functionなので、そのままflow内で
    suspend functionを呼べばflowに変換できる

    View Slide

  17. どのDispatcher上で呼び出すかを変えられる
    runBlocking {
    val f = flow {
    emit(Thread.currentThread().name)
    }
    val f2 = flow {
    emit(Thread.currentThread().name)
    }.flowOn(Dispatchers.IO)
    f.collect { value ->
    println(value)
    }
    f2.collect { value ->
    println(value)
    }
    }
    (ちなみにflowOnはまだexperimentalみたいですが)
    main @coroutine#1
    DefaultDispatcher-worker-2 @coroutine#2

    View Slide

  18. Exceptionのハンドリングもシンプル
    runBlocking {
    val f: Flow = flow {
    emit("Hello")
    delay(500)
    throw RecoverableException()
    // throw UnRecoverableException()
    }.catch { t ->
    if (t is RecoverableException) {
    emit("Recovered")
    } else if (t is UnRecoverableException) {
    throw t
    }
    }
    f.collect { value ->
    println(value)
    }
    }
    Hello
    Recovered
    runBlocking {
    val f: Flow = flow {
    emit("Hello")
    delay(500)
    // throw RecoverableException()
    throw UnRecoverableException()
    }.catch { t ->
    if (t is RecoverableException) {
    emit("Recovered")
    } else if (t is UnRecoverableException) {
    throw t
    }
    }
    f.collect { value ->
    println(value)
    }
    }
    Hello
    jp.dely.flowsample.UnRecoverableException

    View Slide

  19. FlowのAPIはほぼ拡張関数で定義されている
    例えばcollectの定義
    public suspend inline fun Flow.collect(crossinline action: suspend (value: T) -> Unit): Unit =
    collect(object : FlowCollector {
    override suspend fun emit(value: T) = action(value)
    })

    View Slide

  20. アジェンダ
    • Kotlin Coroutines Flowとは
    • Flowの使い方
    • Cold Streamとは
    • Flowが導入されると何がうれしいのか?
    • RxJavaからのマイグレーション
    • まとめ

    View Slide

  21. さて、Cold Streamとは?
    Hot Stream: 常にアクティブ、
    Streamの下流にObserverがいな
    くても仕事する
    Cold Stream: Observerがいな
    いと仕事しない、Observerごとに
    Streamを作る

    View Slide

  22. Hot Streamはもうある(interfaceは違うが)
    runBlocking {
    val channel = Channel()
    launch {
    channel.send("first")
    delay(100)
    channel.send("second")
    delay(100)
    channel.send("third")
    delay(100)
    channel.send("fourth")
    delay(100)
    channel.send("fifth")
    channel.close()
    }
    for (value in channel) {
    println(value)
    }
    }
    > kotlinx.coroutines.channels.Channel <

    View Slide

  23. Channelじゃいかんのか?
    大抵のStreamは Cold Stream
    (=Observerが現れるまで仕事をする必要が無い)
    • 例えば非同期API呼び出し
    • 例えばUIイベント(イベントバス(=Hot Stream)で扱うこともできるが必ずしもする必要もない、
    例: RxBinding)
    • 例えばDBの更新通知
    • あとChannel自体にはStreamとして扱う際の便利機能がほぼない…

    View Slide

  24. Flows are cold
    Flows are cold streams similar to sequences — the code
    inside a flow builder does not run until the flow is
    collected.
    https://kotlinlang.org/docs/reference/coroutines/flow.html#flows-are-cold
    RxJavaのユースケースもほとんどCold Stream

    View Slide

  25. 同じStreamを同時に呼び出す
    runBlocking {
    val f = flow {
    emit("first")
    delay(1000)
    emit("second")
    delay(1000)
    emit("third")
    }
    launch {
    f.collect { value ->
    println("a: $value")
    }
    }
    launch {
    delay(1000)
    f.collect { value ->
    println("b: $value")
    }
    }
    }
    a: first
    a: second
    b: first
    a: third
    b: second
    b: third
    a, b共に互いに影響しない
    (別のStreamが作られている)

    View Slide

  26. collect側で中断するとflow内でも中断される
    runBlocking {
    val f = flow {
    emit("first")
    println("first item emitted")
    delay(1000)
    emit("second")
    println("second item emitted")
    }
    try {
    withTimeout(500) {
    f.collect { value ->
    println(value)
    }
    }
    } catch (e: TimeoutCancellationException) {
    println("aborted")
    }
    }
    first
    first item emitted
    aborted
    Observerがいなくなれば処理をやめる

    View Slide

  27. アジェンダ
    • Kotlin Coroutines Flowとは
    • Flowの使い方
    • Cold Streamとは
    • Flowが導入されると何がうれしいのか?
    • RxJavaからのマイグレーション
    • まとめ

    View Slide

  28. Flowが導入されると何がうれしいのか?
    • 非同期処理をコレクション操作のように扱うことが出来る
    • Stream同士を合成できる
    • Streamを再利用しやすい
    • Streamに対する手続きを再利用しやすい

    View Slide

  29. 非同期処理をコレクション操作のように扱う
    runBlocking {
    val f = flow {
    emit("a")
    delay(100)
    emit("b")
    delay(100)
    emit("c")
    }
    f.filterNot { value ->
    value == "b"
    }.map { value ->
    "transform: $value"
    }.collect { value ->
    println(value)
    }
    }
    transform: a
    transform: c
    コレクション操作用のAPIのメリットをほぼ享受できる
    • メソッド名から操作の目的が明確になる
    • 大体イメージ通りの動きをするはず

    View Slide

  30. Stream同士を合成できる(flatMap)
    例えばAPIの呼び出し結果をもとに
    別のAPIを呼ぶ際に使う
    • flatMapConcat (これがRxJava
    でいうflatMap)
    • flatMapMerge
    もあるがまだPreview
    (flatten系APIをどうするかをまだ
    ちゃんと決めきれて無さそう)
    runBlocking {
    val apiACall = flow {
    delay(1000)
    emit("WebAPIAの呼び出し結果")
    }
    val apiBCall: (String) -> Flow = { result ->
    flow {
    delay(1000)
    emit("$result + WebAPIBの呼び出し結果")
    }
    }
    apiACall.flatMapLatest { result ->
    apiBCall(result)
    }.collect { result ->
    println(result)
    }
    }
    WebAPIAの呼び出し結果 + WebAPIBの呼び出し結果

    View Slide

  31. Stream同士を合成できる(zip)
    runBlocking {
    val uiEventCall = flow {
    delay(1000)
    emit("UIイベント1")
    emit("UIイベント2")
    }
    val apiCall = flow {
    delay(1000)
    emit("WebAPIの呼び出し結果1")
    emit("WebAPIの呼び出し結果2")
    }
    uiEventCall.zip(apiCall) { aResult, bResult ->
    "$aResult + $bResult"
    }.collect { result ->
    println(result)
    }
    }
    UIイベント1 + WebAPIの呼び出し結果1
    UIイベント2 + WebAPIの呼び出し結果2
    例えばUI操作の待ち合わせなど

    View Slide

  32. Stream同士を合成できる(combineLatest)
    runBlocking {
    val uiEventCall = flow {
    emit("UIイベント1")
    delay(500)
    emit("UIイベント2")
    delay(500)
    emit("UIイベント3")
    delay(500)
    emit("UIイベント4")
    }
    val apiCall = flow {
    emit("WebAPIの呼び出し結果1")
    delay(1000)
    emit("WebAPIの呼び出し結果2")
    }
    apiCall.combine(uiEventCall) { uiResult, apiResult ->
    "$uiResult, $apiResult"
    }.collect { result ->
    println(result)
    }
    }
    WebAPIの呼び出し結果1, UIイベント1
    WebAPIの呼び出し結果1, UIイベント2
    WebAPIの呼び出し結果2, UIイベント2
    WebAPIの呼び出し結果2, UIイベント3
    WebAPIの呼び出し結果2, UIイベント4
    例えば常に更新されるタイプのデータ
    をUIイベントの度に更新しながら使う、
    とか

    View Slide

  33. Streamを再利用しやすい
    class StreamsApi {
    fun returnAllValues(): Flow = flow {
    emit("apple")
    emit("banana")
    emit("lemon")
    emit("orange")
    }
    fun returnOnlyApple(): Flow = returnAllValues().filter { value ->
    value == "apple"
    }
    }
    例えばちょっとfilterを追加した
    別のAPIを作成したい、などあれば
    既存のものを再利用して使ったり

    View Slide

  34. Streamに対する手続きを再利用しやすい
    • fun retryConverter(): (Flow) -> Flow = { upstream ->
    upstream.retryWhen { cause, attempt ->
    println("cause: ${cause.javaClass.canonicalName}, attempt:
    $attempt")
    cause is NetworkException && attempt < 2
    }
    }
    runBlocking {
    val f = flow {
    throw NetworkException()
    }
    val f2 = flow {
    throw InternalException()
    }
    try {
    f.let(retryConverter()).collect { value ->
    println(value)
    }
    } catch (e: NetworkException) {
    println("retried over 3 times.")
    }
    try {
    f2.let(retryConverter()).collect { value ->
    println(value)
    }
    } catch (e: InternalException) {
    println("internal exception occurred.")
    }
    }
    cause: jp.dely.flowsample.NetworkException, attempt: 0
    cause: jp.dely.flowsample.NetworkException, attempt: 1
    cause: jp.dely.flowsample.NetworkException, attempt: 2
    retried over 3 times.
    cause: jp.dely.flowsample.InternalException, attempt: 0
    internal exception occurred.
    Flow.letでFlowに対する任意の変換処理を適用
    出来る(RxJavaのcomposeにあたる)

    View Slide

  35. アジェンダ
    • Kotlin Coroutines Flowとは
    • Flowの使い方
    • Cold Streamとは
    • Flowが導入されると何がうれしいのか?
    • RxJavaからのマイグレーション
    • まとめ

    View Slide

  36. RxJavaからのマイグレーション
    • 基本的なOperatorは(まだexperimentalだけど)あります
    • 無いものはIDE上で確認しながらマイグレーションできる仕組みがあ
    ります
    先程のcompose -> letの例がこちら

    View Slide

  37. RxJavaからのマイグレーション
    • とりあえずRxJavaのOperatorを書くと、JavaDocに書き換え例がのっ
    てます
    • RxJava詳しくなくてもとりあえずRxJavaのOperatorを書いてみて
    JavaDoc読むと理解が早いかも(RxJavaのドキュメントは世の中に豊
    富にあります!)
    ここに定義がすべてのってるので一旦これを読んでみるのもあり
    https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/flow/Migration.kt

    View Slide

  38. アジェンダ
    • Kotlin Coroutines Flowとは
    • Flowの使い方
    • Cold Streamとは
    • Flowが導入されると何がうれしいのか?
    • RxJavaからのマイグレーション
    • まとめ

    View Slide

  39. まとめ
    • Kotlin Coroutines FlowはCoroutinesの仕組みの中でCold
    Streamを取り扱うためのAPIです
    • 1.3.2時点では色々なAPIがまだexperimentalですが、ライブラリを
    作る、とかでなければ使ってしまっても大丈夫そうです
    • Flowを導入すると色々とうれしいことがあります
    • RxJavaからのマイグレーション用の仕組みがあります(RxJava知らな
    くても学習用に便利!)

    View Slide