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

What's new in Kotlin Coroutines on Android

What's new in Kotlin Coroutines on Android

takahirom

May 21, 2019
Tweet

More Decks by takahirom

Other Decks in Programming

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. ありがとうございました