Understanding Kotlin Coroutines: コルーチンで進化するアプリケーション開発

D0a4d1da4644054751e3fa7fd023ad8d?s=47 mhidaka
February 08, 2019

Understanding Kotlin Coroutines: コルーチンで進化するアプリケーション開発

Understanding Kotlin Coroutines: コルーチンで進化するアプリケーション開発

-----
発表時の内容に誤りがあったのでTypoおよび本資料のP17を修正しています。詳細は次のブログポスト( https://mhidaka.hatenablog.com/entry/2019/02/12/021016 )をご確認ください
-----

Kotlin Coroutinesは非同期処理をシンプルに記述できるKotlinの言語機能です。実験的な機能としてこれまでも提供されてきましたがKotlin 1.3で正式にリリース予定です。

Androidの誕生から10年たちアプリの利用シーンが増えた結果、アプリ開発の複雑さも増してきています。開発者はアプリの性質に合わせてMVVMをはじめとしたアーキテクチャとArchitecture Components(AAC)など複雑性を解消するライブラリを組み合わせ、実装上の課題を解決してきました。

本セッションでは新登場のCoroutinesを既存のアプリケーションへ適用する方法と導入するメリットを中心に解説します。

Coroutinesの本質を理解することはアーキテクチャをよりシンプルに保ちます。そして言語機能の追加がアプリ開発現場に与える影響を考察していきます。

設計・開発の効率化といえば、これまで多くの場合、ライブラリの導入がメインでした。しかしアプリの複雑性が増加するにつれて多数のライブラリを導入しつづけることは開発者に多くの知識を要求し、イニシャルコストの増加に繋がります。言語そのものの力を理解することでコストを下げ、より素早い開発を実現できます。

本セッションはアプリ開発で実践できるよう、なじみのあるアーキテクチャを通じてCoroutinesを学んでいきます。
聞き終わったときには、AACなどモダンなライブラリのなかで効率的な非同期処理に対応する方法を習得できます。

D0a4d1da4644054751e3fa7fd023ad8d?s=128

mhidaka

February 08, 2019
Tweet

Transcript

  1. 4.
  2. 9.

    はじめてのコルーチン 9 import kotlinx.coroutines.* fun main() { GlobalScope.launch { //

    コルーチンの起動 delay(1000L) // コルーチンを1秒中断 println(“World!”) // “World” を出力 } println(“Hello,”) // メインスレッドで “Hello,” を出力 Thread.sleep(2000L) // スレッドを2秒停止(JVMが終了しないように) } https://kotlinlang.org/docs/reference/coroutines/basics.html
  3. 10.

    はじめてのコルーチンと出力 10 import kotlinx.coroutines.* fun main() { GlobalScope.launch { //

    コルーチンの起動 delay(1000L) // コルーチンを1秒中断 println(“World!”) // “World” を出力 } println(“Hello,”) // メインスレッドで “Hello,” を出力 Thread.sleep(2000L) // スレッドを2秒停止(JVMが終了しないように) } https://kotlinlang.org/docs/reference/coroutines/basics.html $ > Hello, World!
  4. 11.

    コルーチンの特徴 11 Suspending Coroutines, Blocking Threads CC BY SA 2.0

    https://www.flickr.com/photos/librariesrock/44436145281/
  5. 12.

    コルーチンの特徴:中断可能 12 ひとくくりの処理に対して中断と再開を提供 Coroutine Task Job A Job B Job

    C Entry Return Job A~Cに対して処理の中断/再開を提供する (スレッドをブロックしない!)
  6. 13.

    サスペンド関数 - Suspending Functions 13 GlobalScope.launch { // コルーチンの起動 delay(1000L)

    // コルーチンを1秒中断 println(“World!”) // “World” を出力 } suspend fun delay(timeMillis: Long): Unit (source) https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
  7. 16.

    コルーチンの特徴(イメージ) 16 複数のコルーチンで協調し、資源を有効に活用 Coroutine1の関数が分割されて実行している点に注意 Coroutine 1 Job A Job B

    Job F Coroutine 2 Job C Job D Job E Coroutine 1 Coroutine 3 CPU等利用の時間軸 Suspending Functions Coroutines 関数の実行を中断できる
  8. 19.

    コールバックヘルの解決(擬似コード) 19 callbackA ( a -> { callbackB ( b

    -> { callbackC ( c -> { sum = a + b + c } ) }) } ) launch { // コルーチンの起動 val a = callbackA() val b = callbackB() val c = callbackC() val sum = a + b + c }
  9. 22.

    コルーチンスコープ - CoroutineScope 22 GlobalScope.launch { // コルーチンの起動 delay(1000L) //

    コルーチンを1秒中断 println(“World!”) // “World” を出力 } object GlobalScope : CoroutineScope { ...} https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html
  10. 23.

    コルーチンスコープ - CoroutinScope 23 GlobalScope.launch { // コルーチンの起動 delay(1000L) //

    コルーチンを1秒中断 println(“World!”) // “World” を出力 } object GlobalScope : CoroutineScope { ...} https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html Job A Job B
  11. 25.

    キャンセル処理の例 25 https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html val job = CoroutineScope(Dispatchers.Default).launch{ repeat(1000) { i

    -> println(“I‘m sleeping $i ...”) delay(500L) } } delay(1300L) // 1.3秒中断する println(“main: I’m tired of waiting!”) job.cancel() // ジョブの中止 job.join() // 中止処理の完了待ち println("main: Now I can quit.")
  12. 26.

    キャンセル処理の例 26 https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html val job = CoroutineScope(Dispatchers.Default).launch{ repeat(1000) { i

    -> println(“I‘m sleeping $i ...”) delay(500L) } } delay(1300L) // 1.3秒中断する println(“main: I’m tired of waiting!”) job.cancel() // ジョブの中止 job.join() // 中止処理の完了待ち println("main: Now I can quit.") 実行単位・スコープ コルーチンの作成 バックグラウンド・ジョブ
  13. 27.

    キャンセル処理の例 27 https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html val job = CoroutineScope(Dispatchers.Default).launch{ repeat(1000) { i

    -> println(“I‘m sleeping $i ...”) delay(500L) } } delay(1300L) // 1.3秒中断する println(“main: I’m tired of waiting!”) job.cancel() // ジョブの中止 job.join() // 中止処理の完了待ち println("main: Now I can quit.")
  14. 38.

    コルーチンは順次処理が基本 38 suspend fun loadFromNetwork(endPoint1: String, endPoint2: String): ResultData {

    val result1 = callApi(endPoint1) val result2 = callApi(endPoint2, result1) // 最初の結果を使う return combineResults(result1, result2) // 結果をマージして返却 } suspend fun callApi(endPoint: String): ResultData { … } // ネットワーク処理
  15. 39.

    asyncでコルーチン化し待ち合わせる(1/2) 39 suspend fun loadFromNetwork(endPoint1: String, endPoint2: String): ResultData {

    val deferred1 = async { callApi(endPoint1) } val deferred2 = async { callApi(endPoint2) } combineResults(deferred1.await(), deferred2.await()) } https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
  16. 40.

    asyncでコルーチン化し待ち合わせる(1/2) 40 suspend fun loadFromNetwork(endPoint1: String, endPoint2: String): ResultData {

    val deferred1 = async { callApi(endPoint1) } val deferred2 = async { callApi(endPoint2) } combineResults(deferred1.await(), deferred2.await()) } https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html fun <T> CoroutineScope.async( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T ): Deferred<T> (source)
  17. 41.

    CoroutineScopeによる構造化(2/2) 41 suspend fun loadFromNetwork(endPoint1: String, endPoint2: String): ResultData =

    coroutineScope { val deferred1 = async { callApi(endPoint1) } val deferred2 = async { callApi(endPoint2) } combineResults(deferred1.await(), deferred2.await()) } https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html interface Deferred<out T> : Job (source) abstract suspend fun await(): T
  18. 50.

    典型的なアプリ構造の課題設定 50 DataSource Repository ViewModel State Operations UI / View

    Activity Fragment モジュール間でのシーケンスを考える
  19. 55.

    UI 55 https://github.com/googlesamples/android-sunflower class PlantDetailFragment : Fragment() { override fun

    onCreateView(...): View? { ... val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>( inflater, R.layout.fragment_plant_detail, container, false).apply { viewModel = plantDetailViewModel setLifecycleOwner(this@PlantDetailFragment) fab.setOnClickListener { view -> plantDetailViewModel.addPlantToGarden() Snackbar.make(view, R.string.added_plant_to_garden, Snackbar.LENGTH_LONG).show() } } ... } ... } Fragment ViewModel Repository Database fab.setOnClickListener { view -> plantDetailViewModel.addPlantToGarden() Snackbar.make(view, R.string.added_plant_to_garden, Snackbar.LENGTH_LONG).show() }
  20. 56.

    ViewModel – Launch Coroutine 56 class PlantDetailViewModel(...) : ViewModel() {

    ... fun addPlantToGarden() { viewModelScope.launch { gardenPlantingRepository.createGardenPlanting(plantId) } } } Fragment ViewModel Repository Database
  21. 57.

    ViewModel - Create CoroutineScope 57 class PlantDetailViewModel(...) : ViewModel() {

    private val viewModelJob = Job() private val viewModelScope = CoroutineScope(Main + viewModelJob) override fun onCleared() { super.onCleared() viewModelJob.cancel() } fun addPlantToGarden() { viewModelScope.launch { gardenPlantingRepository.createGardenPlanting(plantId) } } } Fragument ViewModel Repository Database viewModelScopeの生成・破棄(cancel)処理はlifecycle- viewmodel-ktx:2.1.0-alpha01では標準でsupportしています
  22. 58.

    Repository – Suspending functions 58 class GardenPlantingRepository private constructor( private

    val gardenPlantingDao: GardenPlantingDao ) { suspend fun createGardenPlanting(plantId: String) { withContext(IO) { val gardenPlanting = GardenPlanting(plantId) gardenPlantingDao.insertGardenPlanting(gardenPlanting) } } ... } Fragment ViewModel Repository Database suspend fun createGardenPlanting(plantId: String) { withContext(IO) { val gardenPlanting = GardenPlanting(plantId) gardenPlantingDao.insertGardenPlanting(gardenPlanting) } }
  23. 59.

    Database – Room 59 @Dao interface GardenPlantingDao { @Query("SELECT *

    FROM garden_plantings") fun getGardenPlantings(): LiveData<List<GardenPlanting>> ... @Transaction @Query("SELECT * FROM plants") fun getPlantAndGardenPlantings(): LiveData<List<PlantAndGardenPlantings @Insert fun insertGardenPlanting(gardenPlanting: GardenPlanting): Long @Delete fun deleteGardenPlanting(gardenPlanting: GardenPlanting) } Fragment ViewModel Repository Database
  24. 61.

    サスペンド関数のテストコード 61 @RunWith(JUnit4::class) class AwaitTest { @Test fun whenAwaitWithResult() {

    runBlocking { val subject = createSuspendFunction("実行待ち") Truth.assertThat(subject.await()).isEqualTo("実行待ち") } } Blocking Thread
  25. 62.

    ボイラープレートの作成 62 fun loadFlowerList() { viewModelScope.launch { try { loading.value

    = true flowerRepository.load() } catch (error: FlowerLoadError) { snackbar.value = error.message } finally { loading.value = false } Suspend ロード表示 非表示
  26. 63.

    ボイラープレートの作成 private fun launchBoilerplate(block: suspend () -> Unit): Job {

    return viewModelScope.launch { try { loading.value = true block() } catch (error: FlowerLoadError) { snackbar.value = error.message } finally { loading.value = false } Suspending Funcation
  27. 65.

    リスナーから値を受け取る 65 suspend fun <T> TargetClass<T>.await(): T { return suspendCoroutine

    { continuation -> addOnResultListener { result -> when (result) { is Success<T> -> continuation.resume(result.data) is Error -> continuation.resumeWithException(result.error) } } } 継続の作成 suspend fun callTarget() { try { val target = createTagetClass() // リスナーを持つクラス val result = target.await() // 値の返却があるまで中断 } catch (error: TargetException) { // resumeWithException発生時 } } 拡張関数 呼び出し https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/suspend-coroutine.html
  28. 68.

    問題を振り返る 68 DataSource Repository ViewModel State Operations UI / View

    Activity Fragment モジュール間でのシーケンスをみてきました
  29. 69.

    問題を捉える(1) 69 DataSource Repository ViewModel State Operations UI / View

    Activity Fragment データフローの問題としてアーキテクチャを組み立てるのであれば ストリームとして捉えてRxJavaなどを使って解決 一貫性を持ったデータの流れを提供
  30. 70.

    問題を捉える(2) 70 DataSource Repository ViewModel State Operations UI / View

    Activity Fragment モジュール単位で関心を分離して、独立性の高い開発モデル を主眼とした場合には関心をモジュールという単位で分離する コルーチンを使った関心の分離
  31. 72.

    問題を捉える(4) 72 ViewModel State Operations UI / Activity ライフサイクルに密接に関わる責務の分解 Android

    specifiedな機能を抽象化し、インターフェイスを提供 Device Camera, GPS, BT Fingerprints
  32. 77.

    Reference 77 https://github.com/Kotlin/kotlinx.coroutines コルーチンのサイト。いつも関数リファレンスのお世話になってます。 https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html ガイドライン。初めてのひとはここから学べる。Android特有の事情は触れられていない。 https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html メソッドリファレンス https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/ Deferredメソッドリファレンス

    https://medium.com/@elizarov/blocking-threads-suspending-coroutines-d33e11bf4761 Roman Elizarovさんによる「Blocking threads, suspending coroutines」 https://talks.golang.org/2012/waza.slide#1 Rob Pikeさんの並行性に関するわかりやすい発表「Concurrency is not Parallelism」 https://ja.wikipedia.org/wiki/%E4%B8%A6%E8%A1%8C%E6%80%A7 Wikipedia 並行性 https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%AB%E3%83%BC%E3%83%81%E3%83%B3 Wikipedia コルーチン。初出はコンウェイの1963年の論文 https://medium.com/@elizarov/structured-concurrency-722d765aa952 Roman Elizarovさん「Structured concurrency」構造化コンカレンシーについての解説 https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/ GoでのStructured concurrencyの解説 http://sys1yagi.hatenablog.com/entry/2018/12/12/224116 yagiさんのブログ https://ja.wikipedia.org/wiki/%E9%96%A2%E5%BF%83%E3%81%AE%E5%88%86%E9%9B%A2 関心の分離 https://xn--97- 273ae6a4irb6e2hsoiozc2g4b8082p.com/%E3%82%A8%E3%83%83%E3%82%BB%E3%82%A4/%E5%8D%98%E4%B8%80%E8%B2%AC%E4%BB%BB%E5%8E%9F %E5%89%87/単一責任原則 https://codelabs.developers.google.com/codelabs/kotlin-coroutines/ Androidアプリ。コルーチンのコードラボ。おすすめ https://kotlinlang.org/docs/reference/coroutines/basics.html 公式リファレンスの基礎編 https://qiita.com/takahirom/items/22a6c6ab4c879dd472e4 takahiromさんによるRxとCoroutinesのバインドのはなし https://resources.jetbrains.com/storage/products/kotlinconf2018/slides/3_Android%20Suspenders.pdf KotlinConf 2018 発表資料 Android Suspenders https://www.youtube.com/watch?v=P7ov_r1JZ1g KotlinConf 2018 - Android Suspenders by Chris Banes https://www.youtube.com/watch?v=a3agLJQ6vt8 KotlinConf 2018 - Kotlin Coroutines in Practice by Roman Elizarov