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. 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