Cách dùng Kotlin Coroutine trong Android
Với việc dùng thư viện kotlinx.coroutines thì các bạn có thể chạy một coroutine mới bằng cách sử dụng hàm launch hoặc async. Về mặt khái niệm thì async gần giống như launch. Nó khởi chạy một coroutine riêng biệt như là một light-weight thread mà chạy đồng thời với các coroutine khác. Điểm khác ...
Với việc dùng thư viện kotlinx.coroutines thì các bạn có thể chạy một coroutine mới bằng cách sử dụng hàm launch hoặc async.
Về mặt khái niệm thì async gần giống như launch. Nó khởi chạy một coroutine riêng biệt như là một light-weight thread mà chạy đồng thời với các coroutine khác. Điểm khác biệt ở đây là launch trả về một Job và không có mang bất kỳ giá trị nào, trong khi đó thì async trả về một Defered là một light-weight non-blocking future mà đại diện cho một lời hứa sẽ cung cấp kết quả sau. Bạn có thể dùng .await() với giá trị defered để nhận kết quả thực sự. Defered cũng là một Job nên bạn có thể cancel nó khi cần thiết.
launch
public actual fun launch( context: CoroutineContext = DefaultDispatcher, start: CoroutineStart = CoroutineStart.DEFAULT, parent: Job? = null, block: suspend CoroutineScope.() -> Unit ): Job { val newContext = newCoroutineContext(context, parent) val coroutine = if (start.isLazy) LazyStandaloneCoroutine(newContext, block) else StandaloneCoroutine(newContext, active = true) coroutine.start(start, coroutine, block) return coroutine }
async
public actual fun <T> async( context: CoroutineContext = DefaultDispatcher, start: CoroutineStart = CoroutineStart.DEFAULT, parent: Job? = null, block: suspend CoroutineScope.() -> T ): Deferred<T> { val newContext = newCoroutineContext(context, parent) val coroutine = if (start.isLazy) LazyDeferredCoroutine(newContext, block) else DeferredCoroutine<T>(newContext, active = true) coroutine.start(start, coroutine, block) return coroutine }
Khi khởi chạy coroutine mà không có context thì mặc định nó sẽ dùng context DefaultDispatcher và theo định nghĩa trong doc thì đó là CommonPool
public actual val DefaultDispatcher: CoroutineDispatcher = CommonPool
Nên các khai báo sau là tương đương
launch {} and launch(CommonPool) {} async {} and async(CommonPool) {}
Khi xảy ra exception:
- trong launch thì nó sẽ được coi như là một exception chưa được bắt trong luồng xử lý của ứng dụng và có thể gây crash.
- trong async sẽ được lưu trữ trong kết quả là Defered và không truyền đi đâu nữa, nó sẽ bị ngầm bỏ qua nếu không được xử lý.
Coroutine Context
Trong Android chúng ta thường sử dụng hai context sau:
- uiContext (hay UI): xử lý code trên Android main UI thread (cho coroutine cha).
- bgContext (hay CommonPool): xử lý code trên background thread (cho coroutine con).
// android ui main thread val uiContext: CoroutineContext = UI // common pool of shared thread val bgContext: CoroutineContext = CommonPool
Trong các ví dụ tiếp theo chúng ta sẽ sử dụng CommonPool cho bgContext, nó có giới hạn số thread có thể chạy song song theo giá trị Runtime.getRuntime.availableProcessors()-1. Nên nếu một công việc được đặt lịch, nhưng tất cả luồng đang được sử sụng thì nó sẽ vào hàng đợi.
launch + async (thực hiện task)
Coroutine cha khởi chạy qua launch với UI context.
Coroutine con khởi chạy qua async với CommonPool context.
Note: Nếu có exception trong launch thì app sẽ crash.
fun loadData() = launch(UI) { view.showLoading() // ui thread val task = async(CommonPool) { dataProvider.loadData() // non ui thread } val result = task.await() // suspend until finished view.showData(result) // ui thread }
launch + withContext (thực hiện task)
Ví dụ trên chạy khá ổn, tuy nhiên chúng ta lại lãng phí tài nguyên khi khởi chạy thêm một coroutine thứ hai để thực hiện background job. Chúng ta có thể tối ưu đoạn này bằng cách chỉ khởi chạy một coroutine và sử dùng withContext để có thể chuyển coroutine context.
Coroutine cha được khởi chạy qua launch với UI context.
Background job được thực hiện qua withContext với CommonPool context.
Note: Nếu có exception trong launch thì app sẽ crash.
fun loadData() = launch(UI) { view.showLoading() // ui thread // non ui thread, suspend util finished val result = withContext(CommonPool) { dataProvider.loadData() } view.showData(result) // ui thread }
launch + withContext (thực hiện 2 task lần lượt)
Coroutine cha khởi chạy qua launch với UI context
Background job được thực hiện qua withContext với CommonPool context.
Note: result1 và result2 được thực hiện lần lượt.
Note: Nếu có exception trong launch thì app sẽ crash.
fun loadData() = launch(UI) { view.showLoading() // ui thread // non ui thread, suspend util finished val result1 = withContext(CommonPool) { dataProvider.loadData() } // non ui thread, suspend util finished val result2 = withContext(CommonPool) { dataProvider.loadData() } val result = result1 + result2 view.showData(result) // ui thread }
launch + async + async (thực hiện 2 task song song)
Coroutine cha khởi chạy qua launch với UI context
Coroutine con khởi chạy qua async với CommonPool context.
Note: task1 và task2 được thực hiện song song.
Note: Nếu có exception trong launch thì app sẽ crash.
fun loadData() = launch(UI) { view.showLoading() // ui thread val task1 = async(CommonPool) { dataProvider.loadData() } val task2 = async(CommonPool) { dataProvider.loadData() } // non ui thread, suspend util finished val result = task1.await() + task2.await() view.showData(result) // ui thread }
Nếu bạn muốn đặt một timeout cho một coroutine job, đóng gói suspended fun với withTimeoutNull sẽ return null khi timeout.
fun loadData() = launch(UI) { view.showLoading() // ui thread val task = async(CommonPool) { dataProvider.loadData() } // non ui thread, suspend util task is finished or return null in 2 sec val result = withTimeoutOrNull(2, TimeUnit.SECONDS) { task.await()} view.showData(result) // ui thread }
job
Hàm loadData return một object Job mà có thể bị cancel. Khi coroutine cha bị cancel thì tất cả các coroutine con của nó cũng sẽ bị cancel.
Nếu stopPresenting() được gọi trong khi dataProvider.loadData() đang được thực hiện, thì hàm view.showData() sẽ không bao giờ được gọi nữa.
fun loadData() = launch(UI) { view.showLoading() // ui thread val task = async(CommonPool) { dataProvider.loadData() } // non ui thread, suspend util finished val result = task.await() view.showData(result) // ui thread } var job: Job? = null fun startPresenting() { job = loadData() } fun stopPresenting() { job?.cancel() }
parent job
Một cách khác để cancel coroutine là tạo ra object Job và truyền nó vào khi khởi tạo coroutine như là parent job.
Note: Nếu bạn cancel parent job, bạn cần phải tạo object Job mới để có thể khởi chạy các coroutine mới.
val job: Job = Job() fun stopPResenting() { job.cancel() } fun loadData() = launch(UI + job) { ... }
lifecycle aware job
Với sự ra đời của Android Architecture Components chúng ta có thể tạo lifecycle aware job mà tự động cancel khi Activity#onDestroy được gọi.
fun loadData() = launch(UI + job) { } /** * Lifecycle aware [Job] is automatically cancelled when * [Activity#onDestroy] event occurs */ class AndroidJob(lifecycle: Lifecycle) : Job by Job(), LifecycleObserver { init { lifecycle.addObserver(this) } @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun destroy() = cancel() }
try-catch
Coroutine cha được khởi chạy thông qua launch với UI context
Coroutine con được khởi chạy thông qua async với CommonPool context
Giả sử dataProvider.loadData() sẽ tạo ra exception. Khi đó, bạn có thể dùng khối try-catch để bắt exception và xử lý.
Note: Có thể bạn sẽ thắc mắc rằng mình đã nói exception trong async không bị bắn ra ngoài thì làm sao mà phải try-catch nhỉ? Thực sự thì exception trong async không bị bắn ra ngoài cho đến khi bạn thực hiện lời gọi task.await().
fun loadData() = launch(UI) { view.showLoading() // ui thread try { // non ui thread val task = async(CommonPool) { dataProvider.loadData() } val result: Result = task.await() // suspend until finished view.showData(result) // ui thread } catch (e: Exception) { e.printStackTrace() } }
Đế tránh phải dùng class logic, bạn có thể xử lý exception trong dataProvider.loadData() và cho nó trả về class Result như sau
sealed class Result() class Success : Result() class Error : Result() fun loadData() = launch(UI) { view.showLoading() // ui thread // non ui thread val task = async(CommonPool) { dataProvider.loadData() } val result: Result = task.await() // suspend until finished when (result) { is Success -> view.showData(result.success) // ui thread is Error -> result.error.printStackTrace() } }
async + async
Async khác với launch ở chỗ, nếu cóexception trong launch mà không được handle thì app sẽ crash, còn async thì không. Exception sẽ được lưu trữ ở Defered là kết quả trả về của async. Defered cũng là một Job nhưng có mang giá trị trả về, còn Job thì không mang giá trị. Nếu exception trong async không được xử lý thì nó sẽ ngầm bị bỏ qua.
fun loadData() = async(UI) { view.showLoading() // ui thread // non ui thread val task = async(CommonPool) { dataProvider.loadData() } val result: Result = task.await() // suspend until finished view.showData(result) // ui thread }
Trong trường hợp trên thì exception sẽ được lưu trong Job. Để có thể lấy chúng ra bạn có thể dùng hàm invokeCompletion()
var job: Job? = null fun startPresenting() { job = loadData() job?.invokeCompletion { it: Throwable? -> it?.printStackTrace() // (1) not contain CancellationException in case job was cancelled // or job?.getCancellationException()?.printStackTrace() // (2) ob was cancelled } }
launch + coroutine exception handler
Một cách khác để các bạn có thể xử lý exception xảy ra trong launch là tạo một exceptionHandler và thêm nó vào param khi khởi chạy coroutine.
val exceptionHandler: CoroutineContext = CoroutineExceptionHandler { _, throwable -> throwable.printStackTrace() } fun loadData() = launch(UI + exceptionHandler) { view.showLoading() // ui thread // non ui thread val task = async(CommonPool) { dataProvider.loadData() } val result: Result = task.await() // suspend until finished view.showData(result) // ui thread }
Để có thể hiểu coroutine đang làm gì, bạn có thể sử dụng Thread.currentThread().name để log lại thread name
fun loadData() = launch(UI + exceptionHandler) { view.showLoading() // ui thread log(Thread.currentThread().name) // non ui thread val task = async(CommonPool) { dataProvider.loadData() log(Thread.currentThread().name) } og(Thread.currentThread().name) val result: Result = task.await() // suspend until finished view.showData(result) // ui thread }
Bài đến đây là hết rồi, hẹn gặp lại các bạn ở các bài tiếp theo ( ^ _ ^ ).
Android Coroutine Recipes
https://proandroiddev.com/android-coroutine-recipes-33467a4302e9
kotlinx.coroutines
https://github.com/Kotlin/kotlinx.coroutines