Coroutine-First Android Architecture -- droidcon Lisbon

0c062ecd54ead5cce8b2b79acbe557d8?s=47 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.

0c062ecd54ead5cce8b2b79acbe557d8?s=128

Rick Busarow

September 09, 2019
Tweet

Transcript

  1. rbusarow COROUTINE-FIRST ANDROID ARCHITECTURE Rick Busarow Photo by Flipboard on

    Unsplash
  2. @rbusarow Coroutines First

  3. 12 SEPTEMBER 2018 @rbusarow

  4. 12 SEPTEMBER 2018 @rbusarow

  5. http://bit.ly/Structured-Concurrency-Article 12 SEPTEMBER 2018 @rbusarow

  6. 12 SEPTEMBER 2018 @rbusarow http://bit.ly/Structured-Concurrency-GitHub

  7. @rbusarow Structured Concurrency

  8. @rbusarow Job hierarchy

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

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

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

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

    job = launch { … } } } Pre- Structured Concurrency
  13. @rbusarow Job hierarchy Job

  14. @rbusarow Job hierarchy Job Job Job Job Job Job Job

    Job Job Job Job
  15. Job @rbusarow Job hierarchy Explicit cancellation Job Job Job Job

    Job Job Job Job Job Job Job Job Job
  16. Job @rbusarow Job hierarchy Explicit cancellation Job Job Job Job

    Job Job Job Job Job Job .cancel() Job Job Job
  17. Job @rbusarow Job hierarchy Explicit cancellation Job Job Job Job

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

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

    job = launch { … } } } Pre- Structured Concurrency Completely
 Autonomous
  20. @rbusarow 0.26 to 1.0

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

    job = launch { … } } } 0.26 to 1.0
  22. @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
  23. @rbusarow class SomeViewModel : ViewModel(), CoroutineScope { override val coroutineContext

    = Job() + Dispatchers.Main fun doSomething() { val job = launch { … } } } Structured Concurrency
  24. @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
  25. @rbusarow Lifecycle

  26. @rbusarow Androidx Lifecycle

  27. @rbusarow Androidx Lifecycle

  28. @rbusarow Androidx Lifecycle INITIALIZED STARTED RESUMED CREATED

  29. @rbusarow Androidx Lifecycle INITIALIZED STARTED RESUMED CREATED STARTED CREATED DESTROYED

  30. @rbusarow Androidx Lifecycle INITIALIZED STARTED RESUMED CREATED STARTED CREATED DESTROYED

    viewModel init viewModel.onCleared()
  31. @rbusarow Androidx Lifecycle STARTED RESUMED CREATED

  32. @rbusarow Androidx Lifecycle STARTED RESUMED CREATED onCreate()

  33. @rbusarow Androidx Lifecycle STARTED RESUMED CREATED onStart()

  34. @rbusarow Androidx Lifecycle STARTED RESUMED CREATED onResume()

  35. @rbusarow Androidx Lifecycle STARTED RESUMED CREATED Lifecycle B STARTED RESUMED

    CREATED Lifecycle A
  36. @rbusarow Androidx Lifecycle STARTED RESUMED CREATED Lifecycle C STARTED RESUMED

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

    CREATED Lifecycle B STARTED RESUMED CREATED Lifecycle A …DESTROYED? ¯\_(ツ)_/¯
  38. @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
  39. @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
  40. @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
  41. None
  42. None
  43. @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
  44. @rbusarow class MainViewModel : ViewModel() { val reminder = viewModelScope.launch

    { while (true) { delay(1_000) Log.v("MainViewModel", "still alive! --> ${UUID.randomUUID()}") } } } ViewModel lifecycle
  45. @rbusarow CoroutineContext

  46. @rbusarow class MainViewModel : ViewModel(), CoroutineScope { override val coroutineContext:

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

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

    CoroutineContext = SupervisorJob() + Dispatchers.Main } CoroutineContext parent Job default Dispatcher
  49. @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 }
  50. @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
  51. @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
  52. @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
  53. @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
  54. @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
  55. @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
  56. @rbusarow ViewModel lifecycle http://bit.ly/Demystifying-CoroutineContext

  57. @rbusarow Dispatchers

  58. @rbusarow object Dispatchers { val Default: CoroutineDispatcher val IO: CoroutineDispatcher

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

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

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

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

    val Main: CoroutineDispatcher val Unconfined: CoroutineDispatcher } Dispatchers CPU-bound IO-bound UI Debugging
  63. @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
  64. @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?
  65. @rbusarow TestCoroutineDispatcher

  66. @rbusarow advanceTimeBy() advanceUntilIdle() pauseDispatcher() resumeDispatcher() cleanupTestCoroutines() TestCoroutineDispatcher

  67. @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
  68. @rbusarow class SomeViewModel(val coroutineScope: CoroutineScope) : ViewModel() { fun doSomething()

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

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

    + Dispatchers.Main } class DefaultCoroutineScope: CoroutineScope { override val coroutineContext = Job() + Dispatchers.Default } CoroutineScope Injection
  71. @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
  72. @rbusarow class SomeViewModel(val coroutineScope: CoroutineScope) : ViewModel() { fun doSomething()

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

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

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

    doSomethingElse() = withContext(Dispatchers.Main) { … } CoroutineScope Injection
  76. @rbusarow No hard-coded Dispatchers

  77. @rbusarow interface DispatcherProvider { val default: CoroutineDispatcher val io: CoroutineDispatcher

    val main: CoroutineDispatcher val mainImmediate: CoroutineDispatcher val unconfined: CoroutineDispatcher } DispatcherProvider
  78. @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
  79. @rbusarow class MainCoroutineScope: CoroutineScope { override val coroutineContext = Job()

    + Dispatchers.Main } DispatcherProvider
  80. @rbusarow class MainCoroutineScopeImpl( dispatcherProvider: DispatcherProvider = DefaultDispatcherProvider() ): MainCoroutineScope =

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

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

  83. @rbusarow val CoroutineContext.dispatcherProvider: DispatcherProvider get() = get(DispatcherProvider) ?: DefaultDispatcherProvider() val

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

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

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

    { coroutineScope.launch(coroutineScope.mainDispatcher) { … } } override fun onCleared() { super.onCleared() coroutineScope.cancel() } } CoroutineScope Injection
  87. @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
  88. @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
  89. @rbusarow interface TestProvidedCoroutineScope : TestCoroutineScope, DefaultCoroutineScope, IOCoroutineScope, MainCoroutineScope, MainImmediateCoroutineScope, UnconfinedCoroutineScope

    { val dispatcherProvider: DispatcherProvider } TestDispatcherProvider
  90. @rbusarow class TestProvidedCoroutineScopeImpl( override val dispatcherProvider: DispatcherProvider, context: CoroutineContext =

    EmptyCoroutineContext ) : TestProvidedCoroutineScope, TestCoroutineScope by TestCoroutineScope(context + dispatcherProvider) TestDispatcherProvider
  91. @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
  92. @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
  93. @rbusarow class SomeViewModel(val coroutineScope: MainCoroutineScope) : ViewModel() { fun doSomething()

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

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

    ... } } CoroutineScope Injection
  96. @rbusarow class SomeRepository { suspend fun doSomething() = withContext(Dispatchers.IO) {

    ... } } val CoroutineContext.dispatcherProvider: DispatcherProvider get() = get(DispatcherProvider) ?: DefaultDispatcherProvider() CoroutineScope Injection
  97. @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
  98. @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
  99. @rbusarow github.com/RBusarow/DispatcherProvider

  100. rbusarow rbusarow rbusarow THANKS! Rick Busarow rickbusarow@gmail.com