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報告会〜

    View Slide

  2. About me
    • Androidが好きです
    • AbemaTVのAndroidアプリの開発
    • DroidKaigi 2018, 2019公式アプリのオーガナイザー
    takahirom
    (@new_runnable)
    @takahirom
    @takahirom

    View Slide

  3. Jetpackにおいて
    Kotlin CoroutinesのFirst Classサポートが発表

    View Slide

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

    View Slide

  5. Kotlin Coroutinesの対応
    • WorkManager
    • Lifecycle
    • LiveData
    • ViewModel
    • Room
    • Compose
    • kotlinx:kotlinx-coroutines-test

    (GoogleとJetbrainsの共同)
    今⽇話すこと
    • 今回は基本的なところは省略します!

    View Slide

  6. WorkManager
    (Stable)

    View Slide

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

    View Slide

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

    場合はこういう形にしないといけない

    View Slide

  9. Coroutines対応したWorkManager
    suspend functionを使うと

    キャンセルに対応できるため、途中で処理を⽌められる 

    (CancelationExceptionが投げられるので処理が⽌まる)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  13. LifecycleOwner.lifecycleScopeの
    問題点
    • onDestoryでキャンセルされるが、

    onStop以降でFragmentのTransactionを

    ⾏うとIllegalStateExceptionになる。

    View Slide

  14. LifecycleCoroutineScope
    .launchWhenStarted {}
    • launchWhenStarted()を使うと

    onStart()以降のときだけ処理が⾏われる

    View Slide

  15. LifecycleCoroutineScope
    .launchWhenStarted {}
    • launchWhenStarted()を使うと

    onStart()以降のときだけ処理が⾏われる
    onStart以降だけ。。?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  24. LiveDataのCoroutiens対応
    liveData {}
    • よくあるコルーチンでアクセスして、LiveDataで流すのを

    簡単にできる
    • それだけでなく、いくつかの問題を解決できる。

    View Slide

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

    View Slide

  26. 全体

    View Slide

  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 = 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 = ...
    * 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 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 = 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 liveData(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = DEFAULT_TIMEOUT,
    @BuilderInference block: suspend LiveDataScope.() -> Unit
    ): LiveData = CoroutineLiveData(context, timeoutInMs, block)

    • LiveDataがactive(onStartの後)になったときに引
    数のブロックが実⾏される
    • LiveDataがinactiveになったとき再度activeになら
    ずに時間が経過したらブロックの処理がキャンセ
    ルされる
    • LiveDataのinactiveによってキャンセルされた
    後、LiveDataが再度Activeになったときはブロッ
    クは最初から再実⾏
    • ブロックが成功したか、LiveDataがinactiveにな
    る以外の理由でキャンセルになったら(Exception
    がthrowされるとそうなります)、activeになって
    も、もう⼀度ブロックが実⾏されることはありま
    せん。
    • ⻑いKDoc

    View Slide

  28. liveData{}のKDocに書かれているこ

    • LiveDataがactive(onStartの後)になったときに引数のブロックが実⾏
    される
    • LiveDataがinactiveになったとき再度activeにならずに時間が経過した
    らブロックの処理がキャンセルされる
    • LiveDataのinactiveによってキャンセルされた後、LiveDataが再度
    Activeになったときはブロックは最初から再実⾏
    • ブロックが成功したか、LiveDataがinactiveになる以外の理由でキャン
    セルになったら(Exceptionがthrowされるとそうなります)、activeに
    なっても、もう⼀度ブロックが実⾏されることはありません。

    View Slide

  29. View Slide

  30. liveData{}が呼ばれる

    View Slide

  31. liveData{}は
    CoroutineLiveDataを返す

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  35. liveData{}のKDocに書かれているこ

    • LiveDataがactive(onStartの後)になったときに引数のブロックが実⾏さ
    れる
    • LiveDataがinactiveになったとき再度activeにならずに時間が経過し
    たらブロックの処理がキャンセルされる
    • LiveDataのinactiveによってキャンセルされた後、LiveDataが再度
    Activeになったときはブロックは最初から再実⾏
    • ブロックが成功したか、LiveDataがinactiveになる以外の理由でキャン
    セルになったら(Exceptionがthrowされるとそうなります)、activeに
    なっても、もう⼀度ブロックが実⾏されることはありません。

    View Slide

  36. View Slide

  37. Activityなどが
    onStop()になったりした
    タイミングでonInactive()が呼ば
    れる。

    View Slide

  38. blockRunner.cancel()を呼ぶ

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  42. このcancellationJobはLiveDataが再度

    Activeになったときにキャンセルされる

    View Slide

  43. このcancellationJobはLiveDataが再度

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

    View Slide

  44. liveData{}のKDocに書かれているこ

    • LiveDataがactive(onStartの後)になったときに引数のブロックが実⾏さ
    れる
    • LiveDataがinactiveになったとき再度activeにならずに時間が経過した
    らブロックの処理がキャンセルされる
    • LiveDataのinactiveによってキャンセルされた後、LiveDataが再度
    Activeになったときはブロックは最初から再実⾏
    • ブロックが成功したか、LiveDataがinactiveになる以外の理由でキャン
    セルになったら(Exceptionがthrowされるとそうなります)、activeに
    なっても、もう⼀度ブロックが実⾏されることはありません。

    View Slide

  45. View Slide

  46. cancelした後に、runningJobにnullを⼊れる

    View Slide

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

    View Slide

  48. liveData{}のKDocに書かれているこ

    • LiveDataがactive(onStartの後)になったときに引数のブロックが実⾏さ
    れる
    • LiveDataがinactiveになったとき再度activeにならずに時間が経過した
    らブロックの処理がキャンセルされる
    • LiveDataのinactiveによってキャンセルされた後、LiveDataが再度
    Activeになったときはブロックは最初から再実⾏
    • ブロックが成功したか、LiveDataがinactiveになる以外の理由でキャ
    ンセルになったら(Exceptionがthrowされるとそうなります)、active
    になっても、もう⼀度ブロックが実⾏されることはありません。

    View Slide

  49. View Slide

  50. ここが実際のliveData{}の引数で渡したブロックの実⾏部分

    View Slide

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

    View Slide

  52. onDoneでblockRunnerがnullになる

    View Slide

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

    View Slide

  54. liveData{}のKDocに
    書かれていることおさらい
    • LiveDataがactive(onStartの後)になったときに引数のブロックが実⾏さ
    れる
    • LiveDataがinactiveになったとき再度activeにならずに時間が経過した
    らブロックの処理がキャンセルされる
    • LiveDataのinactiveによってキャンセルされた後、LiveDataが再度
    Activeになったときはブロックは最初から再実⾏
    • ブロックが成功したか、LiveDataがinactiveになる以外の理由でキャン
    セルになったら(Exceptionがthrowされるとそうなります)、activeに
    なっても、もう⼀度ブロックが実⾏されることはありません。

    View Slide

  55. liveData{}で
    画⾯回転、エラー時の再実⾏など
    をいい感じに実⾏してくれる!

    View Slide

  56. kotlinx:kotlinx-coroutines-test

    (GoogleとJetbrainsの共同)

    View Slide

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

    View Slide

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

    View Slide

  59. これをテストしてみよう
    class ArticleViewModel : ViewModel() {
    var repository = Repository()
    val articles: LiveData> = liveData {
    val articles = repository.articles()
    delay(1000)
    emit(articles)
    }
    delay()が⼊っているので

    簡単には出来なそう?

    View Slide

  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"))

    View Slide

  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

    View Slide

  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に⼊れ替える

    View Slide

  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する

    View Slide

  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

    View Slide

  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> { Unit }
    try {
    articles.observeForever(observer)
    advanceTimeBy(1000)
    require(articles.value == articlesData)
    } finally {
    articles.removeObserver(observer)
    }
    }
    TestCoroutineScope.runBlockingTest {} で
    coroutineの処理を使うテストを⾏う

    View Slide

  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に

    View Slide

  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としてまとめる
    (ライブラリでは提供されていない)

    View Slide

  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> { Unit }
    try {
    articles.observeForever(observer)
    advanceTimeBy(1000)
    require(articles.value == articlesData)
    } finally {
    LiveDataなどの実⾏スレッドを
    そのままのスレッドで⾏うようにする
    (androidx.arch.core:core-testing)

    View Slide

  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> { Unit }
    try {
    articles.observeForever(observer)
    advanceTimeBy(1000)
    require(articles.value == articlesData)
    } finally {
    Mockkの初期化処理

    View Slide

  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> { Unit }
    try {
    articles.observeForever(observer)
    advanceTimeBy(1000)
    require(articles.value == articlesData)
    } finally {
    articles.removeObserver(observer)
    }
    }
    observeすることでliveDataの処理が
    動く

    View Slide

  71. repository.articles()
    } returns articlesData
    val articles = articleViewModel.articles
    val observer = Observer> { Unit }
    try {
    articles.observeForever(observer)
    advanceTimeBy(1000)
    require(articles.value == articlesData)
    } finally {
    articles.removeObserver(observer)
    }
    }
    }
    class ArticleViewModel : ViewModel() {
    var repository = Repository()
    val articles: LiveData> = liveData {
    val articles = repository.articles()
    delay(1000)
    emit(articles)
    }
    delay()の分時間を進める

    View Slide

  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> { Unit }
    try {
    articles.observeForever(observer)
    advanceTimeBy(1000)
    require(articles.value == articlesData)
    } finally {
    articles.removeObserver(observer)
    }
    }
    }
    終わったらremoveObserverする

    View Slide

  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> { Unit }
    try {
    articles.observeForever(observer)
    advanceTimeBy(1000)
    require(articles.value == articlesData)
    } finally {
    articles.removeObserver(observer)
    }
    }
    }
    ボイラープレートなので
    いい感じに

    View Slide

  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)
    }
    }
    }
    いい感じに
    (ライブラリにはないので、⾃作する)

    View Slide

  75. まとめ

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  80. ありがとうございました

    View Slide