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

Coroutines in Practice

Mohit S
April 17, 2019

Coroutines in Practice

To implement asynchronous logic, many devs use RxJava. But, Kotlin provides us with another toolset called Coroutines. I have been integrating Coroutines at Vimeo. In this talk, I will share with you my journey and the challenges I encountered. We will look at how to handle simple to complex uses cases with Coroutines. Some of these use cases are bridging a callback based SDK to coroutines, using Coroutines with MVP, handling polling and writing tests. I'll also show the usage of actors, channels and supervisor scope in real examples. Please join me to learn about my ongoing journey to introduce coroutines in an app.

Mohit S

April 17, 2019
Tweet

More Decks by Mohit S

Other Decks in Programming

Transcript

  1. Document Feature • Gets HTML text from API • Privacy

    policy • TOS • Pull to refresh • Error state
  2. Document Feature • Simple feature • Uses Model View Presenter

    • Playground to show new patterns & libraries
  3. sealed class DocumentResponse { data class Success(val data: String) :

    DocumentResponse() data class Error(val error: DocumentRequestError) : DocumentResponse() } API Response
  4. class DocumentPresenter( val documentModel: DocumentContract.Model, val backgroundScheduler: Scheduler, val foregroundScheduler:

    Scheduler ) : Presenter<View> { val compositeDisposable = CompositeDisposable() var fetchDocumentDisposable: Disposable? = null } Presenter
  5. class DocumentPresenter(…) : Presenter<View> { } Presenter override fun onViewAttached(view:

    DocumentContract.View) { documentView = view fetchHtmlDocument() } override fun onPullToRefresh() = fetchHtmlDocument()
  6. Presenter fun fetchHtmlDocument() { fetchDocumentDisposable = documentModel.fetchHtmlDocument() .subscribeOn(backgroundScheduler) .observeOn(foregroundScheduler) .subscribe

    { response !-> when (response) { is DocumentResponse.Success !-> 
 documentView!?.showRawHtml(response.data) is DocumentResponse.Error !-> { if (response.documentRequestError !== NoNetwork) { documentView!?.showNoNetworkErrorState() } else { documentView!?.showGenericErrorState() } } } } }
  7. Threading fun fetchHtmlDocument() { fetchDocumentDisposable = documentModel.fetchHtmlDocument() .subscribeOn(backgroundScheduler) .observeOn(foregroundScheduler) .subscribe

    { response !-> when (response) { is DocumentResponse.Success !-> 
 documentView!?.showRawHtml(response.data) is DocumentResponse.Error !-> { if (response.documentRequestError !== NoNetwork) { documentView!?.showNoNetworkErrorState() } else { documentView!?.showGenericErrorState() } } } } }
  8. Handle Response fun fetchHtmlDocument() { fetchDocumentDisposable = documentModel.fetchHtmlDocument() .subscribeOn(backgroundScheduler) .observeOn(foregroundScheduler)

    .subscribe { response !-> when (response) { is DocumentResponse.Success !-> 
 documentView!?.showRawHtml(response.data) is DocumentResponse.Error !-> { if (response.documentRequestError !== NoNetwork) { documentView!?.showNoNetworkErrorState() } else { documentView!?.showGenericErrorState() } } } } }
  9. View class HtmlDocumentActivity : DocumentContract.View { swiperefreshlayout!?.setOnRefreshListener(documentPresenter!::onPullToRefresh) override fun onCreate()

    { documentPresenter.onViewAttached(this) } override fun onStop() { documentPresenter.onViewDetached() } … }
  10. Where could you call suspending functions? • Other suspend functions

    • Coroutine builders Using Suspend Method fun onCreate() { val document: Document = model.getDocument(uri) }
  11. How to use coroutine builders 1. Need to create a

    scope
 2. Call any of the builder methods on the scope.
  12. What is a Scope? • Specify the lifetime of async

    operations • Group together async operations
  13. Job val job: Job = GlobalScope.launch { val document: Document

    = model.getDocument(uri) } job.cancel()
  14. Job val job: Job = GlobalScope.launch { val document: Document

    = model.getDocument(uri) } job.isActive job.isCompleted job.isCancelled
  15. Threading val job: Job = GlobalScope.launch { val document: Document

    = model.getDocument(uri) } Which thread will it run on?
  16. Dispatcher Specifies which thread the coroutines will run on. •

    Default Dispatcher • IO • Main • Unconfined
  17. Main Thread val scope = CoroutineScope(Dispatchers.Main) scope.launch { val document

    = model.getDocument(uri) } Network On Main Thread Exception
  18. Switch Threads val scope = CoroutineScope(Dispatchers.Main) scope.launch { val document

    = withContext(Dispatchers.IO) { 
 return model.getDocument(uri) } }
  19. Updating Model 1. Bridge callback to coroutines in the app


    2. Update SDK to use Coroutines with Retrofit • Suspend function • Deferred type
  20. Updating Model 1. Bridge callback to coroutines in the app


    2. Update SDK to use Coroutines with Retrofit • Suspend function • Deferred type
  21. class VimeoClient { Call<Document> getDocument(String uri, VimeoCallback<Document> callback) { Call<Document>

    call = vimeoService.getDocument(uri); call.enqueue(callback); return call; } … } Getting data from the API
  22. interface CancellableContinuation<in T> { fun resumeWith(result: Result<T>) fun resume(value: T)


    fun resumeWithException(exception: Throwable) } Bridge Callbacks to Coroutines
  23. suspend fun getDocument(uri: String): DocumentResponse<Document> { return suspendCancellableCoroutine<Result<Document!>> { cont

    !-> val call = vimeoClient.getDocument(uri, object :VimeoCallback<Document>() { override fun success(document: Document) { cont.resume(Success(document)) } override fun failure(error: VimeoError) { cont.resume(Error(error)) } }) } Bridge Callbacks to Coroutines
  24. Creating a Factory class VimeoClient { Call<Document> getDocument(String uri, VimeoCallback<Document>

    callback) Call<Comment> comment(String uri, String comment, String password, VimeoCallback<Comment> callback) }
  25. Creating a Factory interface SuspendFunctionFactory { fun <A, T> convertToSuspendFunction

    ( fn: (A, VimeoCallback<T>) !-> Call<T> ): suspend (A)!-> Result<T> }
  26. Creating a Factory fun <A, T> convertToSuspendFunction ( fn: (A,

    VimeoCallback<T>) !-> Call<T> ): suspend (A)!-> Result<T> = { a !-> } Function Reference
  27. Creating a Factory fun <A, T> convertToSuspendFunction ( fn: (A,

    VimeoCallback<T>) !-> Call<T> ): suspend (A)!-> Result<T> = { a !-> } Suspending Function
  28. Creating a Factory fun <A, T> convertToSuspendFunction ( fn: (A,

    VimeoCallback<T>) !-> Call<T> ): suspend (A)!-> Result<T> = { a !-> suspendCancellableCoroutine { cont !-> val call = fn(a, object : VimeoCallback<T>() { override fun success(t: T) { cont.resume(Success(t)) } override fun failure(error: VimeoError) { cont.resume(Error(error)) } }) }
 }
  29. Updating Model class DocumentModel(
 val vimeoClient: VimeoClient, val factory: SuspendFunctionFactory,

    val requestDispatcher: CoroutineDispatcher = Dispatchers.IO ) { suspend fun getDocument(): Result<Document> = 
 
 } }
  30. Updating Model class DocumentModel(
 val vimeoClient: VimeoClient, val factory: SuspendFunctionFactory,

    val requestDispatcher: CoroutineDispatcher = Dispatchers.IO ) { suspend fun getDocument() = withContext(requestDispatcher) {
 
 } }
  31. Updating Model suspend fun getDocument() = withContext(requestDispatcher) {
 result =

    factory.convertToSuspendFunction(vimeoClient!::getDocument)(uri) return when (result) { is VimeoApiResponse.Success !-> result.data.html.let { Success(it) } is VimeoApiResponse.Failure !-> Error() 
 } }
  32. Updating Model 1. Bridge callback to coroutines in the app


    2. Update SDK to use Coroutines with Retrofit • Suspend function • Deferred type
  33. !// Retrofit version 2.6.0 SNAPSHOT implementation 'com.squareup.retrofit2:retrofit:2.6.0-SNAPSHOT' implementation ‘com.squareup.retrofit2:converter-moshi:2.6.0-SNAPSHOT' !//

    Coroutines implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.0' Using Coroutines with Retrofit build.gradle
  34. Updating Model 1. Bridge callback to coroutines in the app


    2. Update SDK to use Coroutines with Retrofit • Suspend function • Deferred type
  35. Using Deferred Type val vimeoClient = VimeoClient() val deferred: Deferred<Document>

    = vimeoClient.getDocument(uri) val document = deferred.await()
  36. Update Presenter Approach • Create custom scope in presenter •

    Launch it on the main thread • Do context switch to make request
  37. Update Presenter class DocumentPresenter( val documentModel: DocumentContract.Model, val uiDispatcher: CoroutineDispatcher,

    ) { var uiScope: CoroutineScope = CoroutineScope(uiDispatcher) var fetchDocumentJob: Job? = null override fun onViewAttached(view: DocumentContract.View) { documentView = view fetchHtmlDocument() } }
  38. Launch Coroutine fun fetchHtmlDocument() { fetchDocumentJob = uiScope.launch { val

    result: Result<Document> = documentModel.getDocument() when (result) { is DocumentRequestResult.Success !-> 
 documentView!?.showRawHtml(result.document) is DocumentRequestResult.DocumentRequestError !-> { documentView!?.showGenericErrorState() } } }
  39. Cancellation class DocumentPresenter( val documentModel: DocumentContract.Model, val uiDispatcher: CoroutineDispatcher, )

    { … override fun onViewDetached() { uiScope.coroutineContext[Job]!?.cancel() } }
  40. Coroutine Extensions val ViewModel.viewModelScope: CoroutineScope get() { val scope: CoroutineScope?

    = this.getTag(JOB_KEY) if (scope !!= null) { return scope } return setTagIfAbsent(JOB_KEY, CloseableCoroutineScope(Job() + Dispatchers.Main)) }