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

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. Coroutines ARE light! import kotlinx.coroutines.* fun main() = runBlocking {

    repeat(100_000) { -/ launch a lot of coroutines GlobalScope.launch { delay(1000L) print(".") } } }
  2. “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
  3. 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()) }
  4. 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()) }
  5. 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 }
  6. 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" }
  7. 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 } } }
  8. 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) } } }
  9. Lifecycle scoped coroutine class MyViewModel(val authRepository: AuthRepository) : ViewModel() {

    fun doLogin(username: String, password: String) { viewModelScope.launch { authRepository.login(username, password) } } }
  10. Where should you call launch? LoginFragment MainActivity AuthViewModel AuthRepository Longer

    lifecycle suspend fun login( username: String, password: String )
  11. Login example class LoginApi { suspend fun login(username: String, password:

    String): AuthResult { -/ Call Login API and return AuthResult } }
  12. 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! } } } }
  13. 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 } }
  14. 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!
  15. 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) } }
  16. Coroutine Channels - offer() fun testChannels(channel: Channel<Int>) { if (channel.offer(1))

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

    channel.send(2) channel.send(3) println("Sent three numbers!") }
  18. 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!
  19. 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()
  20. 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)) }
  21. 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) } }