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. 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
  2. 6.
  3. 10.

    Long Task Thread suspend != blocking work work block t

    Java Thread Coroutine work work suspend
  4. 11.

    work work block Java Thread Coroutine work work suspend Long

    Task Thread t suspend != blocking work
  5. 12.

    work work block Java Thread Coroutine work work suspend work

    Long Task Thread t suspend != blocking work
  6. 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 } }
  7. 17.

    Structured Concurrency + Lifecycle = ❤ class MainViewModel : ViewModel()

    { init { // Start async operation for UI } override fun onCleared() { super.onCleared() // Cancel async operation for UI } }
  8. 18.

    Structured Concurrency + Lifecycle = ❤ abstract class CoroutineViewModel :

    ViewModel(), CoroutineScope { override val coroutineContext = SupervisorJob() override fun onCleared() { super.onCleared() cancel() } }
  9. 22.

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

    : CoroutineViewModel() { fun initData() { launch { val data = repository.loadData() if (isActive) { // or yield() liveData.value = data } } } }
  10. 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() } }
  11. 25.

    Structured Concurrency + Lifecycle = ❤ fun FragmentActivity.newCoroutineScope( context: CoroutineContext

    = EmptyCoroutineContext ): CoroutineScope { return LifecycleCoroutineScope( lifecycle, Dispatchers.Main + context ) }
  12. 26.

    Structured Concurrency + Lifecycle = ❤ private val coroutineScope =

    newCoroutineScope() … coroutineScope.launch { // Start async operation for UI }
  13. 28.

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

    CoroutineViewModel() { fun sendMessage(message: String) { launch { repository.sendMessage(message) } } }
  14. 41.

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

    async(Dispatchers.IO) { repository.loadData() } val data = dataDeferred.await() }
  15. 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() } } !=
  16. 48.

    Use immediate Main Dispatcher // Main thread "A" Dispatchers.Main "B"

    C" // Dispatchers.Main works based on handler.post { print("B") }
  17. 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") } }
  18. 55.

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

    loadData() = withContext(Dispatchers.IO) { … } } launch(Dispatchers.Main) { liveData.value = repository.loadData() }
  19. 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() { … }
  20. 58.

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

    { suspend fun loadData(): String { return withContext(Dispatchers.IO) { service.getData().await() } } }
  21. 61.

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

    { suspend fun loadData(): String { return withContext(Dispatchers.IO) { service.getData().await() } } }
  22. 62.

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

    { suspend fun loadData(): String { return service.getData().await() } }
  23. 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> }
  24. 64.

    // Coroutines in Retrofit 2.6.0 (next version) interface MobiusService {

    @GET("participants") suspend fun getParticipants(): List<Participant> } No need to always set Dispatcher
  25. 66.

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

    CoroutineScope { override val coroutineContext = SupervisorJob() + Dispatchers.Main }
  26. 71.

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

    = Dispatchers.Main, val io: CoroutineDispatcher = Dispatchers.IO, val default: CoroutineDispatcher = Dispatchers.Default )
  27. 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 )
  28. 83.

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

    } .observeOn(AndroidSchedulers.mainThread()) .subscribe { data -> print(data) }
  29. 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
  30. 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>
  31. 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