What's new in Kotlin Coroutines on Android

What's new in Kotlin Coroutines on Android

7166bc2cbc462ab5fd1987a76d0fe709?s=128

takahirom

May 21, 2019
Tweet

Transcript

  1. What's new in Kotlin Coroutines on Android takahirom (mixi &

    CA).aab 〜Google I/O報告会〜
  2. About me • Androidが好きです • AbemaTVのAndroidアプリの開発 • DroidKaigi 2018, 2019公式アプリのオーガナイザー

    takahirom (@new_runnable) @takahirom @takahirom
  3. Jetpackにおいて Kotlin CoroutinesのFirst Classサポートが発表

  4. Kotlin Coroutinesの対応を みていこう

  5. Kotlin Coroutinesの対応 • WorkManager • Lifecycle • LiveData • ViewModel

    • Room • Compose • kotlinx:kotlinx-coroutines-test
 (GoogleとJetbrainsの共同) 今⽇話すこと • 今回は基本的なところは省略します!
  6. WorkManager (Stable)

  7. WorkManager • WorkManagerはバックグラウンドでなにか処理を実⾏するもの • 普通に実装するとこういう感じ

  8. Coroutines対応していない WorkManagerの問題点 ユーザーがUIでキャンセルさせたりなど、キャンセルに対応したい
 場合はこういう形にしないといけない

  9. Coroutines対応したWorkManager suspend functionを使うと
 キャンセルに対応できるため、途中で処理を⽌められる 
 (CancelationExceptionが投げられるので処理が⽌まる)

  10. Lifecycle 2.2.0-alpha01 androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01

  11. LifecycleOwner.lifecycleScope • LifecycleOwner.lifecycleScopeができた。

  12. LifecycleOwner.lifecycleScope の中⾝ onDestory()でCorouitnesで⾏っている処理が キャンセルされる

  13. LifecycleOwner.lifecycleScopeの 問題点 • onDestoryでキャンセルされるが、
 onStop以降でFragmentのTransactionを
 ⾏うとIllegalStateExceptionになる。

  14. LifecycleCoroutineScope .launchWhenStarted {} • launchWhenStarted()を使うと
 onStart()以降のときだけ処理が⾏われる

  15. LifecycleCoroutineScope .launchWhenStarted {} • launchWhenStarted()を使うと
 onStart()以降のときだけ処理が⾏われる onStart以降だけ。。?

  16. onStart()以降だけで 実⾏されるとは? minState(onStart以降かどうか)を⾒て処理を ⼀時停⽌、再開する処理が書かれている • LifecycleCoroutineScope.launchWhenStarted {}の中⾝

  17. LifecycleOwner.lifecycleScopeの 問題点 • 画⾯回転などのconfigChangeで毎回⽌まってしまう • そのたびに毎回呼ばれてしまって無駄になる

  18. LifecycleOwner.lifecycleScopeの 問題点 • 画⾯回転などのconfigChangeで毎回⽌まってしまう • そのたびに毎回呼ばれてしまって無駄になる そこでLiveDataの対応

  19. LiveData 2.2.0-alpha01 androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01

  20. LiveDataのCoroutiens対応 liveData {} LiveDataをliveData{}を使って作成する

  21. LiveDataのCoroutiens対応 liveData {} liveData{}の中ではsuspend functionを呼び出せる

  22. LiveDataのCoroutiens対応 liveData {} emitでLiveDataに値を流す

  23. LiveDataのCoroutiens対応 liveData {} emitSourceを使うと他のLiveDataの値を流すこともできる

  24. LiveDataのCoroutiens対応 liveData {} • よくあるコルーチンでアクセスして、LiveDataで流すのを
 簡単にできる • それだけでなく、いくつかの問題を解決できる。

  25. コードを読んで liveData{}がどういう動きをする のか⾒ていこう

  26. 全体

  27. liveData{}のKDocに書かれていることを中⼼に⾒てい きます (セッションでも説明がありました) /** * Builds a LiveData that has

    values yielded from the given [block] that executes on a * [LiveDataScope]. * * The [block] starts executing when the returned [LiveData] becomes active ([LiveData.onActive]). * If the [LiveData] becomes inactive ([LiveData.onInactive]) while the [block] is executing, it * will be cancelled after [timeoutInMs] milliseconds unless the [LiveData] becomes active again * before that timeout (to gracefully handle cases like Activity rotation). Any value * [LiveDataScope.emit]ed from a cancelled [block] will be ignored. * * After a cancellation, if the [LiveData] becomes active again, the [block] will be re-executed * from the beginning. If you would like to continue the operations based on where it was stopped * last, you can use the [LiveDataScope.initialValue] function to get the last * [LiveDataScope.emit]ed value. * If the [block] completes successfully *or* is cancelled due to reasons other than [LiveData] * becoming inactive, it *will not* be re-executed even after [LiveData] goes through active * inactive cycle. * * As a best practice, it is important for the [block] to cooperate in cancellation. See kotlin * coroutines documentation for details * https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html. * * ``` * // a simple LiveData that receives value 3, 3 seconds after being observed for the first time. * val data : LiveData<Int> = liveData { * delay(3000) * emit(3) * } * * * // a LiveData that fetches a `User` object based on a `userId` and refreshes it every 30 seconds * // as long as it is observed * val userId : LiveData<String> = ... * val user = userId.switchMap { id -> * liveData { * while(true) { * // note that `while(true)` is fine because the `delay(30_000)` below will cooperate in * // cancellation if LiveData is not actively observed anymore * val data = api.fetch(id) // errors are ignored for brevity * emit(data) * delay(30_000) * } * } * } * * // A retrying data fetcher with doubling back-off * val user = liveData { * var backOffTime = 1_000 * var succeeded = false * while(!succeeded) { * try { * emit(api.fetch(id)) * succeeded = true * } catch(ioError : IOException) { * delay(backOffTime) * backOffTime *= minOf(backOffTime * 2, 60_000) * } * } * } * * // a LiveData that tries to load the `User` from local cache first and then tries to fetch * // from the server and also yields the updated value * val user = liveData { * // dispatch loading first * emit(LOADING(id)) * // check local storage * val cached = cache.loadUser(id) * if (cached != null) { * emit(cached) * } * if (cached == null || cached.isStale()) { * val fresh = api.fetch(id) // errors are ignored for brevity * cache.save(fresh) * emit(fresh) * } * } * * // a LiveData that immediately receives a LiveData<User> from the database and yields it as a * // source but also tries to back-fill the database from the server * val user = liveData { * val fromDb: LiveData<User> = roomDatabase.loadUser(id) * emitSource(fromDb) * val updated = api.fetch(id) // errors are ignored for brevity * // Since we are using Room here, updating the database will update the `fromDb` LiveData * // that was obtained above. See Room's documentation for more details. * // https://developer.android.com/training/data-storage/room/accessing-data#query-observable * roomDatabase.insert(updated) * } * ``` * * @param context The CoroutineContext to run the given block in. Defaults to * [EmptyCoroutineContext] combined with [Dispatchers.Main] * @param timeoutInMs The timeout in ms before cancelling the block if there are no active observers * ([LiveData.hasActiveObservers]. Defaults to [DEFAULT_TIMEOUT]. * @param block The block to run when the [LiveData] has active observers. */ @UseExperimental(ExperimentalTypeInference::class) fun <T> liveData( context: CoroutineContext = EmptyCoroutineContext, timeoutInMs: Long = DEFAULT_TIMEOUT, @BuilderInference block: suspend LiveDataScope<T>.() -> Unit ): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block) • • LiveDataがactive(onStartの後)になったときに引 数のブロックが実⾏される • LiveDataがinactiveになったとき再度activeになら ずに時間が経過したらブロックの処理がキャンセ ルされる • LiveDataのinactiveによってキャンセルされた 後、LiveDataが再度Activeになったときはブロッ クは最初から再実⾏ • ブロックが成功したか、LiveDataがinactiveにな る以外の理由でキャンセルになったら(Exception がthrowされるとそうなります)、activeになって も、もう⼀度ブロックが実⾏されることはありま せん。 • ⻑いKDoc
  28. liveData{}のKDocに書かれているこ と • LiveDataがactive(onStartの後)になったときに引数のブロックが実⾏ される • LiveDataがinactiveになったとき再度activeにならずに時間が経過した らブロックの処理がキャンセルされる • LiveDataのinactiveによってキャンセルされた後、LiveDataが再度

    Activeになったときはブロックは最初から再実⾏ • ブロックが成功したか、LiveDataがinactiveになる以外の理由でキャン セルになったら(Exceptionがthrowされるとそうなります)、activeに なっても、もう⼀度ブロックが実⾏されることはありません。
  29. None
  30. liveData{}が呼ばれる

  31. liveData{}は CoroutineLiveDataを返す

  32. CoroutineLiveDataクラスは MediatorLiveDataを継承している

  33. LiveDataのonActive() つまり、ActivityなどのonStart()で 呼び出される

  34. maybeRun()を呼ぶことで ブロックが実⾏される

  35. liveData{}のKDocに書かれているこ と • LiveDataがactive(onStartの後)になったときに引数のブロックが実⾏さ れる • LiveDataがinactiveになったとき再度activeにならずに時間が経過し たらブロックの処理がキャンセルされる • LiveDataのinactiveによってキャンセルされた後、LiveDataが再度

    Activeになったときはブロックは最初から再実⾏ • ブロックが成功したか、LiveDataがinactiveになる以外の理由でキャン セルになったら(Exceptionがthrowされるとそうなります)、activeに なっても、もう⼀度ブロックが実⾏されることはありません。
  36. None
  37. Activityなどが onStop()になったりした タイミングでonInactive()が呼ば れる。

  38. blockRunner.cancel()を呼ぶ

  39. BlockRunner.cancel()が呼ばれる

  40. timeoutInMs 後まで待つ timeoutInMsはデフォルトで5秒になっている (理由は2スライド後)

  41. 待った後に、LiveDataがactiveなobserverがいなければ キャンセルする

  42. このcancellationJobはLiveDataが再度
 Activeになったときにキャンセルされる

  43. このcancellationJobはLiveDataが再度
 Activeになったときにキャンセルされる = 画⾯回転のときにはliveData{}に渡す引数のブロックは キャンセルされない!

  44. liveData{}のKDocに書かれているこ と • LiveDataがactive(onStartの後)になったときに引数のブロックが実⾏さ れる • LiveDataがinactiveになったとき再度activeにならずに時間が経過した らブロックの処理がキャンセルされる • LiveDataのinactiveによってキャンセルされた後、LiveDataが再度

    Activeになったときはブロックは最初から再実⾏ • ブロックが成功したか、LiveDataがinactiveになる以外の理由でキャン セルになったら(Exceptionがthrowされるとそうなります)、activeに なっても、もう⼀度ブロックが実⾏されることはありません。
  45. None
  46. cancelした後に、runningJobにnullを⼊れる

  47. cancelした後に、runningJobにnullを⼊れので、 このif⽂を通過する!

  48. liveData{}のKDocに書かれているこ と • LiveDataがactive(onStartの後)になったときに引数のブロックが実⾏さ れる • LiveDataがinactiveになったとき再度activeにならずに時間が経過した らブロックの処理がキャンセルされる • LiveDataのinactiveによってキャンセルされた後、LiveDataが再度

    Activeになったときはブロックは最初から再実⾏ • ブロックが成功したか、LiveDataがinactiveになる以外の理由でキャ ンセルになったら(Exceptionがthrowされるとそうなります)、active になっても、もう⼀度ブロックが実⾏されることはありません。
  49. None
  50. ここが実際のliveData{}の引数で渡したブロックの実⾏部分

  51. 処理が終わるとonDone()が呼びだされる

  52. onDoneでblockRunnerがnullになる

  53. 安全呼び出しなので、nullが⼊ることでonStartのときなどに 実⾏されなくなる

  54. liveData{}のKDocに 書かれていることおさらい • LiveDataがactive(onStartの後)になったときに引数のブロックが実⾏さ れる • LiveDataがinactiveになったとき再度activeにならずに時間が経過した らブロックの処理がキャンセルされる • LiveDataのinactiveによってキャンセルされた後、LiveDataが再度

    Activeになったときはブロックは最初から再実⾏ • ブロックが成功したか、LiveDataがinactiveになる以外の理由でキャン セルになったら(Exceptionがthrowされるとそうなります)、activeに なっても、もう⼀度ブロックが実⾏されることはありません。
  55. liveData{}で 画⾯回転、エラー時の再実⾏など をいい感じに実⾏してくれる!

  56. kotlinx:kotlinx-coroutines-test
 (GoogleとJetbrainsの共同)

  57. これをテストしてみよう class ArticleViewModel : ViewModel() { var repository = Repository()

    val articles: LiveData<List<Article>> = liveData { val articles = repository.articles() delay(1000) emit(articles) }
  58. これをテストしてみよう class ArticleViewModel : ViewModel() { var repository = Repository()

    val articles: LiveData<List<Article>> = liveData { val articles = repository.articles() delay(1000) emit(articles) }
  59. これをテストしてみよう class ArticleViewModel : ViewModel() { var repository = Repository()

    val articles: LiveData<List<Article>> = liveData { val articles = repository.articles() delay(1000) emit(articles) } delay()が⼊っているので
 簡単には出来なそう?
  60. class ArticleViewModelTest { private val testDispatcher = TestCoroutineDispatcher() private val

    testScope = TestCoroutineScope(testDispatcher) @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @MockK lateinit var repository: Repository @Before fun setUp() { Dispatchers.setMain(testDispatcher) MockKAnnotations.init(this, relaxUnitFun = true) } @After fun tearDown() { Dispatchers.resetMain() testScope.cleanupTestCoroutines() } @Test fun testLiveData() = testScope.runBlockingTest { val articleViewModel = ArticleViewModel() articleViewModel.repository = repository val articlesData = listOf(Article("a"))
  61. class ArticleViewModelTest { private val testDispatcher = TestCoroutineDispatcher() private val

    testScope = TestCoroutineScope(testDispatcher) @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @MockK lateinit var repository: Repository @Before fun setUp() { Dispatchers.setMain(testDispatcher) MockKAnnotations.init(this, relaxUnitFun = true) } @After fun tearDown() { Dispatchers.resetMain() testScope.cleanupTestCoroutines() } @Test fun testLiveData() = testScope.runBlockingTest { val articleViewModel = ArticleViewModel() articleViewModel.repository = repository val articlesData = listOf(Article("a")) TestCoroutineDispatcherを作る テストのために設計されたDispatcher (org.jetbrains.kotlinx:kotlinx-coroutines-test利⽤) まだ@ExperimentalCoroutinesApi
  62. class ArticleViewModelTest { private val testDispatcher = TestCoroutineDispatcher() private val

    testScope = TestCoroutineScope(testDispatcher) @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @MockK lateinit var repository: Repository @Before fun setUp() { Dispatchers.setMain(testDispatcher) MockKAnnotations.init(this, relaxUnitFun = true) } @After fun tearDown() { Dispatchers.resetMain() testScope.cleanupTestCoroutines() } @Test fun testLiveData() = testScope.runBlockingTest { val articleViewModel = ArticleViewModel() articleViewModel.repository = repository val articlesData = listOf(Article("a")) AndroidのDispatchers.Mainを テスト⽤のDispatcherに⼊れ替える
  63. class ArticleViewModelTest { private val testDispatcher = TestCoroutineDispatcher() private val

    testScope = TestCoroutineScope(testDispatcher) @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @MockK lateinit var repository: Repository @Before fun setUp() { Dispatchers.setMain(testDispatcher) MockKAnnotations.init(this, relaxUnitFun = true) } @After fun tearDown() { Dispatchers.resetMain() testScope.cleanupTestCoroutines() } @Test fun testLiveData() = testScope.runBlockingTest { val articleViewModel = ArticleViewModel() articleViewModel.repository = repository val articlesData = listOf(Article("a")) testが終わったらresetする
  64. class ArticleViewModelTest { private val testDispatcher = TestCoroutineDispatcher() private val

    testScope = TestCoroutineScope(testDispatcher) @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @MockK lateinit var repository: Repository @Before fun setUp() { Dispatchers.setMain(testDispatcher) MockKAnnotations.init(this, relaxUnitFun = true) } @After fun tearDown() { Dispatchers.resetMain() testScope.cleanupTestCoroutines() } @Test fun testLiveData() = testScope.runBlockingTest { testDispatcherを使って TestCoroutineScopeを作る テストのために設計されたCoroutineScope
  65. @After fun tearDown() { Dispatchers.resetMain() testScope.cleanupTestCoroutines() } @Test fun testLiveData()

    = testScope.runBlockingTest { val articleViewModel = ArticleViewModel() articleViewModel.repository = repository val articlesData = listOf(Article("a")) coEvery { repository.articles() } returns articlesData val articles = articleViewModel.articles val observer = Observer<List<Article>> { Unit } try { articles.observeForever(observer) advanceTimeBy(1000) require(articles.value == articlesData) } finally { articles.removeObserver(observer) } } TestCoroutineScope.runBlockingTest {} で coroutineの処理を使うテストを⾏う
  66. class ArticleViewModelTest { private val testDispatcher = TestCoroutineDispatcher() private val

    testScope = TestCoroutineScope(testDispatcher) @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @MockK lateinit var repository: Repository @Before fun setUp() { Dispatchers.setMain(testDispatcher) MockKAnnotations.init(this, relaxUnitFun = true) } @After fun tearDown() { Dispatchers.resetMain() testScope.cleanupTestCoroutines() } @Test fun testLiveData() = testScope.runBlockingTest { val articleViewModel = ArticleViewModel() articleViewModel.repository = repository val articlesData = listOf(Article("a")) coEvery { ボイラープレートなのでJUnit4のRuleに
  67. class ArticleViewModelTest { @get:Rule val testCoroutinesRule = TestCoroutineRule() @get:Rule val

    instantTaskExecutorRule = InstantTaskExecutorRule() @MockK lateinit var repository: Repository @Before fun setUp() = MockKAnnotations.init(this, relaxUnitFun = true) @Test fun testLiveData() = testCoroutinesRule.runBlockingTest { val articleViewModel = ArticleViewModel() articleViewModel.repository = repository val articlesData = listOf(Article("a")) coEvery { repository.articles() } returns articlesData val articles = articleViewModel.articles TestCoroutineRuleとしてまとめる (ライブラリでは提供されていない)
  68. class ArticleViewModelTest { @get:Rule val testCoroutinesRule = TestCoroutineRule() @get:Rule val

    instantTaskExecutorRule = InstantTaskExecutorRule() @MockK lateinit var repository: Repository @Before fun setUp() = MockKAnnotations.init(this, relaxUnitFun = true) @Test fun testLiveData() = testCoroutinesRule.runBlockingTest { val articleViewModel = ArticleViewModel() articleViewModel.repository = repository val articlesData = listOf(Article("a")) coEvery { repository.articles() } returns articlesData val articles = articleViewModel.articles val observer = Observer<List<Article>> { Unit } try { articles.observeForever(observer) advanceTimeBy(1000) require(articles.value == articlesData) } finally { LiveDataなどの実⾏スレッドを そのままのスレッドで⾏うようにする (androidx.arch.core:core-testing)
  69. class ArticleViewModelTest { @get:Rule val testCoroutinesRule = TestCoroutineRule() @get:Rule val

    instantTaskExecutorRule = InstantTaskExecutorRule() @MockK lateinit var repository: Repository @Before fun setUp() = MockKAnnotations.init(this, relaxUnitFun = true) @Test fun testLiveData() = testCoroutinesRule.runBlockingTest { val articleViewModel = ArticleViewModel() articleViewModel.repository = repository val articlesData = listOf(Article("a")) coEvery { repository.articles() } returns articlesData val articles = articleViewModel.articles val observer = Observer<List<Article>> { Unit } try { articles.observeForever(observer) advanceTimeBy(1000) require(articles.value == articlesData) } finally { Mockkの初期化処理
  70. @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @MockK lateinit var repository: Repository

    @Before fun setUp() = MockKAnnotations.init(this, relaxUnitFun = true) @Test fun testLiveData() = testCoroutinesRule.runBlockingTest { val articleViewModel = ArticleViewModel() articleViewModel.repository = repository val articlesData = listOf(Article("a")) coEvery { repository.articles() } returns articlesData val articles = articleViewModel.articles val observer = Observer<List<Article>> { Unit } try { articles.observeForever(observer) advanceTimeBy(1000) require(articles.value == articlesData) } finally { articles.removeObserver(observer) } } observeすることでliveDataの処理が 動く
  71. repository.articles() } returns articlesData val articles = articleViewModel.articles val observer

    = Observer<List<Article>> { Unit } try { articles.observeForever(observer) advanceTimeBy(1000) require(articles.value == articlesData) } finally { articles.removeObserver(observer) } } } class ArticleViewModel : ViewModel() { var repository = Repository() val articles: LiveData<List<Article>> = liveData { val articles = repository.articles() delay(1000) emit(articles) } delay()の分時間を進める
  72. val instantTaskExecutorRule = InstantTaskExecutorRule() @MockK lateinit var repository: Repository @Before

    fun setUp() = MockKAnnotations.init(this, relaxUnitFun = true) @Test fun testLiveData() = testCoroutinesRule.runBlockingTest { val articleViewModel = ArticleViewModel() articleViewModel.repository = repository val articlesData = listOf(Article("a")) coEvery { repository.articles() } returns articlesData val articles = articleViewModel.articles val observer = Observer<List<Article>> { Unit } try { articles.observeForever(observer) advanceTimeBy(1000) require(articles.value == articlesData) } finally { articles.removeObserver(observer) } } } 終わったらremoveObserverする
  73. @MockK lateinit var repository: Repository @Before fun setUp() = MockKAnnotations.init(this,

    relaxUnitFun = true) @Test fun testLiveData() = testCoroutinesRule.runBlockingTest { val articleViewModel = ArticleViewModel() articleViewModel.repository = repository val articlesData = listOf(Article("a")) coEvery { repository.articles() } returns articlesData val articles = articleViewModel.articles val observer = Observer<List<Article>> { Unit } try { articles.observeForever(observer) advanceTimeBy(1000) require(articles.value == articlesData) } finally { articles.removeObserver(observer) } } } ボイラープレートなので いい感じに
  74. @Before fun setUp() = MockKAnnotations.init(this, relaxUnitFun = true) @Test fun

    testLiveData() = testCoroutinesRule.runBlockingTest { val articleViewModel = ArticleViewModel() articleViewModel.repository = repository val articlesData = listOf(Article("a")) coEvery { repository.articles() } returns articlesData val articles = articleViewModel.articles articles.observeForTesting { advanceTimeBy(1000) require(articles.value == articlesData) } } } いい感じに (ライブラリにはないので、⾃作する)
  75. まとめ

  76. まとめ • WorkManagerのCoroutines対応はキャンセルが楽 になる

  77. まとめ • WorkManagerのCoroutines対応はキャンセルが楽 になる • liveData{}で画⾯回転や再実⾏を処理してくれるの で便利

  78. まとめ • WorkManagerのCoroutines対応はキャンセルが楽 になる • liveData{}で画⾯回転や再実⾏を処理してくれるの で便利 • kotlinx:kotlinx-coroutines-testでテストのための 仕組みが追加されたのでそれを使ってテストを書

    こう
  79. 参考 • Understand Kotlin Coroutines on Android (Google I/O’19) 


    https://www.youtube.com/watch?v=BOHK_w09pVA • AOSPのCoroutineを含むCL⼀覧 
 https://android-review.googlesource.com/q/ project:platform/frameworks/support+coroutines • 雑なSampleリポジトリ 
 https://github.com/takahirom/lifecycle-2.2.0-and-kotlinx- coroutines-test-sample
  80. ありがとうございました