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

Avatar for takahirom

takahirom

May 21, 2019
Tweet

More Decks by takahirom

Other Decks in Programming

Transcript

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

    • Room • Compose • kotlinx:kotlinx-coroutines-test
 (GoogleとJetbrainsの共同) 今⽇話すこと • 今回は基本的なところは省略します!
  2. 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
  3. liveData{}のKDocに書かれているこ と • LiveDataがactive(onStartの後)になったときに引数のブロックが実⾏ される • LiveDataがinactiveになったとき再度activeにならずに時間が経過した らブロックの処理がキャンセルされる • LiveDataのinactiveによってキャンセルされた後、LiveDataが再度

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

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

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

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

    Activeになったときはブロックは最初から再実⾏ • ブロックが成功したか、LiveDataがinactiveになる以外の理由でキャン セルになったら(Exceptionがthrowされるとそうなります)、activeに なっても、もう⼀度ブロックが実⾏されることはありません。
  8. これをテストしてみよう class ArticleViewModel : ViewModel() { var repository = Repository()

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

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

    val articles: LiveData<List<Article>> = liveData { val articles = repository.articles() delay(1000) emit(articles) } delay()が⼊っているので
 簡単には出来なそう?
  11. 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"))
  12. 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
  13. 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に⼊れ替える
  14. 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する
  15. 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
  16. @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の処理を使うテストを⾏う
  17. 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に
  18. 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としてまとめる (ライブラリでは提供されていない)
  19. 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)
  20. 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の初期化処理
  21. @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の処理が 動く
  22. 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()の分時間を進める
  23. 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する
  24. @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) } } } ボイラープレートなので いい感じに
  25. @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) } } } いい感じに (ライブラリにはないので、⾃作する)
  26. 参考 • 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