Slide 1

Slide 1 text

What's new in Kotlin Coroutines on Android takahirom (mixi & CA).aab 〜Google I/O報告会〜

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

Kotlin Coroutinesの対応を みていこう

Slide 5

Slide 5 text

Kotlin Coroutinesの対応 • WorkManager • Lifecycle • LiveData • ViewModel • Room • Compose • kotlinx:kotlinx-coroutines-test
 (GoogleとJetbrainsの共同) 今⽇話すこと • 今回は基本的なところは省略します!

Slide 6

Slide 6 text

WorkManager (Stable)

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

全体

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

liveData{}が呼ばれる

Slide 31

Slide 31 text

liveData{}は CoroutineLiveDataを返す

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

blockRunner.cancel()を呼ぶ

Slide 39

Slide 39 text

BlockRunner.cancel()が呼ばれる

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

No content

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

No content

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

onDoneでblockRunnerがnullになる

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

これをテストしてみよう class ArticleViewModel : ViewModel() { var repository = Repository() val articles: LiveData> = liveData { val articles = repository.articles() delay(1000) emit(articles) } delay()が⼊っているので
 簡単には出来なそう?

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

@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の処理を使うテストを⾏う

Slide 66

Slide 66 text

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に

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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)

Slide 69

Slide 69 text

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の初期化処理

Slide 70

Slide 70 text

@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の処理が 動く

Slide 71

Slide 71 text

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()の分時間を進める

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

@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) } } } ボイラープレートなので いい感じに

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

まとめ

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

参考 • 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

Slide 80

Slide 80 text

ありがとうございました