Share
Embed
Ronen Sabag - Lean async code with Kotlin’s coroutines
droidcon Berlin
July 17, 2018
Transcript
24 slides
-
• Asynchronous code • What are Coroutines • Coroutines in
practice • Why to use Coroutines AGENDA 2 -
4 Asynchronous code Asynchronous code essentially means run some code
not sequentially Eventually, the way to run the asynchronous code is via callbacks Almost every Android app has asynchronous code -
5 Asynchronous code - Observation vs Synchronization Interactor Network Client
UI Repository enqueuRequest(cb:(Data) -> Unit) saveData(Data) fetchData(viewCb: (Status) -> Unit) observeData(ob: (Data) -> Unit) viewCb(OK) cb(data) -
7 What are coroutines Coroutines are piece of code that
can suspend and resume its execution at certain locations. The code inside coroutine can be expressed sequentially. It's an old concept, implemented by many programming languages. Kotlin support coroutines since release 1.1 . Most of the coroutines related code that JetBrains wrote is part of their extensions libraries of kotlinx.coroutines . In Kotlin suspendable function mark with the suspend modifier, only coroutines can run suspend functions -
10 Synchronous way fun handleDeepLink(request: Request) : Response { val
response = Response() …. response.journeyStartAddress = request.addressFetcher.getAddressFromLatLan(request.journeyStartLatLng) response.journeyEndAddress = request.addressFetcher.getAddressFromLatLan(request.journeyEndLatLng) …. return response } -
11 Use-case 1: Make two network calls in parallel and
wait for all fun handleDeepLink(request: Request) : Response { val response = Response() val journeyStartJob = launch { response.journeyStartAddress = request.addressFetcher.getAddressFromLatLan(request.journeyStartLatLng) } val journeyEndJob = launch { response.journeyEndAddress = request.addressFetcher.getAddressFromLatLan(request.journeyEndLatLng) } runBlocking { journeyStartJob.join() journeyEndJob.join() } return response } -
12 Use-case 1 - Cont’: Build My Own Abstractions fun
inParallel(task: suspend () -> Unit) = launch { task() } fun List<Job>.waitForAll() = runBlocking { forEach { it.join() } } class JobCanceler : LifecycleObserver { var jobs: List<Job>? = null @OnLifecycleEvent(ON_DESTROY) fun cancelJob() { jobs?.forEach { it.cancel() } } } -
13 Use-case 1 - Cont’: Use My Abstructions fun handleDeepLink(request:
Request): Response { val response = Response() val journeyStartJob = inParallel { response.journeyStartAddress = request.addressFetcher.getAddressFromLatLan(request.journeyStartLatLng) } val journeyEndJob = inParallel { response.journeyEndAddress = request.addressFetcher.getAddressFromLatLan(request.journeyStartLatLng) } val jobs = listOf(journeyStartJob, journeyEndJob) request.jobCanceler.jobs = jobs jobs.waitForAll() return response } -
14 Use-case 2: Make two network calls in parallel and
wait for first fun handleDeepLink(request: Request) : Response { val response = Response() val firstAddressProvider: Deferred<String> = async { request.addressFetcherProviderA.getAddressFromLatLan(request.journeyStartLatLng) } val secondAddressProvider: Deferred<String> = async { request.addressFetcherProviderB.getAddressFromLatLan(request.journeyStartLatLng) } response.journeyStartAddress = runBlocking { select<String> { firstAddressProvider.onAwait { firstAddressProvider.await() } secondAddressProvider.onAwait { secondAddressProvider.await() } } } return response } fun handleDeepLink(request: Request) : Response { val response = Response() val firstAddressProvider: Deferred<String> = async { request.addressFetcherProviderA.getAddressFromLatLan(request.journeyStartLatLng) } val secondAddressProvider: Deferred<String> = async { request.addressFetcherProviderB.getAddressFromLatLan(request.journeyStartLatLng) } response.journeyStartAddress = runBlocking { select<String> { firstAddressProvider.onAwait { firstAddressProvider.await() } secondAddressProvider.onAwait { secondAddressProvider.await() } } } return response } -
15 Use-case 2 - Cont’: Build My Own Abstractions fun
<T> List<Deferred<T>>.waitForFirst() = runBlocking { select<T> { forEach { deferred -> deferred.onAwait { forEach { if(it != deferred) it.cancel() } deferred.await() } } } } fun fetchAddressWithProvider(addressFetcher: AddressFetcher, latLng: LatLng) = async { addressFetcher.getAddressFromLatLan(latLng) } -
16 Use-case 2 - Cont’: Use My Abstructions fun handleDeepLink(request:
Request) : Response { val response = Response() val firstAddressProvider: Deferred<String> = fetchAddressWithProvider(request.addressFetcherProviderA, request.journeyStartLatLng) val secondAddressProvider: Deferred<String> = fetchAddressWithProvider(request.addressFetcherProviderB, request.journeyStartLatLng) val deferredJobs = listOf(firstAddressProvider, secondAddressProvider) request.jobCanceler.jobs = deferredJobs response.journeyStartAddress = deferredJobs.waitForFirst() return response } -
17 Use-case 3: Observe walking path fun handleDeepLink(request: Request) :
Response { val response = Response() val walkingInfoStream = BroadcastChannel<WalkingInfo>(Channel.CONFLATED) val walkingPath = request.walkingInfoFetcher.fetch(request.journeyStartLatLng, request.pickupLatLng) launch { walkingInfoStream.send(walkingPath) } startTrackingLocationAndUpdateWalkingPath(walkingPath, walkingInfoStream) response.walkingInfoStream = walkingInfoStream return response } fun handleWalkingInfo(response: Response) { val stream = response.walkingInfoStream.openSubscription() stream.consume { let { channel -> launch { while (!channel.isClosedForReceive) { channel.receiveOrNull()?.let { showWalkingInfo(it) } } fun handleDeepLink(request: Request) : Response { val response = Response() val walkingInfoStream = BroadcastChannel<WalkingInfo>(Channel.CONFLATED) val walkingPath = request.walkingInfoFetcher.fetch(request.journeyStartLatLng, request.pickupLatLng) launch { walkingInfoStream.send(walkingPath) } startTrackingLocationAndUpdateWalkingPath(walkingPath, walkingInfoStream) response.walkingInfoStream = walkingInfoStream return response } fun handleWalkingInfo(response: Response) { val stream = response.walkingInfoStream.openSubscription() stream.consume { let { channel -> launch { while (!channel.isClosedForReceive) { channel.receiveOrNull()?.let { showWalkingInfo(it) } } } } -
18 Use-case 3 - Cont’: Build My Own Abstractions class
DataStream<out T>( private val subscription : ReceiveChannel<T> ) { fun notifyOnChange(block: (T) -> Unit) { launch { subscription.consume { let { channel -> while (!channel.isClosedForReceive) { channel.receiveOrNull()?.let { block(it) } } } } } } fun close() { subscription.cancel() } -
19 Use-case 3 - Cont’: Build My Own Abstractions class
DataStore<T : Any> { private val output = BroadcastChannel<T>(Channel.CONFLATED) fun setCurrentValue(value: T) { launch { output.send(value) } } fun shutdown() { launch { output.close() } } fun getDataStream() = DataStream(output.openSubscription()) } -
20 Use-case 3 - Cont’: Use My Abstructions fun handleDeepLink(request:
Request) : Response { val response = Response() val walkingInfoDataStore = DataStore<WalkingInfo>() val walkingPath = request.walkingInfoFetcher.fetch(request.journeyStartLatLng, request.pickupLatLng) walkingInfoDataStore.setCurrentValue(walkingPath) startTrackingLocationAndUpdateWalkingPath(walkingPath, walkingInfoDataStore) response.walkingInfoDataStore = walkingInfoDataStore return response } fun handleWalkingInfo(response: Response) { response.walkingInfoDataStore.getDataStream().notifyOnChange { showWalkingInfo(it) } } -
22 But we have RX…. - if all you have
is a hammer, everything looks like a nail fun login(credentials: Credentials): Single<UserID> fun loadUserData(userID: UserID): Single<UserData> fun showData(data: UserData) fun showUserInfo(credentials: Credentials) { login(credentials) .flatMap { loadUserData(it) } .doOnSuccess { showData(it) } .subscribe() } suspend fun login(credentials: Credentials): UserID suspend fun loadUserData(userID: UserID): UserData fun showData(data: UserData) suspend fun showUserInfo(credentials: Credentials) { val userID = login(credentials) val userData = loadUserData(userID) showData(userData) } -
Why to use coroutines The code inside coroutine can be
expressed sequentially Easy to write our own abstraction and adjust the use of it to our architecture Kotlinx.corout ines have out of the box integration with other major frameworks like RX 1 / 2, Java 8 stream and more Get ready for multi-platform world, coroutines might be the right way to write common asynchronous code