Coroutining Android Apps

Coroutining Android Apps

Kotlin Coroutines is the most trending solution for asynchrony programming in Android development for now. Developers of many existed apps already add them and new apps use Coroutines instead of RxJava by deault.

But how to make that work with Coroutines the most efficient and get the most effective architecture? Kirill has few rules and best practices of how to do that.

2aec47eb9a940c619f05972f0db5aa00?s=128

Kirill Rozov

May 23, 2019
Tweet

Transcript

  1. Coroutining Android Apps Kirill Rozov@EPAM

  2. Kirill Rozov krl.rozov@gmail.com Community Bridger Accelerators Lead @kirill_rozov

  3. Android Broadcast News for Android Developers

  4. 1.0 - November 18, 2014 RxJava

  5. RxJava Issues • Usage of RxJava for simple asynchronous operations

    • RxJava in every app layer • Complex sequences • Not meaningful operators name • Debug is pain, stack traces don't make much sense • Too many object allocations • Observable cancelation is managed by developer • No built-in solution for subscriptions management
  6. None
  7. Don’t block Keep moving otlin 1.3 COROUTINES

  8. Understand Kotlin Coroutines on Android youtu.be/BOHK_w09pVA?t=294

  9. suspend != blocking

  10. Long Task Thread suspend != blocking work work block t

    Java Thread Coroutine work work suspend
  11. work work block Java Thread Coroutine work work suspend Long

    Task Thread t suspend != blocking work
  12. work work block Java Thread Coroutine work work suspend work

    Long Task Thread t suspend != blocking work
  13. Coroutine Thread suspend != blocking work work suspend Dispatcher

  14. suspend Thread suspend != blocking work work Coroutine Dispatcher

  15. Structured Concurrency + Lifecycle = ❤

  16. Structured Concurrency + Lifecycle = ❤ class MainActivity : AppCompatActivity()

    { override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) // Start async operation for UI } override fun onDestroy() { super.onDestroy() // Cancel async operation for UI } }
  17. Structured Concurrency + Lifecycle = ❤ class MainViewModel : ViewModel()

    { init { // Start async operation for UI } override fun onCleared() { super.onCleared() // Cancel async operation for UI } }
  18. Structured Concurrency + Lifecycle = ❤ abstract class CoroutineViewModel :

    ViewModel(), CoroutineScope { override val coroutineContext = SupervisorJob() override fun onCleared() { super.onCleared() cancel() } }
  19. Structured Concurrency + Lifecycle = ❤ CoroutineScope

  20. Structured Concurrency + Lifecycle = ❤ override val coroutineContext =

    SupervisorJob()
  21. Structured Concurrency + Lifecycle = ❤ cancel()

  22. Structured Concurrency + Lifecycle = ❤ class DetailsViewModel(val repository: Repository)

    : CoroutineViewModel() { fun initData() { launch { val data = repository.loadData() if (isActive) { // or yield() liveData.value = data } } } }
  23. Structured Concurrency + Lifecycle = ❤ viewModelScope.launch

  24. Structured Concurrency + Lifecycle = ❤ class LifecycleCoroutineScope( lifecycle: Lifecycle,

    context: CoroutineContext = EmptyCoroutineContext ) : CoroutineScope, LifecycleObserver { override val coroutineContext = context + SupervisorJob() init { lifecycle.addObserver(this) } @OnLifecycleEvent(ON_DESTROY) fun cancel() { lifecycle.removeObserver(this) coroutineContext.cancel() } }
  25. Structured Concurrency + Lifecycle = ❤ fun FragmentActivity.newCoroutineScope( context: CoroutineContext

    = EmptyCoroutineContext ): CoroutineScope { return LifecycleCoroutineScope( lifecycle, Dispatchers.Main + context ) }
  26. Structured Concurrency + Lifecycle = ❤ private val coroutineScope =

    newCoroutineScope() … coroutineScope.launch { // Start async operation for UI }
  27. Choose CoroutineScope properly

  28. Choose CoroutineScope properly class MessagesViewModel( val repository: Repository ) :

    CoroutineViewModel() { fun sendMessage(message: String) { launch { repository.sendMessage(message) } } }
  29. Choose CoroutineScope properly launch { repository.sendMessage(message) }

  30. Choose CoroutineScope properly launch(Job())

  31. Choose CoroutineScope properly GlobalScope.launch

  32. Choose CoroutineScope properly fun coroutineScope launch

  33. Choose CoroutineScope properly CoroutineScope(coroutineContext + Job()).launch

  34. Default Dispatcher for CoroutineScope

  35. Default Dispatcher for CoroutineScope launch(Dispatchers.Main)

  36. Default Dispatcher for CoroutineScope override val coroutineContext = SupervisorJob()

  37. Default Dispatcher for CoroutineScope override val coroutineContext = SupervisorJob() +

    Dispatchers.Main
  38. Default Dispatcher for CoroutineScope launch(Dispatchers.Main)

  39. Default Dispatcher for CoroutineScope launch

  40. Prefer withContext() for switch CoroutineContext

  41. Prefer withContext() for switch CoroutineContext launch { val dataDeferred =

    async(Dispatchers.IO) { repository.loadData() } val data = dataDeferred.await() }
  42. Prefer withContext() for switch CoroutineContext val dataDeferred = async(Dispatchers.IO)

  43. Prefer withContext() for switch CoroutineContext dataDeferred.await()

  44. Prefer withContext() for switch CoroutineContext = withContext(Dispatchers.IO)

  45. Prefer withContext() for switch CoroutineContext launch { val data1Deferred =

    async(Dispatchers.IO) { repository.loadData1() } val data2Deferred = async(Dispatchers.IO) { repository.loadData2() } } launch { withContext(Dispatchers.IO) { repository.loadData1() } withContext(Dispatchers.IO) { repository.loadData2() } } !=
  46. Use immediate Main Dispatcher

  47. Use immediate Main Dispatcher // Main thread print("A") launch(Dispatchers.Main) {

    print("B") } print("C") // Result "ACB"
  48. Use immediate Main Dispatcher // Main thread "A" Dispatchers.Main "B"

    C" // Dispatchers.Main works based on handler.post { print("B") }
  49. Use immediate Main Dispatcher print("A") launch(Dispatchers.Main) { print("B") } print("C")

  50. Use immediate Main Dispatcher "A" Dispatchers.Main.immediate "B" C" // Result

    “ABC" // Dispatchers.Main.immediate works based on if (isMainThread()) { print("B") } else { handler.post { print("B") } }
  51. suspend function must be self-sufficient

  52. suspend function must be self-sufficient class Repository { suspend fun

    loadData() { … } }
  53. suspend function must be self-sufficient launch(Dispatchers.Main) { liveData.value = withContext(Dispatchers.IO)

    { repository.loadData() } }
  54. = withContext(Dispatchers.IO) suspend function must be self-sufficient

  55. suspend function must be self-sufficient class Repository { suspend fun

    loadData() = withContext(Dispatchers.IO) { … } } launch(Dispatchers.Main) { liveData.value = repository.loadData() }
  56. No need to always set Dispatcher

  57. No need to always set Dispatcher interface RetrofitService { fun

    getData(): Call<String> } // Adapter for Retrofit Call to Kotlin Coroutines suspend fun <T> Call<T>.await() { … }
  58. No need to always set Dispatcher class Repository(val service: RetrofitService)

    { suspend fun loadData(): String { return withContext(Dispatchers.IO) { service.getData().await() } } }
  59. No need to always set Dispatcher withContext(Dispatchers.IO)

  60. No need to always set Dispatcher dispatcher(Dispatcher)

  61. No need to always set Dispatcher class Repository(val service: RetrofitService)

    { suspend fun loadData(): String { return withContext(Dispatchers.IO) { service.getData().await() } } }
  62. No need to always set Dispatcher class Repository(val service: RetrofitService)

    { suspend fun loadData(): String { return service.getData().await() } }
  63. No need to always set Dispatcher // Coroutines in Room

    2.1 @Dao interface ParticipantDao { @Insert suspend fun insert(participants: List<Participant>) @Query("SELECT * FROM participant") suspend fun getAll(): List<Participant> }
  64. // Coroutines in Retrofit 2.6.0 (next version) interface MobiusService {

    @GET("participants") suspend fun getParticipants(): List<Participant> } No need to always set Dispatcher
  65. Use your own Dispatchers provider

  66. Use your own Dispatchers provider abstract class CoroutineViewModel : ViewModel(),

    CoroutineScope { override val coroutineContext = SupervisorJob() + Dispatchers.Main }
  67. Use your own Dispatchers provider Dispatchers.Main

  68. Use your own Dispatchers provider // With 'kotlinx-coroutines-test' library Dispatchers.setMain(dispatcher)

    Dispatchers.setIO(dispatcher) Dispatchers.setDefault(dispatcher)
  69. Use your own Dispatchers provider Dispatchers.Main

  70. Use your own Dispatchers provider dispatchers: AppDispatchers dispatchers.main

  71. Use your own Dispatchers provider class AppDispatchers( val main: CoroutineDispatcher

    = Dispatchers.Main, val io: CoroutineDispatcher = Dispatchers.IO, val default: CoroutineDispatcher = Dispatchers.Default )
  72. Use your own Dispatchers provider // Set all dispatchers to

    execute on current thread val testDispatchers = AppDispatchers( main = Dispatchers.Unconfined, io = Dispatchers.Unconfined, default = Dispatchers.Unconfined )
  73. Debugging coroutines

  74. Debugging coroutines Exception in thread "DefaultDispatcher-worker-1” java.lang.IllegalArgumentException at TestKt$main$1$2.invokeSuspend(Test.kt:23)

  75. Debugging coroutines System.setProperty( DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON )

  76. Debugging coroutines CoroutineName("Sending message”)

  77. Debugging coroutines Exception in thread "DefaultDispatcher-worker-1" java.lang.IllegalArgumentException at TestKt$main$1$2.invokeSuspend(Test.kt:23)

  78. Debugging coroutines @Sending Message#3

  79. Log Exceptions

  80. Log Exceptions val logExceptionHandler = CoroutineExceptionHandler { _, error ->

    Log.e(TAG, "Exception", error) }
  81. Log Exceptions , logExceptionHandler

  82. Migration from RxJava

  83. Migration from RxJava repository.loadData() .subscribeOn(Schedulers.io()) .map { data -> convertData(data)

    } .observeOn(AndroidSchedulers.mainThread()) .subscribe { data -> print(data) }
  84. Migration from RxJava launch(Dispatchers.Main) { val data = withContext(Dispatchers.IO) {

    convertData(repository.loadData()) } print(data) }
  85. Migration from RxJava launch { val data = convertData(repository.loadData()) print(data)

    }
  86. Migration from RxJava • Deferred • zip(), zipWith() • blter()

    • map() • catMap(), concatMap() • retryDeferredWithDelay() • ReceiverChannel • asyncFlatMap() • asyncConcatMap() • asyncMap() • distinctUntilChanged() • reduce() • concat(), concatWith() • debounce() Coroutines Extensions github.com/epam/CoroutinesExtensions
  87. Migration from RxJava RxJava 2 Coroutine Single<T> Deferred<T> ❄ Maybe<T>

    Deferred<T?> ❄ Completable Job ❄ Observable<T> Channel<T> (experimental) Flow<T> (preview) ❄ Flowable<T>
  88. Coroutines isn’t silver bullet

  89. Coroutines isn’t silver bullet • Don’t work with Coroutines like

    with Java Threads and Executors • Don’t forget about thread safety • Remember that CPU has limits • Analyze stack trace still complex Except Kotlin/JVM • Coroutines isn’t RxJava replacement
  90. Thank you! Kirill Rozov krl.rozov@gmail.com