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

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.

Kirill Rozov

May 23, 2019
Tweet

More Decks by Kirill Rozov

Other Decks in Programming

Transcript

  1. Coroutining
    Android
    Apps
    Kirill Rozov@EPAM

    View Slide

  2. Kirill Rozov
    [email protected]
    Community Bridger
    Accelerators Lead
    @kirill_rozov

    View Slide

  3. Android Broadcast
    News for Android Developers

    View Slide

  4. 1.0 - November 18, 2014
    RxJava

    View Slide

  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

    View Slide

  6. View Slide

  7. Don’t block
    Keep moving
    otlin 1.3 COROUTINES

    View Slide

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

    View Slide

  9. suspend != blocking

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  15. Structured Concurrency
    +
    Lifecycle
    =

    View Slide

  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
    }
    }

    View Slide

  17. Structured
    Concurrency
    +
    Lifecycle
    =

    class MainViewModel : ViewModel() {
    init {
    // Start async operation for UI
    }
    override fun onCleared() {
    super.onCleared()
    // Cancel async operation for UI
    }
    }

    View Slide

  18. Structured
    Concurrency
    +
    Lifecycle
    =

    abstract class CoroutineViewModel :
    ViewModel(), CoroutineScope {
    override val coroutineContext = SupervisorJob()
    override fun onCleared() {
    super.onCleared()
    cancel()
    }
    }

    View Slide

  19. Structured
    Concurrency
    +
    Lifecycle
    =

    CoroutineScope

    View Slide

  20. Structured
    Concurrency
    +
    Lifecycle
    =

    override val coroutineContext = SupervisorJob()

    View Slide

  21. Structured
    Concurrency
    +
    Lifecycle
    =

    cancel()

    View Slide

  22. Structured
    Concurrency
    +
    Lifecycle
    =

    class DetailsViewModel(val repository: Repository)
    : CoroutineViewModel() {
    fun initData() {
    launch {
    val data = repository.loadData()
    if (isActive) { // or yield()
    liveData.value = data
    }
    }
    }
    }

    View Slide

  23. Structured
    Concurrency
    +
    Lifecycle
    =

    viewModelScope.launch

    View Slide

  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()
    }
    }

    View Slide

  25. Structured
    Concurrency
    +
    Lifecycle
    =

    fun FragmentActivity.newCoroutineScope(
    context: CoroutineContext = EmptyCoroutineContext
    ): CoroutineScope {
    return LifecycleCoroutineScope(
    lifecycle, Dispatchers.Main + context
    )
    }

    View Slide

  26. Structured
    Concurrency
    +
    Lifecycle
    =

    private val coroutineScope = newCoroutineScope()

    coroutineScope.launch {
    // Start async operation for UI
    }

    View Slide

  27. Choose CoroutineScope
    properly

    View Slide

  28. Choose
    CoroutineScope
    properly
    class MessagesViewModel(
    val repository: Repository
    ) : CoroutineViewModel() {
    fun sendMessage(message: String) {
    launch {
    repository.sendMessage(message)
    }
    }
    }

    View Slide

  29. Choose
    CoroutineScope
    properly
    launch {
    repository.sendMessage(message)
    }

    View Slide

  30. Choose
    CoroutineScope
    properly
    launch(Job())

    View Slide

  31. Choose
    CoroutineScope
    properly
    GlobalScope.launch

    View Slide

  32. Choose
    CoroutineScope
    properly
    fun
    coroutineScope
    launch

    View Slide

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

    View Slide

  34. Default Dispatcher for
    CoroutineScope

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  39. Default
    Dispatcher for
    CoroutineScope
    launch

    View Slide

  40. Prefer withContext() for
    switch CoroutineContext

    View Slide

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

    View Slide

  42. Prefer
    withContext() for
    switch
    CoroutineContext
    val dataDeferred = async(Dispatchers.IO)

    View Slide

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

    View Slide

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

    View Slide

  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() }
    }
    !=

    View Slide

  46. Use immediate
    Main Dispatcher

    View Slide

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

    View Slide

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

    View Slide

  49. Use immediate
    Main Dispatcher
    print("A")
    launch(Dispatchers.Main) {
    print("B")
    }
    print("C")

    View Slide

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

    View Slide

  51. suspend function must
    be self-sufficient

    View Slide

  52. suspend
    function must be
    self-sufficient
    class Repository {
    suspend fun loadData() { … }
    }

    View Slide

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

    View Slide

  54. = withContext(Dispatchers.IO)
    suspend
    function must be
    self-sufficient

    View Slide

  55. suspend
    function must be
    self-sufficient
    class Repository {
    suspend fun loadData() =
    withContext(Dispatchers.IO) { … }
    }
    launch(Dispatchers.Main) {
    liveData.value = repository.loadData()
    }

    View Slide

  56. No need to always set
    Dispatcher

    View Slide

  57. No need to
    always set
    Dispatcher
    interface RetrofitService {
    fun getData(): Call
    }
    // Adapter for Retrofit Call to Kotlin Coroutines
    suspend fun Call.await() { … }

    View Slide

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

    View Slide

  59. No need to
    always set
    Dispatcher
    withContext(Dispatchers.IO)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  63. No need to
    always set
    Dispatcher
    // Coroutines in Room 2.1
    @Dao
    interface ParticipantDao {
    @Insert
    suspend fun insert(participants: List)
    @Query("SELECT * FROM participant")
    suspend fun getAll(): List
    }

    View Slide

  64. // Coroutines in Retrofit 2.6.0 (next version)
    interface MobiusService {
    @GET("participants")
    suspend fun getParticipants(): List
    }
    No need to
    always set
    Dispatcher

    View Slide

  65. Use your own
    Dispatchers provider

    View Slide

  66. Use your own
    Dispatchers
    provider
    abstract class CoroutineViewModel :
    ViewModel(), CoroutineScope {
    override val coroutineContext =
    SupervisorJob() + Dispatchers.Main
    }

    View Slide

  67. Use your own
    Dispatchers
    provider
    Dispatchers.Main

    View Slide

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

    View Slide

  69. Use your own
    Dispatchers
    provider
    Dispatchers.Main

    View Slide

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

    View Slide

  71. Use your own
    Dispatchers
    provider
    class AppDispatchers(
    val main: CoroutineDispatcher = Dispatchers.Main,
    val io: CoroutineDispatcher = Dispatchers.IO,
    val default: CoroutineDispatcher = Dispatchers.Default
    )

    View Slide

  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
    )

    View Slide

  73. Debugging coroutines

    View Slide

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

    View Slide

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

    View Slide

  76. Debugging
    coroutines
    CoroutineName("Sending message”)

    View Slide

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

    View Slide

  78. Debugging
    coroutines
    @Sending Message#3

    View Slide

  79. Log Exceptions

    View Slide

  80. Log
    Exceptions
    val logExceptionHandler =
    CoroutineExceptionHandler { _, error ->
    Log.e(TAG, "Exception", error)
    }

    View Slide

  81. Log
    Exceptions
    ,
    logExceptionHandler

    View Slide

  82. Migration from RxJava

    View Slide

  83. Migration from
    RxJava
    repository.loadData()
    .subscribeOn(Schedulers.io())
    .map { data -> convertData(data) }
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { data -> print(data) }

    View Slide

  84. Migration from
    RxJava
    launch(Dispatchers.Main) {
    val data = withContext(Dispatchers.IO) {
    convertData(repository.loadData())
    }
    print(data)
    }

    View Slide

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

    View Slide

  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

    View Slide

  87. Migration from
    RxJava
    RxJava 2 Coroutine
    Single Deferred ❄
    Maybe Deferred ❄
    Completable Job ❄
    Observable Channel (experimental)
    Flow (preview) ❄
    Flowable

    View Slide

  88. Coroutines isn’t
    silver bullet

    View Slide

  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

    View Slide

  90. Thank you!
    Kirill Rozov
    [email protected]

    View Slide