TRY ANDROID DEV

Android アプリ開発のコーディングネタ。あとエンジニアとしての活動

DroidKaigi 2019 official appを読んでみる④:Kotlin-coroutines-channel

背景

  • DroidKaigi 2019 のofficial appが公開されている:参照
  • せっかくだしコントリビュートしたいなと思ったけど思った以上に皆新しい技術使っててよくわからない。。
  • とりあえず一つずつ理解することにする。

Storeを読む

ついにMainActivityから脱出してStoreの処理を見ます。
DroidKaigi2019はFluxベースで開発されているため、以下のような処理順になるようです。

  1. Activity / FragmentからActionCreatorに対してアクションのメッセージを送る
  2. ActionCreatorはデータ等取ってきて、Dispatcherに渡す
  3. DispacherはStoreに通知する。
  4. StoreはActivity / Fragmentに通知する

https://github.com/DroidKaigi/conference-app-2019/blob/master/images/architecture.png?raw=true

conference-app-2019から引用しています。

こんな構成だとクラス間の依存関係がめちゃめちゃになりそうですが、どうやらkotlin-coroutines-channnelというものを利用してクラス間で情報を渡しているようです。今回はこのKotlin-croutines-channelについて調べてみます。

kotlin-coroutineのchannels

基本の使い方

まずは公式ドキュメント1を読みます。
channelはBlockingQueueによく似ているらしいです。違いは一時停止と再開があるところだそう。

    fun getValue() = runBlocking {
        var value = ""


        //sampleStart
        val channel = Channel<Int>()

        // 5回sendするが、非同期処理のため処理を待たないで次に行く
        launch {
            for (x in 1..5) channel.send(x * x)
        }
        // receive()は一つずつsendされた情報を受け取る
        // なお、ここで中断されるため、sendされない限り次の処理に進まない。非同期でないところに注意。
        repeat(5) {
            value += channel.receive()
            value += System.lineSeparator()
        }
        println("Done!")
    }

つまりsendした情報をreceiveで受け取るという流れみたいですね。 これだけだと何に使うんだって感じです。

パイプライン

もう一つ、パイプラインという使い方があるようです。
これはchannelとchannelを繋ぐという意味ですね。
例えば以下の二つのchannelを考えます。

channel 1 : produceNumbers()

// produceはchannelを渡すという意味。
// このchannelは数値を垂れ流す。
fun CoroutineScope.produceNumbers() = produce<Int> {
    var x = 1
    while (true) send(x++) // infinite stream of integers starting from 1
}

channel 2 : square()

// このchannelはパラメータのchannelからIntを受け取り、二乗するchannel
fun CoroutineScope.square(numbers: ReceiveChannel<Int>): ReceiveChannel<Int> = produce {
    for (x in numbers) send(x * x)
}
fun main() = runBlocking {
    val numbers = produceNumbers() // Intを垂れ流すChannelを取得
    val squares = square(numbers) // 二乗するChannel
    for (i in 1..5) println(squares.receive()) // 5回だけ受け取る
    println("Done!") 
    coroutineContext.cancelChildren() // このCoroutine以下すべてのCoroutineをストップ
}

コメントに記載している通り、チャンネルから発行されてくる値を受け取って、別のチャンネルでまた発行していくということが可能です。これをパイプラインと呼ぶみたいです。

DroidKaigiではどのように使っているのか

Flubベースの話に戻ると、Channelを利用すれば以下のようなことが可能っぽいです。

  1. ActionCreatorとStoreは共通のDispatcherインスタンスを持つ
  2. ActionCreatorが通信などの処理を終えて、Dispatcherにchannelを通じてデータをsendする
  3. StoreはDispatcherを通じてChannelからデータが来ないか購読している。データが来たら処理して画面表示。

最初Dispatcherいらなくね?と思ったのですが、ないとStoreは対応するActionCreatorをすべて知ってないとダメですね。

Channelのサンプルプログラム

5つのクラスを利用しました。

Action

/**
 * Actionクラス
 * - sealed : enum拡張みたいなもの。
 * sealedクラス単体ではインスタンス化できない。
 * またメンバーにはサブクラスを持つことができるので、データクラスも持てる。
 * 今回は「どんな処理かを示しつつデータも渡したい」のでsealed classを使う。
 */
sealed class Action {
    data class TestActionFinished(val str : String): Action()
}

ActionCreator

// まだChannelって実験段階なので、アノテーションで実験段階APIって書いてあげる必要があるみたい。
@ExperimentalCoroutinesApi
class ActionCreator(val dispatcher: Dispatcher) {
    fun load() {
        dispatcher.launchAndDispatch(Action.TestActionFinished("Test"))
    }
}

Dispatcher

@ExperimentalCoroutinesApi
class Dispatcher {

    // ActionCreatorからデータを送るChannel
    private val actions = BroadcastChannel<Action>(Channel.CONFLATED)

    // StoreがこのChannelを購読する
    val receiveChannel : ReceiveChannel<Action> = actions.openSubscription()

    fun launchAndDispatch(action: Action) {
        GlobalScope.launch {
            actions.send(action)
        }
    }

store

@ExperimentalCoroutinesApi
class Store(val dispatcher: Dispatcher) {
    val sendData = dispatcher.receiveChannel
}

MainActivity

@ExperimentalCoroutinesApi
class MainActivity : AppCompatActivity() {

    // dispatcherはとりあえずActivityに置いているけど、
    // 別のところから供給する方が依存関係的にいいと思います。
    private val dispatcher: Dispatcher = Dispatcher()

    private val actionCreator: ActionCreator by lazy {
        ActionCreator(dispatcher)
    }

    private val store: Store by lazy {
        Store(dispatcher)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // load 実施
        actionCreator.load()

        // ActionCreatorのload()の結果がDispatcherを通じてstoreに返却されているので、storeでレシーブしてtext表示する
        // LiveDataを利用するとよりよい感じでデータ反映できそう。
        GlobalScope.launch {
            val action = store.sendData.receive()
            when (action) {
                is Action.TestActionFinished -> findViewById<TextView>(R.id.textView).text = action.str
            }
        }
    }

}

実行結果

testが表示された。

f:id:off2white:20190117155616p:plain

感想

  • ひとまずChannelを利用したFluxの動作は理解できてきた。
  • ただ、もうちょっと理解するスピードをあげたいなぁと思う。理解するより技術が進むスピードの方が早いとかこわい。
  • DroidKaigi 2019 アプリだと拡張関数を使ってLiveDataに変換してるみたいだけど仕組みがわかんない...
  • あとKotlin - coroutine使う上でrunBlockingなりlauchなり使う場所ってどこが適切なんだろう。

今回作成したサンプルコード

github.com

参照