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

Useful Coroutine patterns for Android applications

2307a37297162f815342545a2068b2f1?s=47 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.

2307a37297162f815342545a2068b2f1?s=128

Erik Hellman

February 02, 2020
Tweet

Transcript

  1. Useful Coroutine patterns for Android applications @ErikHellman - github.com/ErikHellman -

    hellsoft.se https://speakerdeck.com/erikhellman
  2. Kotlin Coroutines are light-weight threads!

  3. Coroutines ARE light! import kotlinx.coroutines.* fun main() = runBlocking {

    repeat(100_000) { -/ launch a lot of coroutines GlobalScope.launch { delay(1000L) print(".") } } }
  4. Structured Concurrency

  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
  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()) }
  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()) }
  8. CoroutineScope interface public interface CoroutineScope { public val coroutineContext: CoroutineContext

    }
  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 }
  10. CoroutineScope.cancel() fun cancelScope(scope: CoroutineScope) { repeat(100_000) { scope.launch { delay(1000L)

    print(".") } } Thread.sleep(100) scope.cancel() }
  11. Android lifecycles (Activity & Fragment)

  12. When to cancel?

  13. Android Jetpack to the rescue!

  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" }
  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 } } }
  16. AndroidX Lifecycle KTX library val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope get() = lifecycle.coroutineScope

  17. Lifecycle scoped coroutine override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.login_form)

    val loginButton = findViewById<Button>(R.id.loginButton) loginButton.setOnClickListener { lifecycleScope.launch { val username = … val password = … doLogin(username, password) } } }
  18. Lifecycle scoped coroutine class MyViewModel(val authRepository: AuthRepository) : ViewModel() {

    fun doLogin(username: String, password: String) { viewModelScope.launch { authRepository.login(username, password) } } }
  19. Use Lifecycle KTX for Coroutines on Android!

  20. Where should you call launch? LoginFragment MainActivity AuthViewModel AuthRepository Longer

    lifecycle suspend fun login( username: String, password: String )
  21. Where should you call launch? It Depends!

  22. launch() vs async()

  23. Login example class LoginApi { suspend fun login(username: String, password:

    String): AuthResult { -/ Call Login API and return AuthResult } }
  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! } } } }
  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 } }
  26. async() or launch()? It Depends!

  27. How about event streams?

  28. Instant Search

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

    events Search result events
  30. Search Repository class SearchRepository { suspend fun performSearch(query: String): List<String>

    { -/ Call online Search API here } }
  31. SearchViewModel class SearchViewModel : ViewModel() { val queryChannel = ConflatedBroadcastChannel<String>()

    val searchResult = queryChannel .asFlow() .debounce(SEARCH_DELAY_MS) .mapLatest { searchApi.performSearch(it) } .asLiveData() } Equivalent to BehaviorSubject in RxJava Lifecycle KTX extension function!
  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) } }
  33. Coroutine Channels - offer() fun testChannels(channel: Channel<Int>) { if (channel.offer(1))

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

    channel.send(2) channel.send(3) println("Sent three numbers!") }
  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!
  36. Coroutine Channel + Flow for events!

  37. What about error handling?

  38. Sealed classes to the rescue! sealed class SearchResult class ValidResult(val

    result: List<String>) : SearchResult() object EmptyResult : SearchResult() object EmptyQuery : SearchResult() class ErrorResult(val e: IOException) : SearchResult() class TerminalError(val t: Throwable) : SearchResult()
  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)) }
  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) } }
  41. Use sealed classes to handle results and errors!

  42. Instant Search Demo • Blog post: ◦ www.hellsoft.se/instant-search-with-kotlin-coroutines • Source

    code for sample: ◦ github.com/ErikHellman/InstantSearchDemo
  43. Thank you for listening!