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

droidcon Transylvania - Kotlin Coroutines

ArthurNagy
September 24, 2018

droidcon Transylvania - Kotlin Coroutines

We face challenges with threading on a daily basis. There are too many ways to do it, too many options. Do I use an AsyncTaks, a Runnable or RxJava, what do I do? All these available APIs lead to an almost inevitable callback hell.
This talk will introduce Kotlin Coroutines as an alternative solution to this problem. We’re going to explore simple ways to perform long-running tasks and computation heavy operations in an asynchronous way, without freezing or crashing your app, while keeping the code clean and readable at the same time.

ArthurNagy

September 24, 2018
Tweet

More Decks by ArthurNagy

Other Decks in Programming

Transcript

  1. What are coroutines “coroutines are computations that can be suspended

    without blocking a thread.” • Not a new concept, introduced in the `60s • Adopted by multiple programming languages • Coroutines in Kotlin: ◦ minimal low-level APIs in its standard library ◦ kotlinx.coroutines libraries
  2. What are coroutines • Terminology : ◦ Coroutine ◦ Suspending

    function ◦ Coroutine builder ◦ Continuation ◦ Coroutine context
  3. Coroutine • An instance of suspendable computation • Similar to

    a thread (light-weight thread) • Also similar to a future or promise
  4. Suspending functions • Functions which suspend a coroutine execution •

    suspend keyword • Suspending functions can be used: ◦ Inside other functions marked with the suspend keyword ◦ Inside a coroutine • suspendCoroutine ◦ Bridge between coroutine suspension and callback world
  5. Suspending functions /** * Obtains the current continuation instance *

    inside suspend functions and suspends * currently running coroutine. */ suspend inline fun <T> suspendCoroutine( crossinline block: (Continuation<T>) -> Unit): T =
  6. Suspending functions public interface Continuation<in T> { public val context:

    CoroutineContext public fun resume(value: T) public fun resumeWithException(exception: Throwable) }
  7. Suspending functions suspend fun firstFun(): ResultType = suspendCoroutine { continuation

    -> // do some heavy work // resume the coroutine continuation with a result: continuation.resume(result) // or if there was an error: continuation.resumeWithException(exception) }
  8. Coroutine builders • Functions which start a coroutine • Bridge

    between non-coroutine & coroutine world • Most common coroutine builders: ◦ launch ◦ async ◦ withContext
  9. Coroutine builders launch { // everything inside happens on //

    a background thread pool } // continue app operations
  10. Coroutine builders fun launch( context: CoroutineContext = Dispatchers.Default, start: CoroutineStart

    = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job
  11. Coroutine builders val result: Deferred<T> = async { // do

    stuff on the background thread // which returns a value T when done } result.await()
  12. Coroutine builders val result: T = withContext(BACKGROUND) { // do

    stuff on the background thread // which returns a value T when done }
  13. Coroutines in Android kotlin { experimental { coroutines 'enable' }

    } dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.25.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.25.0' }
  14. class BlurActivity : AppCompatActivity() { ... override fun onActivityResult(requestC: Int,

    resultC: Int, data: Intent?) { val imageUri = data?.data launch(UI) { val resultImage = withContext(BACKGROUND) { blurImage(imageUri) } viewBinding.image.setImageURI(resultImage) viewModel.uploadImage(resultImage) } } ... }
  15. suspend fun blurImage(imageUri: Uri): Uri = suspendCoroutine { continuation ->

    try { val imageBitmap = getBitmapFromImageUri(imageUri) val blurredImage = applyBlurOnBitmap(imageBitmap) val blurredImageFile = saveBitmapToTemporaryFile(blurredImage) continuation.resume(blurredImageFile) } catch (e: Exception) { continuation.resumeWithException(e) } }
  16. class BlurActivity : AppCompatActivity() { ... override fun onActivityResult(requestC: Int,

    resultC: Int, data: Intent?) { val imageUri = data?.data launch(UI) { val resultImage = withContext(BACKGROUND) { blurImage(imageUri) } viewBinding.image.setImageURI(resultImage) viewModel.uploadImage(resultImage) } } ... }
  17. class BlurViewModel( private val restApiService: RestApiService ) : ViewModel() {

    ... fun uploadImage(imageUri: Uri) { // update UI state, i.e: show progress, etc launch(BACKGROUND) { val imageFile = File(imageUri.path) val imageFilePart = createImagePart(imageFile) val response = restApiService.uploadBlurredImage(imageFilePart).await() // use our response to update UI state, etc. } } ... }
  18. restApiService.uploadBlurredImage(imageFilePart).enqueue(Callback<> { override fun onResponse(call, response) { if (response.isSuccessful){ val

    blurResponse = response.body()!! // use our response to update UI state, etc. } } override fun onFailure(call, throwable) { } })
  19. suspend fun <T> Call<T>.await(): T = suspendCoroutine { continuation ->

    enqueue(object: Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { if (response.isSuccessful) { continuation.resume(response.body()!!) } else { continuation.resumeWithException(RequestException(response.message())) } } override fun onFailure(call: Call<T>, t: Throwable) { continuation.resumeWithException(t) } }) }
  20. class BlurActivity : AppCompatActivity() { ... launch(UI) { val resultImage

    = withContext(BACKGROUND) { blurImage(imageUri) } } ... }
  21. class BlurActivity : AppCompatActivity() { ... val job = launch(UI)

    { val resultImage = withContext(BACKGROUND) { blurImage(imageUri) } } ... override fun onStop() { super.onStop() job.cancel() } }
  22. class CoroutineLifecycleObserver : LifecycleObserver { var parentJob: Job = Job()

    @OnLifecycleEvent(Lifecycle.Event.ON_START) fun start() { if (parentJob.isCompleted) { parentJob = Job() } } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) fun cancel() { if (parentJob.isActive) { parentJob.cancel() } } } gist.github.com/chrisbanes/152d325211401843326068c24e0d11b1
  23. class BlurActivity : AppCompatActivity() { private val coroutineObserver = CoroutineLifecycleObserver()

    override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... lifecycle.addObserver(coroutineObserver) } launch(context = UI, parent = coroutineObserver.job) { val resultImage = withContext(BACKGROUND) { blurImage(imageUri) } } }
  24. open class CoroutineViewModel : ViewModel() { private val parentJob =

    Job() override fun onCleared() { super.onCleared() parentJob.cancel() } protected fun launchWithParent( context: CoroutineContext = BACKGROUND, block: suspend CoroutineScope.() -> Unit ) = launch(context = context, parent = parentJob, block = block) }
  25. class BlurViewModel( private val restApiService: RestApiService ) : CoroutineViewModel() {

    fun uploadImage(imageUri: Uri) { launchWithParent(BACKGROUND) { ... } } }
  26. asyncOperation(object: Callback { fun onSuccess(result) { anotherOne(result, object: Callback {

    fun onSuccess(secondResult){ lastOne(secondResult, object: Callback { fun onSuccess(thirdResult){ useResult(thirdResult) } fun onError(exception){ handleError(exception) } }) } fun onError(exception){ handleError(exception) } }) } fun onError(exception) { handleError(exception) } })
  27. Why use coroutines • Asynchronous code sequentially • No more

    callback-hell • “It’s experimental, we can’t use it!” • “We already have so many options, why use this one?!”
  28. Why use coroutines val blurAsyncTask = object : AsyncTask<Uri, Int,

    Uri>() { override fun doInBackground(vararg params: Uri?): Uri { return blurImage(params) } override fun onPostExecute(result: Uri?) { viewBinding.image.setImageURI(result) viewModel.uploadImage(result) } } blurAsyncTask.execute(imageUri)
  29. Why use coroutines private val executors = Executors.newCachedThreadPool() fun uploadImage(imageUri:

    Uri) { executors.execute { val imageFile = File(imageUri.path) val imageFilePart = createImagePart(imageFile) restApiService.uploadBlurredImage(imageFilePart).enqueue(Callback<> { override fun onResponse(response: Response<>) { // use the response to update state, etc. } override fun onFailure(t: Throwable) { } }) } }
  30. Why use coroutines disposables.add(Single .fromCallable { blurImage(imageUri) } .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread())

    .subscribe({ resultImage -> viewBinding.image.setImageURI(resultImage) viewModel.uploadImage(resultImage) },{ error -> }) )
  31. Why use coroutines fun uploadImage(imageUri: Uri) { disposables.add(Single.fromCallable { val

    imageFile = File(imageUri.path) createImagePart(imageFile) }.flatMap(restApiService::uploadBlurredImage) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ blurResponse -> //update UI state, etc. }, { error -> }) ) }
  32. • There’s a lot more: ◦ Exception handling, channels, actors,

    etc. • Resources: ◦ kotlinlang.org/docs/reference/coroutines.html ◦ github.com/Kotlin/kotlin-coroutines/blob/master/kotlin-coroutines-infor mal.md ◦ github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md ◦ gist.github.com/chrisbanes/152d325211401843326068c24e0d11b1 ◦ github.com/gildor/kotlin-coroutines-retrofit