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

Lean way write asynchronous code with Kotlin’s ...

Lean way write asynchronous code with Kotlin’s coroutines

LEAN WAY WRITE ASYNCHRONOUS CODE WITH KOTLIN’S COROUTINES
A very common scenario for a modern app is the need to perform several background tasks in parallel. Sometimes we want to wait for the response of all or some of them and this can be cumbersome to develop. We will see how can we use Kotlin's coroutines power and simplicity to run and manage several tasks in parallel in a very convenient way. This could also be a great and native alternative to RxJava without its steep learning curve and complexity.

Ronen Sabag

June 26, 2018
Tweet

Other Decks in Technology

Transcript

  1. • Asynchronous code • What are Coroutines • Coroutines in

    practice • Why to use Coroutines AGENDA 2
  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
  3. 5 Asynchronous code - Observation vs Synchronization Interactor Network Client

    UI Repository fetchData(viewCb: (Status) -> Unit)
  4. 12 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
  5. 17 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 }
  6. 18 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 }
  7. 19 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() } } }
  8. 20 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 }
  9. 21 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 }
  10. 22 Use-case 2: Make two network calls in parallel and

    wait for first 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) }
  11. 23 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) }
  12. 24 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 }
  13. 25 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) } }
  14. 26 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) } } } }
  15. 27 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() }
  16. 28 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()) }
  17. 29 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) } }
  18. 31 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) }
  19. 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