$30 off During Our Annual Pro Sale. View Details »

Useful Coroutine patterns for Android applications

Erik Hellman
February 02, 2020

Useful Coroutine patterns for Android applications

Kotlin Coroutines are a great match for implementing common features in Android applications. In this presentation, we will go through a number of patterns that solves the pattern we often encounter when implementing traditional asynchronous features.

Coroutines can help us build robust solution for asynchronous work in Android applications, but it can also be difficult to learn what pattern should be used in different scenarios. When should you use launch instead of async, or when should you use a Channel instead of Flow, or maybe both? What use is a conflated channel and why is the catch() operator important? All this and some more will be covered in this talk.

Erik Hellman

February 02, 2020
Tweet

More Decks by Erik Hellman

Other Decks in Programming

Transcript

  1. Useful Coroutine patterns
    for Android applications
    @ErikHellman - github.com/ErikHellman - hellsoft.se
    https://speakerdeck.com/erikhellman

    View Slide

  2. Kotlin Coroutines
    are light-weight threads!

    View Slide

  3. Coroutines ARE light!
    import kotlinx.coroutines.*
    fun main() = runBlocking {
    repeat(100_000) { -/ launch a lot of coroutines
    GlobalScope.launch {
    delay(1000L)
    print(".")
    }
    }
    }

    View Slide

  4. Structured Concurrency

    View Slide

  5. “Coroutines are always related to
    some local scope in your
    application, which is an entity with
    a limited life-time, like a UI
    element.”
    - Structured concurrency by Roman Elizarov
    https://medium.com/@elizarov/structured-concurrency-722d765aa952

    View Slide

  6. Legacy - don’t do this!
    suspend fun loadAndCombine(name1: String, name2: String): Image {
    val deferred1 = async { loadImage(name1) }
    val deferred2 = async { loadImage(name2) }
    return combineImages(deferred1.await(), deferred2.await())
    }

    View Slide

  7. Launch coroutines in a CoroutineScope
    suspend fun loadAndCombine(name1: String, name2: String): Image =
    coroutineScope {
    val deferred1 = async { loadImage(name1) }
    val deferred2 = async { loadImage(name2) }
    combineImages(deferred1.await(), deferred2.await())
    }

    View Slide

  8. CoroutineScope interface
    public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
    }

    View Slide

  9. CoroutineScope.launch()
    public 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
    }

    View Slide

  10. CoroutineScope.cancel()
    fun cancelScope(scope: CoroutineScope) {
    repeat(100_000) {
    scope.launch {
    delay(1000L)
    print(".")
    }
    }
    Thread.sleep(100)
    scope.cancel()
    }

    View Slide

  11. Android lifecycles (Activity & Fragment)

    View Slide

  12. When to cancel?

    View Slide

  13. Android Jetpack to the rescue!

    View Slide

  14. AndroidX Lifecycle KTX library
    dependencies {
    def lifecycle_version = "2.2.0"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
    }

    View Slide

  15. AndroidX Lifecycle KTX library
    val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
    while (true) {
    val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
    if (existing -= null) {
    return existing
    }
    val newScope = LifecycleCoroutineScopeImpl(
    this,
    kotlinx.coroutines.SupervisorJob() +
    kotlinx.coroutines.Dispatchers.Main.immediate
    )
    if (mInternalScopeRef.compareAndSet(null, newScope)) {
    newScope.register()
    return newScope
    }
    }
    }

    View Slide

  16. AndroidX Lifecycle KTX library
    val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

    View Slide

  17. Lifecycle scoped coroutine
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.login_form)
    val loginButton = findViewById(R.id.loginButton)
    loginButton.setOnClickListener {
    lifecycleScope.launch {
    val username = …
    val password = …
    doLogin(username, password)
    }
    }
    }

    View Slide

  18. Lifecycle scoped coroutine
    class MyViewModel(val authRepository: AuthRepository) : ViewModel() {
    fun doLogin(username: String, password: String) {
    viewModelScope.launch {
    authRepository.login(username, password)
    }
    }
    }

    View Slide

  19. Use Lifecycle KTX for
    Coroutines on Android!

    View Slide

  20. Where should you call launch?
    LoginFragment
    MainActivity
    AuthViewModel
    AuthRepository
    Longer lifecycle
    suspend fun login(
    username: String,
    password: String
    )

    View Slide

  21. Where should you call launch?
    It Depends!

    View Slide

  22. launch() vs async()

    View Slide

  23. Login example
    class LoginApi {
    suspend fun login(username: String, password: String): AuthResult {
    -/ Call Login API and return AuthResult
    }
    }

    View Slide

  24. Login example - launch()
    class LoginFragment : Fragment() {
    lateinit var loginApi: LoginApi
    fun performLogin(username: String, password: String) {
    lifecycleScope.launch {
    val authResult = loginApi.login(username, password)
    if (authResult.isAuthenticated) {
    -/ Login ok!
    } else {
    -/ Login failed!
    }
    }
    }
    }

    View Slide

  25. Login example - async()
    fun performLogin(username: String, password: String) {
    val deferredResult = lifecycleScope.async {
    loginApi.login(username, password)
    }
    lifecycleScope.launch {
    val result = deferredResult.await()
    -/ Handle login result
    }
    }

    View Slide

  26. async() or launch()?
    It Depends!

    View Slide

  27. How about event streams?

    View Slide

  28. Instant Search

    View Slide

  29. Instant Search events
    SearchFragment
    SearchViewModel
    SearchRepository
    Search API
    Search query events
    Search result events

    View Slide

  30. Search Repository
    class SearchRepository {
    suspend fun performSearch(query: String): List {
    -/ Call online Search API here
    }
    }

    View Slide

  31. SearchViewModel
    class SearchViewModel : ViewModel() {
    val queryChannel = ConflatedBroadcastChannel()
    val searchResult = queryChannel
    .asFlow()
    .debounce(SEARCH_DELAY_MS)
    .mapLatest { searchApi.performSearch(it) }
    .asLiveData()
    }
    Equivalent to BehaviorSubject in RxJava
    Lifecycle KTX extension function!

    View Slide

  32. SearchActivity
    class SearchActivity : AppCompatActivity() {
    -/ Adapter for RecycleView displaying search result
    val searchAdapter = SearchAdapter()
    val viewModel: SearchViewModel by viewModels()
    -/ Setup the EditText for instant search
    fun setupSearchField(searchText: EditText) {
    searchText.doAfterTextChanged { editable ->
    viewModel.queryChannel.offer(editable.toString())
    }
    }
    -/ Listen for search results
    fun handleSearchResult() {
    viewModel.searchResult.observe(this, searchAdapter-:submitList)
    }
    }

    View Slide

  33. Coroutine Channels - offer()
    fun testChannels(channel: Channel) {
    if (channel.offer(1)) {
    if (channel.offer(2)) {
    if (channel.offer(3)) {
    println("Sent three numbers!")
    }
    }
    }
    }

    View Slide

  34. Coroutine Channels - send()
    suspend fun testChannels(channel: Channel) {
    channel.send(1)
    channel.send(2)
    channel.send(3)
    println("Sent three numbers!")
    }

    View Slide

  35. Instant Search events
    SearchFragment
    SearchViewModel
    SearchRepository
    Search API
    Pass events down
    using a Channel
    Use Flow for events
    coming up
    Convert Flow to
    LiveData for simplicity!

    View Slide

  36. Coroutine Channel + Flow
    for events!

    View Slide

  37. What about error handling?

    View Slide

  38. Sealed classes to the rescue!
    sealed class SearchResult
    class ValidResult(val result: List) : SearchResult()
    object EmptyResult : SearchResult()
    object EmptyQuery : SearchResult()
    class ErrorResult(val e: IOException) : SearchResult()
    class TerminalError(val t: Throwable) : SearchResult()

    View Slide

  39. Sealed classes to the rescue!
    .mapLatest {
    try {
    if (it.length > 2) {
    val searchResult = searchApi.performSearch(it)
    if (searchResult.isNotEmpty()) {
    ValidResult(searchResult)
    } else {
    EmptyResult
    }
    } else {
    EmptyQuery
    }
    } catch (e: IOException) {
    ErrorResult(e)
    }
    }
    .catch { emit(TerminalError(it)) }

    View Slide

  40. Sealed classes to the rescue!
    fun handleSearchResult() {
    viewModel.searchResult.observe(this, this-:showSearchResult)
    }
    fun showSearchResult(searchResult: SearchResult) {
    when (searchResult) {
    is ValidResult -> displayResult(searchResult)
    is ErrorResult -> showErrorMessage(searchResult)
    is EmptyResult -> showNoResultMessage()
    is EmptyQuery -> showEmptyQueryWarning()
    is TerminalError -> showTerminalError(searchResult)
    }
    }

    View Slide

  41. Use sealed classes to handle
    results and errors!

    View Slide

  42. Instant Search Demo
    ● Blog post:
    ○ www.hellsoft.se/instant-search-with-kotlin-coroutines
    ● Source code for sample:
    ○ github.com/ErikHellman/InstantSearchDemo

    View Slide

  43. Thank you for listening!

    View Slide