Share
Embed
Dive into Async apps with Kotlin Coroutines
This talk highlights the challenges underpinning some of the paradigms of asynchronous programming, in particular, the callback-based approach. The talk will address how Kotlin aims to solve this problem with coroutines by providing an asynchronous interface to the developer.
- Coroutines Generator API - Sequences or iterables in an asynchronous manner
- Coroutine Scopes and how to manage them
- How to use Coroutines to do network calls from the app.
- Actors, easing concurrency for Android Developers
- Sneak peek at channels, broadcast channel, flow
- Using Coroutines and Rx2 together
Adit Lal
November 02, 2019
Transcript
73 slides
-
Asynchronous • Threading • AsyncTask • Callbacks • Observable •
Future/Promise • Coroutines -
Threads • Threads are expensive • occupies about 1-2 mb
• Requires context switching • Thread management • Race conditions • Thread Pools -
Callbacks • Difficulty of nested callbacks • Callback Hell •
Error Handling is a mess -
Future/Promise • Similar to callbacks • Composition model with chained
calls • Complicated Exception handling • Returns Promise -
Reactive • Observable streams • Constant API across platforms •
Great error handling -
Coroutines • Launch a long-running operation w/o blocking thread •
Functions are now suspend-able • Platform independent -
Coroutines simplify asynchronous programming by providing possibility to write code
in direct style (sequentially). -
fun loadData() = GlobalScope.launch(uiContext) { showLoading() // ui thread val
result = dataProvider.fetchData() // non ui thread, suspend until finished showText(result) // ui thread hideLoading() // ui thread } Suspend -
fun loadData() = GlobalScope.launch(uiContext) { showLoading() // ui thread val
result = dataProvider.fetchData() // non ui thread, suspend until finished showText(result) // ui thread hideLoading() // ui thread } -
fun loadData() = GlobalScope.launch(uiContext) { showLoading() // ui thread val
result = dataProvider.fetchData() // non ui thread, suspend until finished showText(result) // ui thread hideLoading() // ui thread } -
fun loadData() = GlobalScope.launch(uiContext) { showLoading() // ui thread val
result = dataProvider.fetchData() // non ui thread, suspend until finished showText(result) // ui thread hideLoading() // ui thread } -
fun loadData() = GlobalScope.launch(uiContext) { showLoading() // ui thread val
result = dataProvider.fetchData() // non ui thread, suspend until finished showText(result) // ui thread hideLoading() // ui thread } -
Context fun main() = runBlocking<Unit> { println("My context is: $coroutineContext")
} My context is: [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@73c6c3b2, BlockingEventLoop@48533e64] -
‘Dat Sequential suspend fun doSomethingUsefulOne(): Int { delay(1000L) //some fancy
magic return 13 } suspend fun doSomethingUsefulTwo(): Int { delay(1000L) //some fancy magic again return 29 } -
‘Dat Sequential val time = measureTimeMillis { val one =
doSomethingUsefulOne() val two = doSomethingUsefulTwo() println("The answer is ${one + two}") } println("Completed in $time ms") The answer is 42 Completed in 2007 ms -
‘Dat async val time = measureTimeMillis { val one =
async(IO){ doSomethingUsefulOne() } val two = async(IO) { doSomethingUsefulTwo() } println("The answer is ${one.await() + two.await()}") } println("Completed in $time ms") The answer is 42 Completed in 1014 ms -
Dispatcher To specify where the coroutines should run, Kotlin provides
three Dispatchers you can use for thread dispatch. -
Dispatchers.Main Main thread on Android, interact with the UI and
perform light work Great for : - Calling suspend functions - Call UI functions - Updating LiveData -
Dispatchers.IO Optimized for disk and network IO off the main
thread Great for : - Database* - Reading/writing files - Networking$** -
Dispatchers.Default Optimized for CPU intensive work off the main thread
Great for : - Sorting a list - Parsing JSON - DiffUtils -
class SearchViewModel(val repo: SearchRepository): ViewModel() { private val _sortedResults =
MutableLiveData<List<SearchResults>>() val sortedResults: LiveData<List<SearchResults>> = _sortedResults fun onSortAscending() = sortSearchBy(sort = true) fun onSortDescending() = sortSearchBy(sort = false) fun sortSearchBy(sort: Boolean, query: String) { viewModelScope.launch { // suspend and resume make this database request main-safe // so our ViewModel doesn't need to worry about threading _sortedResults.value = repo.fetchSearchResults(query, sort) } } } -
class SearchViewModel(val repo: SearchRepository): ViewModel() { private val _sortedResults =
MutableLiveData<List<SearchResults>>() val sortedResults: LiveData<List<SearchResults>> = _sortedResults fun onSortAscending() = sortSearchBy(ascending = true) fun onSortDescending() = sortSearchBy(ascending = false) fun sortSearchBy(sort: Boolean, query: String) { viewModelScope.launch { // suspend and resume make this database request main-safe // so our ViewModel doesn't need to worry about threading _sortedResults.value = repo.fetchSearchResults(query, sort) } } } -
class SearchViewModel(val repo: SearchRepository): ViewModel() { private val _sortedResults =
MutableLiveData<List<SearchResults>>() val sortedResults: LiveData<List<SearchResults>> = _sortedResults fun onSortAscending() = sortSearchBy(ascending = true) fun onSortDescending() = sortSearchBy(ascending = false) fun sortSearchBy(sort: Boolean, query: String) { viewModelScope.launch { // suspend and resume make this database request main-safe // so our ViewModel doesn't need to worry about threading _sortedResults.value = repo.fetchSearchResults(query, sort) } } } -
class SearchViewModel(val repo: SearchRepository): ViewModel() { private val _sortedResults =
MutableLiveData<List<SearchResults>>() val sortedResults: LiveData<List<SearchResults>> = _sortedResults fun onSortAscending() = sortSearchBy(ascending = true) fun onSortDescending() = sortSearchBy(ascending = false) fun sortSearchBy(sort: Boolean, query: String) { viewModelScope.launch { // suspend and resume make this database request main-safe // so our ViewModel doesn't need to worry about threading _sortedResults.value = repo.fetchSearchResults(query, sort) } } } -
class SearchRepository(val api: SearchAPI) { suspend fun fetchSearchResults( query: String,
sort: Boolean ): List<SearchResults> { api.fetchResults(query, sort) .map(::mapToList) } } -
class SearchRepository(val api: SearchAPI) { suspend fun fetchSearchResults( query: String,
sort: Boolean ): List<SearchResults> { api.fetchResults(query, sort) .map(::mapToList) } } -
class SearchRepository(val api: SearchAPI) { suspend fun fetchSearchResults( query: String,
sort: Boolean ): List<SearchResults> { api.fetchResults(query, sort) .map(::mapToList) } } -
//Rx fun fetchResults( query: String, sort: Boolean ): Single<SearchResponse> //Coroutine
suspend fun fetchResults( query: String, sort: Boolean ): SearchResponse -
//Rx fun fetchResults( query: String, sort: Boolean ): Single<SearchResponse> //Coroutine
suspend fun fetchResults( query: String, sort: Boolean ): SearchResponse -
//Rx fun fetchResults( query: String, sort: Boolean ): Single<SearchResponse> //Coroutine
suspend fun fetchResults( query: String, sort: Boolean ): SearchResponse -
//Fast and local fetch fun foo(param: Param) : Result //Slow
and IO suspend fun foo(param: Param) : Result //BG fun CoroutineScope.foo(param: Param): Result -
Asynchronicity Comparison • Observables in RxJava Observable.create<Int> { emitter ->
for (i in 1..5) { emitter.onNext(i) } emitter.onComplete() } -
Asynchronicity Comparison • Channels val channel = Channel<Int>() launch {
// this might be heavy CPU-consuming computation or async logic //we'll just send five squares for (x in 1..5) channel.send(x) } // here we print five received integers: repeat(5) { println(channel.receive()) } println("Done!") -
Asynchronicity Comparison • Channels val channel = Channel<Int>() launch {
// this might be heavy CPU-consuming computation or async logic //we'll just send five squares for (x in 1..5) channel.send(x) channel.close() } // here we print five received integers: repeat(5) { println(channel.receive()) } println("Done!") -
Asynchronicity Comparison • Channels val channel = Channel<Int>() launch {
// this might be heavy CPU-consuming computation or async logic //we'll just send five squares for (x in 1..5) channel.send(x) channel.close() } // here we print five received integers: repeat(5) { println(channel.receive()) } println("Done!") -
Asynchronicity Comparison • Channels val channel = Channel<Int>() launch {
// this might be heavy CPU-consuming computation or async logic //we'll just send five squares for (x in 1..5) channel.send(x) channel.close() } // here we print five received integers: repeat(5) { println(channel.receive()) } println("Done!") -
Asynchronicity Comparison • Channels val sequence = produceSequence() sequence.consumeEach {
println(it) } println("Done!") -
Asynchronicity Comparison • Channels ? Observable ? When you consume
an object from the channel no other coroutine will be able to get the same object -
Asynchronicity Comparison • Channels ~ Race Conditions ? val channel
= Channel<Int>() launch { val value1 = channel.receive() } launch { val value2 = channel.receive() } launch { channel.send(1) } -
Asynchronicity Comparison • Rx Subjects val subject: PublishSubject<Int> = PublishSubject.create()
subject.subscribe { consumeValue(it) } subject.subscribe { println(it) } -
Asynchronicity Comparison • BroadcastChannel val channel = BroadcastChannel<Int>(2) val observer1Job
= launch { channel.openSubscription().use { channel -> for (value in channel) { consumeValue(value) } // subscription will be closed } } -
Asynchronicity Comparison • BroadcastChannel val observer2Job = launch { channel.consumeEach
{ value -> consumeValue(value) } } -
Asynchronicity Comparison • Produce val publisher = produce(capacity = 2)
{ for (i in 1..5) send(i) } • Only the code inside produce can send elements to the channel -
Asynchronicity Comparison Produce is a pretty useful way to create
custom operators -
Asynchronicity Comparison • Actor An actor also creates a coroutine
with a channel built in. Produce implements the ReceiveChannel<E> interface whereas actor implements the SendChannel<E> interface. -
Asynchronicity Comparison • Actor val actor = actor<Int>(CommonPool) { for
(int in channel) { // iterate over received Integers } } launch { actor.send(2) } -
Flow Flow is a Stream of Data, which can emit
multiple values and complete with or without an error -
Flow val myFlow: Flow<Int> = flow { emit(1) emit(2) delay(100)
emit(3) } flow.collect { value -> println("Received $value") } Received 1 Received 2 Received 3 $$==> -
Flow + Channels • Channels are Hot whereas Flows are
cold • One Emits values from 1 side, receives from other side • A bit complex to work with -
Flow + Channels • Hot + Cold = Hot •
Ex: Flow from a channel -
Flow + Channels object Action { private val channel =
BroadcastChannel<Unit>(1) button.onClick { channel.send(Unit) } fun getFlow(): Flow<Unit> = channel.asFlow() } Action.getFlow().collect { println(“Received Emission”) } -
Flow + Channels object Action { private val channel =
BroadcastChannel<Unit>(1) button.onClick { channel.send(Unit) } fun getFlow(): Flow<Unit> = channel.asFlow() } Action.getFlow().collect { println(“Received Emission”) } -
Flow + Channels object Action { private val channel =
BroadcastChannel<Unit>(1) button.onClick { channel.send(Unit) } fun getFlow(): Flow<Unit> = channel.asFlow() } Action.getFlow().collect { println(“Received Emission”) } -
Flow + Channels object Action { private val channel =
BroadcastChannel<Unit>(1) button.onClick { channel.send(Unit) } fun getFlow(): Flow<Unit> = channel.asFlow() } Action.getFlow().collect { println(“Received Emission”) } -
Flow and Rx compositeDisposable.add(api.search(query, sort) .map{response -> response.toList() } .flatMap
{ searchResults -> cache.saveAndReturn(searchResults) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( { searchResults -> displayResults(searchResults) }, { error -> Log.e("TAG", "Failed to search", error) } )) -
Flow and Rx try { flow { emit(api.search(query, sort))} .map{response
->response.toList()} .flatMapConcat { searchResults -> cache.saveAndReturn(searchResults) } .flowOn(Dispatchers.IO) .collect { searchResultList -> withContext(Dispatchers.Main) { showSearchResult(searchResultList) } } } catch(e: Exception) { e.printStackTrace() } -
Rx / Suspend //Rx Observable.interval(1, TimeUnit.SECONDS) .subscribe { textView.text =
"$it seconds have passed" } //Coroutine GlobalScope.launch { var i = 0 while (true){ textView.text = "${it++} seconds have passed" delay(1000) } } -
Rx / Suspend publishSubject .debounce(300, TimeUnit.MILLISECONDS) .distinctUntilChanged() .switchMap { searchQuery
-> api.fetchResults(query, sort) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ ... }) -
Rx / Suspend launch { broadcast.consumeEach { delay(300) val results
= api.fetchResults(it) .await() displayResultsLogic() ) } } -
Rx / Suspend //Rx searchQuery.addTextChangedListener(object: TextWatcher { override fun afterTextChanged(s:
Editable?) { publishSubject.onNext(s.toString()) } } //Coroutines searchQuery.addTextChangedListener(object: TextWatcher { override fun afterTextChanged(s: Editable?) { broadcast.send(s.toString()) } }