Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Kotlin Coroutines are light-weight threads!

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

Structured Concurrency

Slide 5

Slide 5 text

“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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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 }

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

Android lifecycles (Activity & Fragment)

Slide 12

Slide 12 text

When to cancel?

Slide 13

Slide 13 text

Android Jetpack to the rescue!

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Use Lifecycle KTX for Coroutines on Android!

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Where should you call launch? It Depends!

Slide 22

Slide 22 text

launch() vs async()

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

async() or launch()? It Depends!

Slide 27

Slide 27 text

How about event streams?

Slide 28

Slide 28 text

Instant Search

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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!

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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!

Slide 36

Slide 36 text

Coroutine Channel + Flow for events!

Slide 37

Slide 37 text

What about error handling?

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Use sealed classes to handle results and errors!

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Thank you for listening!