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

Coroutine-First Android Architecture -- droidco...

Rick Busarow
September 09, 2019

Coroutine-First Android Architecture -- droidcon Lisbon

Coroutines allow us to write asynchronous code in an easy-to-read, declarative style. The structured concurrency pattern allows us to write this code without fear of leaks. But there’s much more to the coroutines API than suspend modifiers and CoroutineScopes, and with these new abilities should come new patterns.

In this talk, we’ll discuss the power behind structured concurrency and how we can use it to make our entire stack lifecycle-aware. We’ll see how we can use the tools coroutines give us to inform our application architecture, so that we can quickly write maintainable and testable features at a large scale.

Rick Busarow

September 09, 2019
Tweet

More Decks by Rick Busarow

Other Decks in Programming

Transcript

  1. @rbusarow class SomeViewModel : ViewModel() { fun doSomething() { val

    job = launch { … } } } Pre- Structured Concurrency
  2. @rbusarow class SomeViewModel : ViewModel() { fun doSomething() { val

    job = launch { … } } } Pre- Structured Concurrency
  3. @rbusarow class SomeViewModel : ViewModel() { fun doSomething() { val

    job = launch { … } } } Pre- Structured Concurrency
  4. @rbusarow class SomeViewModel : ViewModel() { fun doSomething() { val

    job = launch { … } } } Pre- Structured Concurrency
  5. Job @rbusarow Job hierarchy Explicit cancellation Job Job Job Job

    Job Job Job Job Job Job .cancel() Job Job Job
  6. @rbusarow class SomeViewModel : ViewModel() { fun doSomething() { val

    job = launch { … } } } Pre- Structured Concurrency
  7. @rbusarow class SomeViewModel : ViewModel() { fun doSomething() { val

    job = launch { … } } } Pre- Structured Concurrency Completely
 Autonomous
  8. @rbusarow class SomeViewModel : ViewModel() { fun doSomething() { val

    job = launch { … } } } fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() !-> Unit ): Job { … } 0.26 to 1.0
  9. @rbusarow class SomeViewModel : ViewModel(), CoroutineScope { override val coroutineContext

    = Job() + Dispatchers.Main fun doSomething() { val job = launch { … } } } Structured Concurrency
  10. @rbusarow class SomeViewModel : ViewModel(), CoroutineScope { override val coroutineContext

    = Job() + Dispatchers.Main fun doSomething() { val job = launch { … } } override fun onCleared() { super.onCleared() coroutineContext.cancel() } } Structured Concurrency
  11. @rbusarow Androidx Lifecycle STARTED RESUMED CREATED Lifecycle C STARTED RESUMED

    CREATED Lifecycle B STARTED RESUMED CREATED Lifecycle A
  12. @rbusarow Androidx Lifecycle STARTED RESUMED CREATED Lifecycle C STARTED RESUMED

    CREATED Lifecycle B STARTED RESUMED CREATED Lifecycle A …DESTROYED? ¯\_(ツ)_/¯
  13. @rbusarow class MainViewModel : ViewModel(), CoroutineScope { override val coroutineContext:

    CoroutineContext = SupervisorJob() + Dispatchers.Main val reminder = launch { while (true) { delay(1_000) Log.v("MainViewModel", "still alive! --> ${UUID.randomUUID()}") } } override fun onCleared() { super.onCleared() cancel() } } ViewModel lifecycle
  14. @rbusarow class MainViewModel : ViewModel(), CoroutineScope { override val coroutineContext:

    CoroutineContext = SupervisorJob() + Dispatchers.Main val reminder = launch { while (true) { delay(1_000) Log.v("MainViewModel", "still alive! --> ${UUID.randomUUID()}") } } override fun onCleared() { super.onCleared() cancel() } } ViewModel lifecycle
  15. @rbusarow class MainViewModel : ViewModel(), CoroutineScope { override val coroutineContext:

    CoroutineContext = SupervisorJob() + Dispatchers.Main val reminder = launch { while (true) { delay(1_000) Log.v("MainViewModel", "still alive! --> ${UUID.randomUUID()}") } } override fun onCleared() { super.onCleared() cancel() } } ViewModel lifecycle
  16. @rbusarow class MainViewModel : ViewModel(), CoroutineScope { override val coroutineContext:

    CoroutineContext = SupervisorJob() + Dispatchers.Main val reminder = launch { while (true) { delay(1_000) Log.v("MainViewModel", "still alive! --> ${UUID.randomUUID()}") } } override fun onCleared() { super.onCleared() cancel() } } ViewModel lifecycle
  17. @rbusarow class MainViewModel : ViewModel() { val reminder = viewModelScope.launch

    { while (true) { delay(1_000) Log.v("MainViewModel", "still alive! --> ${UUID.randomUUID()}") } } } ViewModel lifecycle
  18. @rbusarow class MainViewModel : ViewModel(), CoroutineScope { override val coroutineContext:

    CoroutineContext = SupervisorJob() + Dispatchers.Main } CoroutineContext
  19. @rbusarow class MainViewModel : ViewModel(), CoroutineScope { override val coroutineContext:

    CoroutineContext = SupervisorJob() + Dispatchers.Main } CoroutineContext parent Job
  20. @rbusarow class MainViewModel : ViewModel(), CoroutineScope { override val coroutineContext:

    CoroutineContext = SupervisorJob() + Dispatchers.Main } CoroutineContext parent Job default Dispatcher
  21. @rbusarow class MainViewModel : ViewModel(), CoroutineScope { override val coroutineContext:

    CoroutineContext = SupervisorJob() + Dispatchers.Main } CoroutineContext interface CoroutineContext { val job: Job val continuationInterceptor: ContinuationInterceptor val coroutineExceptionHandler: CoroutineExceptionHandler val coroutineName: CoroutineName }
  22. @rbusarow fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart =

    CoroutineStart.DEFAULT, block: suspend CoroutineScope.() !-> Unit ): Job { val newContext = newCoroutineContext(context) val coroutine = if (start.isLazy) LazyStandaloneCoroutine(newContext, block) else StandaloneCoroutine(newContext, active = true) coroutine.start(start, coroutine, block) return coroutine } CoroutineContext
  23. @rbusarow fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart =

    CoroutineStart.DEFAULT, block: suspend CoroutineScope.() !-> Unit ): Job { val newContext = newCoroutineContext(context) val coroutine = if (start.isLazy) LazyStandaloneCoroutine(newContext, block) else StandaloneCoroutine(newContext, active = true) coroutine.start(start, coroutine, block) return coroutine } CoroutineContext
  24. @rbusarow fun CoroutineScope.newCoroutineContext( context: CoroutineContext ): CoroutineContext { val combined

    = coroutineContext + context val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined return if ( combined !!!== Dispatchers.Default !&& combined[ContinuationInterceptor] !== null ) debug + Dispatchers.Default else debug } CoroutineContext
  25. @rbusarow fun CoroutineScope.newCoroutineContext( context: CoroutineContext ): CoroutineContext { val combined

    = coroutineContext + context val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined return if ( combined !!!== Dispatchers.Default !&& combined[ContinuationInterceptor] !== null ) debug + Dispatchers.Default else debug } CoroutineContext
  26. @rbusarow public operator fun plus(context: CoroutineContext): CoroutineContext = if (context

    === EmptyCoroutineContext) this else // fast path -- avoid lambda creation context.fold(this) { acc, element -> val removed = acc.minusKey(element.key) if (removed === EmptyCoroutineContext) element else { // make sure interceptor is always last in the context (and thus is fast to get when present) val interceptor = removed[ContinuationInterceptor] if (interceptor == null) CombinedContext(removed, element) else { val left = removed.minusKey(ContinuationInterceptor) if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else CombinedContext(CombinedContext(left, element), interceptor) } } } CoroutineContext
  27. @rbusarow public operator fun plus(context: CoroutineContext): CoroutineContext = if (context

    === EmptyCoroutineContext) this else // fast path -- avoid lambda creation context.fold(this) { acc, element -> val removed = acc.minusKey(element.key) if (removed === EmptyCoroutineContext) element else { // make sure interceptor is always last in the context (and thus is fast to get when present) val interceptor = removed[ContinuationInterceptor] if (interceptor == null) CombinedContext(removed, element) else { val left = removed.minusKey(ContinuationInterceptor) if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else CombinedContext(CombinedContext(left, element), interceptor) } } } CoroutineContext
  28. @rbusarow object Dispatchers { val Default: CoroutineDispatcher val IO: CoroutineDispatcher

    val Main: CoroutineDispatcher val Unconfined: CoroutineDispatcher } Dispatchers
  29. @rbusarow object Dispatchers { val Default: CoroutineDispatcher val IO: CoroutineDispatcher

    val Main: CoroutineDispatcher val Unconfined: CoroutineDispatcher } Dispatchers CPU-bound
  30. @rbusarow object Dispatchers { val Default: CoroutineDispatcher val IO: CoroutineDispatcher

    val Main: CoroutineDispatcher val Unconfined: CoroutineDispatcher } Dispatchers CPU-bound IO-bound
  31. @rbusarow object Dispatchers { val Default: CoroutineDispatcher val IO: CoroutineDispatcher

    val Main: CoroutineDispatcher val Unconfined: CoroutineDispatcher } Dispatchers CPU-bound IO-bound UI
  32. @rbusarow object Dispatchers { val Default: CoroutineDispatcher val IO: CoroutineDispatcher

    val Main: CoroutineDispatcher val Unconfined: CoroutineDispatcher } Dispatchers CPU-bound IO-bound UI Debugging
  33. @rbusarow object Dispatchers { val Default: CoroutineDispatcher val IO: CoroutineDispatcher

    val Main: CoroutineDispatcher val Unconfined: CoroutineDispatcher } fun doSomething() = launch(Dispatchers.IO) { … } suspend fun doSomethingElse() = withContext(Dispatchers.Main) { … } Dispatchers
  34. @rbusarow object Dispatchers { val Default: CoroutineDispatcher val IO: CoroutineDispatcher

    val Main: CoroutineDispatcher val Unconfined: CoroutineDispatcher } fun doSomething() = launch(Dispatchers.IO) { … } suspend fun doSomethingElse() = withContext(Dispatchers.Main) { … } Dispatchers Test CoroutineDispatcher?
  35. @rbusarow class SomeViewModel : ViewModel(), CoroutineScope { override val coroutineContext

    = Job() + Dispatchers.Main fun doSomething() { val job = launch { … } } override fun onCleared() { super.onCleared() coroutineContext.cancel() } } CoroutineScope Injection
  36. @rbusarow class SomeViewModel(val coroutineScope: CoroutineScope) : ViewModel() { fun doSomething()

    { coroutineScope.launch { … } } override fun onCleared() { super.onCleared() coroutineScope.cancel() } } CoroutineScope Injection
  37. @rbusarow val myScope = object: CoroutineScope { override val coroutineContext

    = Job() + Dispatchers.Main } val myOtherScope = object: CoroutineScope { override val coroutineContext = Job() + Dispatchers.Default } CoroutineScope Injection
  38. @rbusarow class MainCoroutineScope: CoroutineScope { override val coroutineContext = Job()

    + Dispatchers.Main } class DefaultCoroutineScope: CoroutineScope { override val coroutineContext = Job() + Dispatchers.Default } CoroutineScope Injection
  39. @rbusarow class MainCoroutineScopeImpl: MainCoroutineScope { override val coroutineContext = Job()

    + Dispatchers.Main } class DefaultCoroutineScopeImpl: DefaultCoroutineScope { override val coroutineContext = Job() + Dispatchers.Default } interface MainCoroutineScope: CoroutineScope interface DefaultCoroutineScope: CoroutineScope CoroutineScope Injection
  40. @rbusarow class SomeViewModel(val coroutineScope: CoroutineScope) : ViewModel() { fun doSomething()

    { coroutineScope.launch { … } } override fun onCleared() { super.onCleared() coroutineScope.cancel() } } CoroutineScope Injection
  41. @rbusarow class SomeViewModel(val coroutineScope: MainCoroutineScope) : ViewModel() { fun doSomething()

    { coroutineScope.launch { … } } override fun onCleared() { super.onCleared() coroutineScope.cancel() } } CoroutineScope Injection
  42. @rbusarow class MainCoroutineScopeImpl: MainCoroutineScope { override val coroutineContext = Job()

    + Dispatchers.Main } interface MainCoroutineScope: CoroutineScope class TestMainCoroutineScope: MainCoroutineScope { override val coroutineContext = Job() + TestCoroutineDispatcher() } CoroutineScope Injection
  43. @rbusarow fun doSomething() = launch(Dispatchers.IO) { … } suspend fun

    doSomethingElse() = withContext(Dispatchers.Main) { … } CoroutineScope Injection
  44. @rbusarow interface DispatcherProvider { val default: CoroutineDispatcher val io: CoroutineDispatcher

    val main: CoroutineDispatcher val mainImmediate: CoroutineDispatcher val unconfined: CoroutineDispatcher } DispatcherProvider
  45. @rbusarow interface DispatcherProvider : CoroutineContext.Element { override val key: CoroutineContext.Key<*>

    get() = Key val default: CoroutineDispatcher val io: CoroutineDispatcher val main: CoroutineDispatcher val mainImmediate: CoroutineDispatcher val unconfined: CoroutineDispatcher companion object Key : CoroutineContext.Key<DispatcherProvider> } DispatcherProvider
  46. @rbusarow class MainCoroutineScopeImpl( dispatcherProvider: DispatcherProvider = DefaultDispatcherProvider() ): MainCoroutineScope =

    MainCoroutineScope { override val coroutineContext = Job() + dispatcherProvider.main + dispatcherProvider } DispatcherProvider
  47. @rbusarow class MainCoroutineScopeImpl( dispatcherProvider: DispatcherProvider = DefaultDispatcherProvider() ): MainCoroutineScope =

    MainCoroutineScope { override val coroutineContext = Job() + dispatcherProvider.main + dispatcherProvider } DispatcherProvider
  48. @rbusarow val CoroutineContext.dispatcherProvider: DispatcherProvider get() = get(DispatcherProvider) ?: DefaultDispatcherProvider() val

    CoroutineScope.mainDispatcher: CoroutineDispatcher get() = dispatcherProvider.main DispatcherProvider
  49. @rbusarow val CoroutineContext.dispatcherProvider: DispatcherProvider get() = get(DispatcherProvider) ?: DefaultDispatcherProvider() val

    CoroutineScope.mainDispatcher: CoroutineDispatcher get() = dispatcherProvider.main DispatcherProvider
  50. @rbusarow class SomeViewModel(val coroutineScope: MainCoroutineScope) : ViewModel() { fun doSomething()

    { coroutineScope.launch(Dispatchers.Main) { … } } override fun onCleared() { super.onCleared() coroutineScope.cancel() } } CoroutineScope Injection
  51. @rbusarow class SomeViewModel(val coroutineScope: MainCoroutineScope) : ViewModel() { fun doSomething()

    { coroutineScope.launch(coroutineScope.mainDispatcher) { … } } override fun onCleared() { super.onCleared() coroutineScope.cancel() } } CoroutineScope Injection
  52. @rbusarow class TestDispatcherProvider( override val default: TestCoroutineDispatcher = TestCoroutineDispatcher(), override

    val io: TestCoroutineDispatcher = TestCoroutineDispatcher(), override val main: TestCoroutineDispatcher = TestCoroutineDispatcher(), override val mainImmediate: TestCoroutineDispatcher = TestCoroutineDispatcher(), override val unconfined: TestCoroutineDispatcher = TestCoroutineDispatcher() ) : DispatcherProvider TestDispatcherProvider
  53. @rbusarow class TestDispatcherProvider( override val default: TestCoroutineDispatcher = TestCoroutineDispatcher(), override

    val io: TestCoroutineDispatcher = TestCoroutineDispatcher(), override val main: TestCoroutineDispatcher = TestCoroutineDispatcher(), override val mainImmediate: TestCoroutineDispatcher = TestCoroutineDispatcher(), override val unconfined: TestCoroutineDispatcher = TestCoroutineDispatcher() ) : DispatcherProvider fun TestDispatcherProvider(dispatcher: TestCoroutineDispatcher): TestDispatcherProvider = TestDispatcherProvider( default = dispatcher, io = dispatcher, main = dispatcher, mainImmediate = dispatcher, unconfined = dispatcher ) TestDispatcherProvider
  54. @rbusarow class TestProvidedCoroutineScopeImpl( override val dispatcherProvider: DispatcherProvider, context: CoroutineContext =

    EmptyCoroutineContext ) : TestProvidedCoroutineScope, TestCoroutineScope by TestCoroutineScope(context + dispatcherProvider) TestDispatcherProvider
  55. @rbusarow class TestProvidedCoroutineScopeImpl( override val dispatcherProvider: DispatcherProvider, context: CoroutineContext =

    EmptyCoroutineContext ) : TestProvidedCoroutineScope, TestCoroutineScope by TestCoroutineScope(context + dispatcherProvider) fun TestProvidedCoroutineScope( dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher(), dispatcherProvider: TestDispatcherProvider = TestDispatcherProvider(dispatcher), context: CoroutineContext = EmptyCoroutineContext ): TestProvidedCoroutineScope = TestProvidedCoroutineScopeImpl( dispatcherProvider = dispatcherProvider, context = context + dispatcher ) TestDispatcherProvider
  56. @rbusarow class TestProvidedCoroutineScopeImpl( override val dispatcherProvider: DispatcherProvider, context: CoroutineContext =

    EmptyCoroutineContext ) : TestProvidedCoroutineScope, TestCoroutineScope by TestCoroutineScope(context + dispatcherProvider) fun TestProvidedCoroutineScope( dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher(), dispatcherProvider: TestDispatcherProvider = TestDispatcherProvider(dispatcher), context: CoroutineContext = EmptyCoroutineContext ): TestProvidedCoroutineScope = TestProvidedCoroutineScopeImpl( dispatcherProvider = dispatcherProvider, context = context + dispatcher ) val myScope = TestProvidedCoroutineScope() TestDispatcherProvider
  57. @rbusarow class SomeViewModel(val coroutineScope: MainCoroutineScope) : ViewModel() { fun doSomething()

    { coroutineScope.launch(coroutineScope.mainDispatcher) { … } } override fun onCleared() { super.onCleared() coroutineScope.cancel() } } CoroutineScope Injection
  58. @rbusarow class SomeViewModel(val coroutineScope: MainCoroutineScope) : ViewModel() { fun doSomething()

    { coroutineScope.launch(coroutineScope.mainDispatcher) { … } } override fun onCleared() { super.onCleared() coroutineScope.cancel() } } CoroutineScope Injection
  59. @rbusarow class SomeRepository { suspend fun doSomething() = withContext(Dispatchers.IO) {

    ... } } val CoroutineContext.dispatcherProvider: DispatcherProvider get() = get(DispatcherProvider) ?: DefaultDispatcherProvider() CoroutineScope Injection
  60. @rbusarow class SomeRepository { suspend fun doSomething() = withContext(Dispatchers.IO) {

    ... } } val CoroutineContext.dispatcherProvider: DispatcherProvider get() = get(DispatcherProvider) ?: DefaultDispatcherProvider() suspend fun <T> withIO( context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T ): T { val newContext = coroutineContext.dispatcherProvider.io + context return withContext(newContext, block) } CoroutineScope Injection
  61. @rbusarow class SomeRepository { suspend fun doSomething() = withIO {

    ... } } val CoroutineContext.dispatcherProvider: DispatcherProvider get() = get(DispatcherProvider) ?: DefaultDispatcherProvider() suspend fun <T> withIO( context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T ): T { val newContext = coroutineContext.dispatcherProvider.io + context return withContext(newContext, block) } CoroutineScope Injection