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

Ronen Sabag - Lean async code with Kotlin’s coroutines

Ronen Sabag - Lean async code with Kotlin’s coroutines

droidcon Berlin

July 17, 2018
Tweet

More Decks by droidcon Berlin

Other Decks in Programming

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 enqueuRequest(cb:(Data) -> Unit) saveData(Data) fetchData(viewCb: (Status) -> Unit) observeData(ob: (Data) -> Unit) viewCb(OK) cb(data)
  4. 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
  5. 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 }
  6. 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 }
  7. 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() } } }
  8. 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 }
  9. 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 }
  10. 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) }
  11. 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 }
  12. 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) } } } }
  13. 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() }
  14. 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()) }
  15. 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) } }
  16. 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) }
  17. 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