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

Coroutine-first Android Architecture

Coroutine-first Android Architecture

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

July 17, 2019
Tweet

More Decks by Rick Busarow

Other Decks in Programming

Transcript

  1. class SomeViewModel(private val repository: SomeRepository) { private val _liveData =

    MutableLiveData<String>() val liveData: LiveData<String> = _liveData }
  2. class SomeViewModel(private val repository: SomeRepository) { private val _liveData =

    MutableLiveData<String>().apply { value = repository.getTheWindsOfWinter() } val liveData: LiveData<String> = _liveData }
  3. class SomeScreen : LifecycleOwner { val viewModel: SomeViewModel by lazy

    { … } fun onCreate() { viewModel.liveData.observe(this, Observer { str !-> tvSomeTextView.text = str }) } }
  4. class SomeRepository(val api: Api) { fun getTheWindsOfWinter(): LiveData<String> { return

    MutableLiveData<String>().apply { Thread.sleep(LONG_TIME) value = api.getTwow() } } }
  5. class SomeViewModel(private val repository: SomeRepository) { private val _liveData =

    MutableLiveData<String>().apply { value = repository.getTheWindsOfWinter() } val liveData: LiveData<String> = _liveData }
  6. class SomeRepository(val api: Api) { fun getTheWindsOfWinter(): LiveData<String> { return

    MutableLiveData<String>().apply { Thread.sleep(LONG_TIME) value = api.getTwow() } } }
  7. class SomeRepository(val api: Api) { fun getTheWindsOfWinter(): LiveData<String> { return

    MutableLiveData<String>().apply { api.getTwow { value = it } } } }
  8. class SomeRepository(val api: Api) { fun getTheWindsOfWinter(): LiveData<String> { return

    MutableLiveData<String>().apply { api.getTwow { value = it } } } }
  9. class SomeRepository(val api: Api) { fun getTheWindsOfWinter(): LiveData<String> { return

    MutableLiveData<String>().apply { api.getTwow { postValue(it) } } } }
  10. class SomeViewModel(private val repository: SomeRepository) { val liveData: LiveData<List<String!>> =

    Transformations.map(repository.getTheWindsOfWinter()) { novel !-> novel!!!.split("half a hundred") } }
  11. class SomeViewModel(private val repository: SomeRepository) { val liveData: LiveData<List<String!>> =

    Transformations.map(repository.getTheWindsOfWinter()) { novel !-> novel!!!.split("half a hundred") } }
  12. class SomeViewModel( private val repository: SomeRepository, private val coroutineScope: CoroutineScope

    ) { private val _liveData = MutableLiveData<List<String!>>() val liveData: LiveData<List<String!>> = _liveData val transform: LiveData<Unit> = Transformations.map(repository.getTheWindsOfWinter()) { novel !-> coroutineScope.launch { _liveData.postValue(novel!!!.split("half a hundred”)) } } }
  13. class SomeViewModel( private val repository: SomeRepository, private val coroutineScope: CoroutineScope

    ) { private val _liveData = MutableLiveData<List<String!>>() val liveData: LiveData<List<String!>> = _liveData val transform: LiveData<Unit> = Transformations.map(repository.getTheWindsOfWinter()) { novel !-> coroutineScope.launch { _liveData.postValue(novel!!!.split("half a hundred”)) } } }
  14. class SomeScreen : LifecycleOwner { val viewModel: SomeViewModel by lazy

    { … } fun onCreate() { viewModel.liveData.observe(this, Observer { str !-> tvSomeTextView.text = str }) } }
  15. class SomeScreen : LifecycleOwner { val viewModel: SomeViewModel by lazy

    { … } fun onCreate() { viewModel.transform.observe(this, Observer { }) viewModel.liveData.observe(this, Observer { str !-> tvSomeTextView.text = str }) } }
  16. class SomeViewModel( private val repository: SomeRepository, private val coroutineScope: CoroutineScope

    ) { val liveData: LiveData<List<String!>> = Transformations.switchMap( repository.getTheWindsOfWinter() ) { full !-> liveData(Default) { val split = full.split("half a hundred") emit(split) } } } lifecycle-livedata-ktx:2.2.0-alpha02
  17. class SomeViewModel( private val repository: SomeRepository, private val coroutineScope: CoroutineScope

    ) { val liveData: LiveData<List<String!>> = Transformations.switchMap( repository.getTheWindsOfWinter() ) { full !-> liveData(Default) { val split = full.split("half a hundred") emit(split) } } } lifecycle-livedata-ktx:2.2.0-alpha02
  18. class SomeViewModel( private val repository: SomeRepository, private val coroutineScope: CoroutineScope

    ) { val liveData: LiveData<List<String!>> = Transformations.switchMap( repository.getTheWindsOfWinter() ) { full !-> liveData(Default) { val split = full.split("half a hundred") emit(split) } } } lifecycle-livedata-ktx:2.2.0-alpha02
  19. class SomeViewModel( private val repository: SomeRepository, private val coroutineScope: CoroutineScope

    ) { val liveData: LiveData<List<String!>> = repository.getTheWindsOfWinter().switchMap { full !-> liveData(Default) { val split = full.split("half a hundred") emit(split) } } } lifecycle-livedata-ktx:2.2.0-alpha02
  20. LiveData ‣ Always tied to the main thread ‣ Requires

    LifecycleOwner ‣ Observer-heavy ‣ Written by George R. R. Martin
  21. 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 }
  22. 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 }
  23. 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 }
  24. 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 }
  25. suspend fun <T> withContext( context: CoroutineContext, block: suspend CoroutineScope.() !->

    T ): T = suspendCoroutineUninterceptedOrReturn sc@ { continuation !-> val oldContext = continuation.context … }
  26. suspend fun <T> withContext( context: CoroutineContext, block: suspend CoroutineScope.() !->

    T ): T = suspendCoroutineUninterceptedOrReturn sc@ { continuation !-> val oldContext = continuation.context … }
  27. CoroutineScope CoroutineContext interface CoroutineContext { val job: Job val continuationInterceptor:

    ContinuationInterceptor val coroutineExceptionHandler: CoroutineExceptionHandler val coroutineName: CoroutineName }
  28. CoroutineScope CoroutineContext interface CoroutineContext { val job: Job val continuationInterceptor:

    ContinuationInterceptor val coroutineExceptionHandler: CoroutineExceptionHandler val coroutineName: CoroutineName }
  29. class SomeClass : CoroutineScope { val A = Job() override

    val coroutineContext = A fun someFunction() { val B = launch { val D = launch {} val E = launch {} } val C = launch { val F = launch {} } } }
  30. class SomeClass : CoroutineScope { val A = Job() override

    val coroutineContext = A fun someFunction() { val B = launch { val D = launch {} val E = launch {} } val C = launch { val F = launch {} } } }
  31. class SomeClass : CoroutineScope { val A = Job() override

    val coroutineContext = A fun someFunction() { val B = launch { val D = launch {} val E = launch {} } val C = launch { val F = launch {} } } }
  32. A

  33. A B

  34. class SomeClass : CoroutineScope { val A = Job() override

    val coroutineContext = A fun someFunction() { val B = launch { val D = launch {} val E = launch {} } val C = launch { val F = launch {} } } }
  35. class SomeClass : CoroutineScope { val A = Job() override

    val coroutineContext = A fun someFunction() { val B = launch { val D = launch {} val E = launch {} } val C = launch { val F = launch {} } } }
  36. class SomeClass : CoroutineScope { val A = SupervisorJob() override

    val coroutineContext = A fun someFunction() { val B = launch { val D = launch {} val E = launch {} } val C = launch { val F = launch {} } } }
  37. class SomeClass : CoroutineScope { val A = SupervisorJob() override

    val coroutineContext = A fun someFunction() { val B = launch { val D = launch {} val E = launch {} } val C = launch { val F = launch {} } } }
  38. class FileParser(file: File) : CoroutineScope { override val coroutineContext =

    Job() + Dispatchers.IO suspend fun parse(): String = TODO() }
  39. class FileParser(file: File) : CoroutineScope { override val coroutineContext =

    Job() + Dispatchers.IO suspend fun parse(): String = TODO() } class SomeOtherClass { var novel: String? = null suspend fun onUpdatedFile(file: File) { novel = FileParser(file).parse() } }
  40. class FileParser(file: File) : CoroutineScope { override val coroutineContext =

    Job() + Dispatchers.IO suspend fun parse(): String = TODO() } class SomeOtherClass { var novel: String? = null suspend fun onUpdatedFile(file: File) { FileParser(file).launch { … } } }
  41. class FileParser(file: File) { private val scope = CoroutineScope(Job() +

    Dispatchers.IO) suspend fun parse(): String = TODO() } class SomeOtherClass { var novel: String? = null suspend fun onUpdatedFile(file: File) { FileParser(file).launch { … } } }
  42. fun SupervisorScope( continuationInterceptor: ContinuationInterceptor = Dispatchers.Default ): CoroutineScope = CoroutineScope(SupervisorJob()

    + continuationInterceptor) fun DefaultCoroutineScope() = SupervisorScope(Dispatchers.Default) fun IOCoroutineScope() = SupervisorScope(Dispatchers.IO)
  43. Channel ‣ Observable type ‣ Linked blocking queue ‣ Thread

    Concurrency-safe ‣ Synchronization primitive ‣ Sender and receiver have their own CoroutineScope
  44. suspend fun talkToYourself() { val channel = Channel<Int>() scope.launch {

    repeat(5) { channel.send(it) } } val output = mutableListOf<Int>() for (i in channel) { output.add(i) } output shouldBe listOf(0, 1, 2, 3, 5) }
  45. suspend fun talkToYourself() { val channel = Channel<Int>() scope.launch {

    repeat(5) { channel.send(it) } } val output = mutableListOf<Int>() for (i in channel) { output.add(i) } output shouldBe listOf(0, 1, 2, 3, 5) }
  46. suspend fun talkToYourself() { val channel = Channel<Int>() scope.launch {

    repeat(5) { channel.send(it) } } val output = mutableListOf<Int>() for (i in channel) { output.add(i) } output shouldBe listOf(0, 1, 2, 3, 5) }
  47. suspend fun talkToYourself() { val channel = Channel<Int>() scope.launch {

    repeat(5) { channel.send(it) } } val output = mutableListOf<Int>() for (i in channel) { output.add(i) } output shouldBe listOf(0, 1, 2, 3, 5) }
  48. suspend fun talkToYourself() { val channel = Channel<Int>() scope.launch {

    repeat(5) { channel.send(it) } } val output = mutableListOf<Int>() for (i in channel) { output.add(i) } output shouldBe listOf(0, 1, 2, 3, 5) } ORDER OF OPERATIONS Send 0 Receive 0 Send 1 Receive 1 …
  49. fun <T> ReceiveChannel<T>.observe( coroutineScope: CoroutineScope, observer: CoroutineScope.(t: T) !-> Unit

    ): Job = coroutineScope.launch { for (t in this@observe) { observer(t) } }
  50. fun <T> ReceiveChannel<T>.observe( coroutineScope: CoroutineScope, observer: CoroutineScope.(t: T) !-> Unit

    ): Job = coroutineScope.launch { for (t in this@observe) { observer(t) } }
  51. fun <T> ReceiveChannel<T>.observe( coroutineScope: CoroutineScope, observer: CoroutineScope.(t: T) !-> Unit

    ): Job = coroutineScope.launch { for (t in this@observe) { observer(t) } }
  52. fun <T> ReceiveChannel<T>.observe( coroutineScope: CoroutineScope, observer: CoroutineScope.(t: T) !-> Unit

    ): Job = coroutineScope.launch { for (t in this@observe) { observer(t) } }
  53. class SomeClass( val someRepository: SomeRepository, val scope: CoroutineScope ) {

    init { someRepository.channel.observe(scope) { emitted !-> println(emitted) } } }
  54. class SomeClass( val someRepository: SomeRepository, val scope: CoroutineScope ) {

    init { someRepository.channel.observe(scope) { emitted !-> println(emitted) } } }
  55. fun Screen.whileResumed(block: CoroutineScope.() !-> Job) { var job: Job? =

    null val observer = object : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun onResume() { job = block() } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun onPause() { job!?.cancel() } } lifecycle.addObserver(observer) }
  56. fun Screen.whileResumed(block: CoroutineScope.() !-> Job) { var job: Job? =

    null val observer = object : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun onResume() { job = block() } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun onPause() { job!?.cancel() } } lifecycle.addObserver(observer) }
  57. fun Screen.whileResumed(block: CoroutineScope.() !-> Job) { var job: Job? =

    null val observer = object : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun onResume() { job = block() } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun onPause() { job!?.cancel() } } lifecycle.addObserver(observer) }
  58. fun Screen.whileResumed(block: CoroutineScope.() !-> Job) { var job: Job? =

    null val observer = object : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun onResume() { job = block() } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun onPause() { job!?.cancel() } } lifecycle.addObserver(observer) }
  59. fun Screen.whileResumed(block: CoroutineScope.() !-> Job) { var job: Job? =

    null val observer = object : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun onResume() { job = block() } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun onPause() { job!?.cancel() } } lifecycle.addObserver(observer) }
  60. fun Screen.whileResumed(block: CoroutineScope.() !-> Job) { var job: Job? =

    null val observer = object : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun onResume() { job = block() } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun onPause() { job!?.cancel() } } lifecycle.addObserver(observer) }
  61. fun Screen.whileResumed(block: CoroutineScope.() !-> Job) { var job: Job? =

    null val observer = object : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun onResume() { job = block() } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun onPause() { job!?.cancel() } } lifecycle.addObserver(observer) }
  62. fun getUserProfile(): Deferred<UserProfile?> { val deferred = CompletableDeferred<UserProfile?>() api.getUserProfile() .enqueue(object

    : Callback<UserProfile> { override fun onResponse(call: Call<UserProfile>, response: Response<UserProfile>) { deferred.complete(response.body()) } override fun onFailure(call: Call<UserProfile>, t: Throwable) { deferred.complete(null) } }) return deferred }
  63. fun getUserProfile(): Deferred<UserProfile?> { val deferred = CompletableDeferred<UserProfile?>() api.getUserProfile() .enqueue(object

    : Callback<UserProfile> { override fun onResponse(call: Call<UserProfile>, response: Response<UserProfile>) { deferred.complete(response.body()) } override fun onFailure(call: Call<UserProfile>, t: Throwable) { deferred.complete(null) } }) return deferred }
  64. fun getUserProfile(): Deferred<UserProfile?> { val deferred = CompletableDeferred<UserProfile?>() api.getUserProfile() .enqueue(object

    : Callback<UserProfile> { override fun onResponse(call: Call<UserProfile>, response: Response<UserProfile>) { deferred.complete(response.body()) } override fun onFailure(call: Call<UserProfile>, t: Throwable) { deferred.complete(null) } }) return deferred }
  65. fun getUserProfile(): Deferred<UserProfile?> { val deferred = CompletableDeferred<UserProfile?>() api.getUserProfile() .enqueue(object

    : Callback<UserProfile> { override fun onResponse(call: Call<UserProfile>, response: Response<UserProfile>) { deferred.complete(response.body()) } override fun onFailure(call: Call<UserProfile>, t: Throwable) { deferred.complete(null) } }) return deferred }
  66. fun getUserProfile(): Deferred<UserProfile?> { val deferred = CompletableDeferred<UserProfile?>() api.getUserProfile() .enqueue(object

    : Callback<UserProfile> { override fun onResponse(call: Call<UserProfile>, response: Response<UserProfile>) { deferred.complete(response.body()) } override fun onFailure(call: Call<UserProfile>, t: Throwable) { deferred.complete(null) } }) return deferred }
  67. class SomeClass(val repository: ProfileRepository) { var profile: UserProfile? = null

    suspend fun updateProfile() { profile = repository.getUserProfile().await() } }
  68. class SomeClass(val repository: ProfileRepository) { var profile: UserProfile? = null

    suspend fun updateProfile() { profile = repository.getUserProfile().await() } }