【Kotlin】ぼくらは Flow の マーブルダイアグラム を見るのか。

ドキュメントや関連記事を見てもよく意味が分からないような複雑なオペレーターの機能が、一見するだけでで分かることがよくありました。

Reactivex-FlatMap
👉 ReactiveX - FlatMap operator 

Kotlin Flow のオペレーターたちも、これらの名前や機能を元に作られているので、上記 ReactiveX サイトのマーブル図を見れば大体は分かりますが。

Kotlin Flow 向けにも公開してる方がいます。

マーブルをドラッグもできます。

FlowMarbles
👉 FlowMarbles 

ソースコードも確認できるようです。

👉 rougsig/flowmarbles: Interactive diagrams of Kotlin Flow https://flowmarbles.com 


複数の Flow を受け取る How to collect multiple flows

パラレルで同じライフサイクルの場合だけども。

flow-lifecycle



launch をネストしていますね!


View at Medium.com

👉 A safer way to collect flows from Android UIs | by Manuel Vivo | Android Developers | Medium 


今どきの Retrofit と LiveData で Coroutine

ありがとうございます。


👉 Retrofit 

ご存知の通り Retrofit2 では、サスペンドな関数も利用できるようになっております。

👉 SpaceX REST API で試す Retrofit の coroutine 対応 


// NewModel.kt
@GET("/feed/here/")
suspend fun getData(@Query("token") token : String) : Status


// NewRepository.kt
class Repository {
  var client = RetrofitService.createService(JsonApi::class.java)
  suspend fun getData(token : String) = client.getData(token)
}

ので、以下のようなこれまでのコードは、


// OldViewModel.kt
val data = MutableLiveData<Status>()

private fun loadData(token: String){
  viewModelScope.launch {
    val retrievedData = withContext(Dispatchers.IO) {
      repository.getData(token)
    }
    data.value = retrievedData
  }
}

シンプルに以下のように書けます。


// NewViewModel.kt
val data : LiveData<Status> = liveData(Dispatchers.IO) {
      val retrievedData = repository.getData(token)
      emit(retrievedData)
    }

ありがとうございます。

👉 Exploring new Coroutines and Lifecycle Architectural Components integration on Android 
👉 Using Retrofit 2 with Kotlin coroutines - ProAndroidDev 


coroutine の使い方 - Android Architecture Blueprints v2 #1

気がついたら「v2」です。MVVM のみとなって、 RxJava は姿を消していますが。

👉 googlesamples/android-architecture: A collection of samples to discuss and showcase different architectural tools and patterns for Android apps. 



現在、4つのバリエーションが公開されていますが、以下は共通。


- kotlin
- coroutine
- single activity
- architecture component
- navigation component + fragment
- presentetion layer(per page) = fragment + view model
- reactive ui = live data + data binding
- data layer = repositpory + local(room) + remote
- datalayer one shot operations(no listener or data streams)

非同期処理には、どのバリエーションも「coroutine」使う。

どのように coroutine を使っているか、を定型化しておきたい。

関数 coroutineScope

「kotlinx-coroutines-core」の関数の「coroutineScope」 が目についたので見ておく。


suspend fun <R> coroutineScope(
    block: suspend CoroutineScope.() → R
): R (source)

CoroutineScope を作成し、このスコープで指定された suspend ブロックを呼び出します。その外側のスコープから coroutineContext を継承しますが、そのコンテキストのJobをオーバーライドします。

この関数は、処理の並列分解用に設計されています。 このスコープ内のいずれかの子コルーチンが失敗すると、このスコープは失敗し、残りのすべての子はキャンセルされます(supervisorScope との違いを参照してください)。 与えられたブロックとそのすべての子コルーチンが完了するとすぐに戻ります。
スコープの使用例は次のようになります。


suspend fun showSomeData() = coroutineScope {

  val data = async(Dispatchers.IO) { // <- extension on current scope
     // ... load some UI data for the Main thread ...
  }

  withContext(Dispatchers.Main) {
    doSomeWork()
    val result = data.await()
    display(result)
  }
}

この例のスコープの意味は次のとおりです。

1. showSomeDataは、データがロードされてUIに表示されるとすぐに戻ります。
2. doSomeWorkが例外をスローすると、非同期タスクはキャンセルされ、showSomeDataはその例外を再スローします。
3. showSomeData の外部スコープが取り消されると、開始された async ブロックとwithContext ブロックの両方が取り消されます。
4. 非同期ブロックが失敗すると、withContext はキャンセルされます。

現在のジョブが外部でキャンセルされた場合、メソッドは CancellationException をスローします。このスコープ内に未処理の例外がある場合(たとえば、このスコープ内の起動で開始されたクラッシュコルーチンから)は、対応する未処理のThrowableをスローします。

coroutineScope - kotlinx-coroutines-core

実際の記述

coroutine を使った非同期処理は 各 ViewModel を起点に記述されています。


class TasksViewModel(
    private val tasksRepository: TasksRepository
) : ViewModel() {

    fun loadTasks(forceUpdate: Boolean) {
        viewModelScope.launch {

            // 対応する Repository メソッドをコール
            // val tasksResult = tasksRepository.getTasks(forceUpdate)

        }
    }

}

👉 android-architecture/TasksViewModel.kt at master · googlesamples/android-architecture 

Repository を展開して、スコープ周りを抜き出してみると、


class TasksViewModel(
    private val tasksRepository: TasksRepository
) : ViewModel() {

    fun loadTasks(forceUpdate: Boolean) {
        viewModelScope.launch {

            withContext(ioDispatcher) {
            
                // ...            

            }

        }
    }

}

というこれまで通り withContext() を使った dispatcher の切り換え。

他には、

前述の coroutineScope 関数を使った記述が、単独、並列、入れ子の3種類。


override suspend fun saveTask(task: Task) {
    coroutineScope {
        launch { tasksRemoteDataSource.saveTask(it) }
        launch { tasksLocalDataSource.saveTask(it) }
    }
}


override suspend fun clearCompletedTasks() {
    coroutineScope {
        launch { tasksRemoteDataSource.clearCompletedTasks() }
        launch { tasksLocalDataSource.clearCompletedTasks() }
    }
    withContext(ioDispatcher) {
        cachedTasks?.entries?.removeAll { it.value.isCompleted }
    }
}


override suspend fun deleteAllTasks() {
    withContext(ioDispatcher) {
        coroutineScope {
            launch { tasksRemoteDataSource.deleteAllTasks() }
            launch { tasksLocalDataSource.deleteAllTasks() }
        }
    }
}

👉 android-architecture/DefaultTasksRepository.kt at master · googlesamples/android-architecture 

今後、さらに簡潔に記述されていくのだろうが、意図は今のが理解しやすいと思いメモ。




SpaceX REST API で試す Retrofit の coroutine 対応

Retrofit の coroutine 対応が進んでいるようです。

Jake Wharton's retrofit2-kotlin-coroutines-adapter has been the go-to solution for bridging the coroutine world with Retrofit for a little while. But now, a much-anticipated PR has finally been merged, officially bringing coroutine support to Retrofit 2.

Retrofit meets coroutines - zsmb.co

JakeWharton/retrofit2-kotlin-coroutines-adapter: A Retrofit 2 adapter for Kotlin coroutine's Deferred type.

SpaceX-API とか公開されていたのですね。

r-spacex/SpaceX-API: Open Source REST API for rocket, core, capsule, pad, and launch data

https://api.spacexdata.com/v3/rockets

対応する最小限のデータクラスを作って、試してみましょう。


data class Rocket(

    val id: Int,

    @field:Json(name = "rocket_name") 
    val name: String

)


val retrofit = Retrofit.Builder()
    .baseUrl("https://api.spacexdata.com/v3/")
    .addConverterFactory(MoshiConverterFactory.create())
    .build()

val api = retrofit.create<SpaceXApi>()

create() は reified type が使えるようになってます。

JSONのシリアライズはお好みのものを。

そして、interface を書いていきますが、

ここを戻り値に別に見てみます。

Call<List<Rocket>>

interface はこれまでと同じ記述。


interface SpaceXApi {

  @GET("rockets")
  fun getRockets(): Call<List<Rocket>>

}

受け側の3つの例。


runBlocking {
  val rockets: List<Rocket> = api.getRockets().await()
  rockets.forEach(::println)
}


runBlocking {
  val rockets: List<Rocket> = try {
    api.getRockets().await()
  } catch (e: Exception) {
    println("Network error :[")
    return@runBlocking
  }
  rockets.forEach(::println)
}


runBlocking {
  val response = api.getRockets().awaitResponse()
  if (response.code() == 200) {
    response.body()?.forEach(::println)
  }
}

結果はすべて同じ。


Rocket(id=1, name=Falcon 1)
Rocket(id=2, name=Falcon 9)
Rocket(id=3, name=Falcon Heavy)
Rocket(id=4, name=Big Falcon Rocket)

これまで非同期処理時に必要だった コールバック や enqueue の記述は必要ありません。

受け側の記述を変化させることで、Exception や レスポンスコードを拾うことができます。

suspend List<Rocket>

suspend を使うことでさらに直感的に書けます。


interface SpaceXApi {

  @GET("rockets")
  suspend fun getRockets(): List<Rocket>

}


runBlocking {
  val rockets = api.getRockets()
  rockets.forEach(::println)
}

結果。


Rocket(id=1, name=Falcon 1)
Rocket(id=2, name=Falcon 9)
Rocket(id=3, name=Falcon Heavy)
Rocket(id=4, name=Big Falcon Rocket)

List<Rocket> に対して Call や Deferred のようなラッパーは必要ありません。

suspend Response<List<Rocket>>

Response を拾います。


interface SpaceXApi {

  @GET("rockets")
  suspend fun getRockets(): Response<List<Rocket>>

}


runBlocking {
  val response = api.getRockets()
  if (response.code() == 200) {
    response.body()?.forEach(::println)
  }
}

結果。


Rocket(id=1, name=Falcon 1)
Rocket(id=2, name=Falcon 9)
Rocket(id=3, name=Falcon Heavy)
Rocket(id=4, name=Big Falcon Rocket)

最も実用的で簡潔な記述と思われます。

まとめ

Kotlin で記述するにしても、多くの記述が可能となりそうです。

RxJava他ライブラリを利用しての非同期実装を含めると様々となります。

ネット上で検索するにも自分の環境や好みに合った記述を探すのにも時間がかかることになり混乱もありそうで。

Jakeも今後はメンテしないと思われます。

Márton Braun on Twitter: "Retrofit is finally receiving the proper coroutine support it deserves! Read more about it in my latest blog post here: https://t.co/8k9EauzzUa #AndroidDev #Kotlin #Coroutines" / Twitter

ちなみに、これらの話は今現在はSNAPSHOTで進行中です。

以下でどうぞ。


repositories {
  maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
}

dependencies {
  implementation "com.squareup.retrofit2:retrofit:2.5.1-SNAPSHOT"
}