Coroutines in Practice
Mohit Sarveiya
/heyitsmohit
twitter.com
Slide 2
Slide 2 text
Coroutines in Practice
• Convert feature from RxJava to Coroutines
• Scopes
• Context & Dispatchers
Slide 3
Slide 3 text
Why Coroutines?
• Language level support
• Multiplatform support - In Progress
• Scopes
Slide 4
Slide 4 text
Mac OS App
(VIPER)
Android App
(MVP)
Libraries
Problem
iOS App
(VIPER)
Libraries Libraries
Slide 5
Slide 5 text
Mac OS App
(VIPER)
Android App
(MVP)
Async
Logic
Goal
iOS App
(VIPER)
Slide 6
Slide 6 text
TV App
Android App
Document
Feature
Document Feature
Slide 7
Slide 7 text
Document Feature
• Gets HTML text from API
• Privacy policy
• TOS
• Pull to refresh
• Error state
Slide 8
Slide 8 text
Document Feature
• Simple feature
• Uses Model View Presenter
• Playground to show new patterns & libraries
Slide 9
Slide 9 text
Document Feature
View Presenter Model
Slide 10
Slide 10 text
Document Feature
View Presenter Model
Attached
Slide 11
Slide 11 text
Document Feature
View Presenter Model
Attached getDocument(uri: String)
Slide 12
Slide 12 text
Document Feature
View Presenter Model
Document
Slide 13
Slide 13 text
data class Document(
@Json(name = "html")
val html: String? = null
)
Document Model
Slide 14
Slide 14 text
Document Feature
View Presenter Model
showHtml(html:String)
Document
Slide 15
Slide 15 text
Feature in RxJava
Slide 16
Slide 16 text
interface DocumentContract {
interface Presenter {
fun onViewAttached(view: DocumentContract.View)
fun onViewDetached()
fun onPullToRefresh()
}
}
MVP Contract
Slide 17
Slide 17 text
interface DocumentContract {
interface View {
fun showLoadingState()
fun showErrorState()
fun showRawHtml(rawHtml: String)
}
}
MVP Contract
Slide 18
Slide 18 text
interface Model {
fun fetchHtmlDocument(): Single
}
Model
Slide 19
Slide 19 text
sealed class DocumentResponse {
data class Success(val data: String) : DocumentResponse()
data class Error(val error: DocumentRequestError) : DocumentResponse()
}
API Response
Slide 20
Slide 20 text
class DocumentPresenter(
val documentModel: DocumentContract.Model,
val backgroundScheduler: Scheduler,
val foregroundScheduler: Scheduler
) : Presenter {
val compositeDisposable = CompositeDisposable()
var fetchDocumentDisposable: Disposable? = null
}
Presenter
Slide 21
Slide 21 text
class DocumentPresenter(…) : Presenter {
}
Presenter
override fun onViewAttached(view: DocumentContract.View) {
documentView = view
fetchHtmlDocument()
}
override fun onPullToRefresh() = fetchHtmlDocument()
Slide 22
Slide 22 text
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()
}
}
}
}
}
Slide 23
Slide 23 text
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()
}
}
}
}
}
Slide 24
Slide 24 text
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()
}
}
}
}
}
Slide 25
Slide 25 text
Cancellation
val compositeDisposable = CompositeDisposable()
override fun onViewDetached() {
compositeDisposable.clear()
}
Slide 26
Slide 26 text
View
class HtmlDocumentActivity : DocumentContract.View {
swiperefreshlayout!?.setOnRefreshListener(documentPresenter!::onPullToRefresh)
override fun onCreate() {
documentPresenter.onViewAttached(this)
}
override fun onStop() {
documentPresenter.onViewDetached()
}
…
}
Coroutine
GlobalScope.launch {
val document: Document = model.getDocument(uri)
view.showHtml(document.html)
}
Slide 30
Slide 30 text
interface Model {
suspend fun getDocument(): Document
}
Model
Slide 31
Slide 31 text
Suspend keyword
• Allows method to suspend and resume computation
• Non blocking
Slide 32
Slide 32 text
Using Suspend Method
fun onCreate() {
val document: Document = model.getDocument(uri)
}
Slide 33
Slide 33 text
Where could you call suspending functions?
• Other suspend functions
• Coroutine builders
Using Suspend Method
fun onCreate() {
val document: Document = model.getDocument(uri)
}
Slide 34
Slide 34 text
Coroutine Builders
• Launch
• Async
• RunBlocking
• Actor
Slide 35
Slide 35 text
How to use coroutine builders
1. Need to create a scope
2. Call any of the builder methods on the scope.
Slide 36
Slide 36 text
What is a Scope?
• Specify the lifetime of async operations
• Group together async operations
Slide 37
Slide 37 text
Global Scope
GlobalScope.launch {
val document: Document = model.getDocument(uri)
}
Depends on app’s life time
Slide 38
Slide 38 text
Job
val job: Job = GlobalScope.launch {
val document: Document = model.getDocument(uri)
}
job.cancel()
Slide 39
Slide 39 text
Job States
• Active
• Completing
• Cancelling
• Completed
Slide 40
Slide 40 text
Job
val job: Job = GlobalScope.launch {
val document: Document = model.getDocument(uri)
}
job.isActive
job.isCompleted
job.isCancelled
Slide 41
Slide 41 text
Threading
val job: Job = GlobalScope.launch {
val document: Document = model.getDocument(uri)
}
Which thread will it run on?
Slide 42
Slide 42 text
Context
object GlobalScope : CoroutineScope {
val coroutineContext: CoroutineContext
}
Slide 43
Slide 43 text
Context
Set of elements contains
• Job
• Dispatcher
Slide 44
Slide 44 text
Dispatcher
Specifies which thread the coroutines will run on.
• Default Dispatcher
• IO
• Main
• Unconfined
Slide 45
Slide 45 text
Threading
GlobalScope.launch {
val document: Document = model.getDocument(uri)
}
Default Dispatcher Thread Pool
Slide 46
Slide 46 text
Threading
GlobalScope(Dispatchers.IO).launch {
val document: Document = model.getDocument(uri)
}
Slide 47
Slide 47 text
Main Thread
val scope = CoroutineScope(Dispatchers.Main)
scope.launch {
}
Slide 48
Slide 48 text
Main Thread
val scope = CoroutineScope(Dispatchers.Main)
scope.launch {
val document = model.getDocument(uri)
}
Network On Main Thread Exception
Slide 49
Slide 49 text
Switch Threads
val scope = CoroutineScope(Dispatchers.Main)
scope.launch {
val document = withContext(Dispatchers.IO) {
return model.getDocument(uri)
}
}
Slide 50
Slide 50 text
Document Feature
View Presenter Model
Slide 51
Slide 51 text
Document Feature
View Presenter Model
Slide 52
Slide 52 text
Updating Model
1. Bridge callback to coroutines in the app
2. Update SDK to use Coroutines with Retrofit
• Suspend function
• Deferred type
Slide 53
Slide 53 text
Updating Model
1. Bridge callback to coroutines in the app
2. Update SDK to use Coroutines with Retrofit
• Suspend function
• Deferred type
Slide 54
Slide 54 text
SDK
Model API
Networking Library
Slide 55
Slide 55 text
No content
Slide 56
Slide 56 text
class VimeoClient {
Call getDocument(String uri, VimeoCallback callback) {
Call call = vimeoService.getDocument(uri);
call.enqueue(callback);
return call;
}
…
}
Getting data from the API
Slide 57
Slide 57 text
suspend fun suspendCancellableCoroutine(
block: (CancellableContinuation) !-> Unit
): T
Bridge Callbacks to Coroutines
Slide 58
Slide 58 text
interface CancellableContinuation {
fun resumeWith(result: Result)
fun resume(value: T)
fun resumeWithException(exception: Throwable)
}
Bridge Callbacks to Coroutines
Slide 59
Slide 59 text
suspend fun getDocument(uri: String): DocumentResponse {
return suspendCancellableCoroutine> { cont !->
}
}
Bridge Callbacks to Coroutines
Slide 60
Slide 60 text
suspend fun getDocument(uri: String): DocumentResponse {
return suspendCancellableCoroutine> { cont !->
}
}
Bridge Callbacks to Coroutines
How do we resume on success and failure?
Slide 61
Slide 61 text
suspend fun getDocument(uri: String): DocumentResponse {
return suspendCancellableCoroutine> { cont !->
val call = vimeoClient.getDocument(uri, object :VimeoCallback() {
override fun success(document: Document) {
cont.resume(Success(document))
}
override fun failure(error: VimeoError) {
cont.resume(Error(error))
}
})
}
Bridge Callbacks to Coroutines
Slide 62
Slide 62 text
Problems
• Boiler plate code
• Repetitive
Slide 63
Slide 63 text
Factory
Callback Function Suspend Function
Improvement
Creating a Factory
interface SuspendFunctionFactory {
fun convertToSuspendFunction (
fn: (A, VimeoCallback) !-> Call
): suspend (A)!-> Result
}
Slide 66
Slide 66 text
Creating a Factory
fun convertToSuspendFunction (
fn: (A, VimeoCallback) !-> Call
): suspend (A)!-> Result = { a !-> }
Function Reference
Slide 67
Slide 67 text
Creating a Factory
fun convertToSuspendFunction (
fn: (A, VimeoCallback) !-> Call
): suspend (A)!-> Result = { a !-> }
Suspending Function
Slide 68
Slide 68 text
Creating a Factory
fun convertToSuspendFunction (
fn: (A, VimeoCallback) !-> Call
): suspend (A)!-> Result = { a !->
suspendCancellableCoroutine { cont !->
val call = fn(a, object : VimeoCallback() {
override fun success(t: T) {
cont.resume(Success(t))
}
override fun failure(error: VimeoError) {
cont.resume(Error(error))
}
})
}
}
Slide 69
Slide 69 text
Factory
Callback Function Suspend Function
Reusable Factory
Slide 70
Slide 70 text
Updating Model
class DocumentModel(
val vimeoClient: VimeoClient,
val factory: SuspendFunctionFactory
) {
suspend fun getDocument(): Result =
}
Slide 71
Slide 71 text
Updating Model
class DocumentModel(
val vimeoClient: VimeoClient,
val factory: SuspendFunctionFactory,
val requestDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
suspend fun getDocument(): Result =
}
}
Slide 72
Slide 72 text
Updating Model
class DocumentModel(
val vimeoClient: VimeoClient,
val factory: SuspendFunctionFactory,
val requestDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
suspend fun getDocument() = withContext(requestDispatcher) {
}
}
Slide 73
Slide 73 text
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()
}
}
Slide 74
Slide 74 text
Updating Model
1. Bridge callback to coroutines in the app
2. Update SDK to use Coroutines with Retrofit
• Suspend function
• Deferred type
Slide 75
Slide 75 text
No content
Slide 76
Slide 76 text
repositories {
maven {
url 'https:!//oss.sonatype.org/content/repositories/snapshots/'
}
google()
jcenter()
}
Using Coroutines with Retrofit
build.gradle
Slide 77
Slide 77 text
!// 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
Slide 78
Slide 78 text
interface VimeoService {
@GET
fun getDocument(@Url uri: String): Call
…
}
Using Coroutines with Retrofit
Slide 79
Slide 79 text
interface VimeoService {
@GET
fun getDocument(@Url uri: String): Call
…
}
Using Coroutines with Retrofit
Slide 80
Slide 80 text
interface VimeoService {
@GET
fun getDocument(@Url uri: String): Document
…
}
Using Coroutines with Retrofit
Slide 81
Slide 81 text
interface VimeoService {
@GET
suspend getDocument(@Url uri: String): Call
…
}
Using Coroutines with Retrofit
Slide 82
Slide 82 text
Using Coroutines with Retrofit
class VimeoClient {
suspend fun getDocument(uri: String): Document
…
}
Slide 83
Slide 83 text
Updating Model
1. Bridge callback to coroutines in the app
2. Update SDK to use Coroutines with Retrofit
• Suspend function
• Deferred type
Slide 84
Slide 84 text
Using Deferred Type
• Kotlin’s future type
• It has a associated type
Slide 85
Slide 85 text
No content
Slide 86
Slide 86 text
Using Deferred Type
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
Slide 87
Slide 87 text
Using Deferred Type
interface VimeoService {
@GET
fun getDocument(@Url uri: String): Deferred
…
}
Slide 88
Slide 88 text
Using Deferred Type
val vimeoClient = VimeoClient()
val deferred: Deferred = vimeoClient.getDocument(uri)
val document = deferred.await()
Slide 89
Slide 89 text
Update Presenter
View Presenter Model
Slide 90
Slide 90 text
Update Presenter Approach
• Create custom scope in presenter
• Launch it on the main thread
• Do context switch to make request
Slide 91
Slide 91 text
Inject Dispatcher
class DocumentPresenter(
val documentModel: DocumentContract.Model,
val uiDispatcher: CoroutineDispatcher
) {
}
Slide 92
Slide 92 text
Create scope
class DocumentPresenter(
val documentModel: DocumentContract.Model,
val uiDispatcher: CoroutineDispatcher
) {
var uiScope: CoroutineScope = CoroutineScope(uiDispatcher)
}
Slide 93
Slide 93 text
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()
}
}
Launch Coroutine
fun fetchHtmlDocument() {
fetchDocumentJob = uiScope.launch {
val result: Result = documentModel.getDocument()
when (result) {
is DocumentRequestResult.Success !->
documentView!?.showRawHtml(result.document)
is DocumentRequestResult.DocumentRequestError !-> {
documentView!?.showGenericErrorState()
}
}
}
Slide 96
Slide 96 text
Cancellation
class DocumentPresenter(
val documentModel: DocumentContract.Model,
val uiDispatcher: CoroutineDispatcher,
) {
…
override fun onViewDetached() {
uiScope.coroutineContext[Job]!?.cancel()
}
}