Bí kiếp thần công Coroutine
Bài đăng này đã không được cập nhật trong 5 năm
Nội dung
- Làm sao để chạy một coroutine
- Làm sao để chạy coroutine với timeout
- Làm sao để cancel một coroutine
- Cách sử lý exception với coroutines
- Cách test coroutines
- Log coroutines thread
Dependencies
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0'
How to launch a coroutine
Trong thư viện kotlinx.coroutines
, bạn có thể bắt đầu coroutine mới bằng cách sử dụng launch
hoặc async
Về mặt khái niệm, async
giống như launch
. Nó bắt đầu một coroutine riêng biệt, đó là một light-weight thread , chạy song song với tất cả các coroutine khác. Sự khác biệt là launch
trả về một Job
và không mang bất kỳ giá trị kết quả nào, trong khi async trả về Deferred
- một light-weight non-blocking thể hiện lời hứa sẽ trả về kết quả sau này. Bạn có thể sử dụng .await ()
trên giá trị deferred để có kết quả cuối cùng, nhưng Deferred
cũng là một Job
, vì vậy bạn có thể hủy nó nếu cần.
Nếu đoạn code bên trong
launch
bị chết vớiexception
, thì nó bị coi như là exception đó chưa được xử lý trong thread và sẽ làm crash ứng dụng . Một exception trong khối async sẽ được lưu trong kết quả của Deferred và nó sẽ ko trả về kết quả, và nó sẽ bị hủy bỏ âm thầm trừ khi được xử lý.
Coroutine dispatcher
Trong Android, chúng tôi thường sử dụng hai người điều phối:
- uiDispatcher để gửi thực thi lên luồng UI chính của Android (cho coroutine gốc).
- bgDispatcher để gửi thực thi trong luồng IO, Background (cho các coroutines con)
// dispatches execution into Android main thread
val uiDispatcher: CoroutineDispatcher = Dispatchers.Main
// represent a pool of shared threads as coroutine dispatcher
val bgDispatcher: CoroutineDispatcher = Dispatchers.I0
Trong ví dụ sau, chúng ta sẽ sử dụng CommonPool cho bgContext, giới hạn số lượng luồng chạy song song với giá trị 64 luồng hoặc số lõi (tùy theo số nào lớn hơn).
Bạn có thể muốn xem xét sử dụng newFixedThreadPoolContext hoặc bạn có thể sử dụng cached thread pool.
Coroutine scope
Để khởi chạy coroutine, bạn cần cung cấp CoroutineScope hoặc sử dụng GlobalScope.
Xem thêm: Tránh sử dụng GlobalScope.
// GlobalScope example
class MainFragment : Fragment() {
fun loadData() = GlobalScope.launch { ... }
}
// CoroutineScope example
class MainFragment : Fragment() {
val uiScope = CoroutineScope(Dispatchers.Main)
fun loadData() = uiScope.launch { ... }
}
// Fragment implements CoroutineScope example
class MainFragment : Fragment(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main
fun loadData() = launch { ... }
}
launch + async (execute task)
Coroutine cha được chạy qua hàm launch
với Main
dispatcher.
Các coroutine con được chạy qua hàm async
với IO
dispatcher.
Chú ý: Một coroutine cha luôn luôn chờ đợi tất cả các coroutine con hoàn thành
Chú ý: Nếu exception không được kiểm tra, ứng dụng sẽ bị crash
Xem thêm : Tránh sử dụng async/ await không cần thiết
val uiScope = CoroutineScope(Dispatchers.Main)
fun loadData() = uiScope.launch {
view.showLoading() // ui thread
val task = async(bgDispatcher) { // background thread
// your blocking call
}
val result = task.await()
view.showData(result) // ui thread
}
launch + withContext (execute task)
Ví dụ trước hoạt động ngon lành, chúng tôi đang lãng phí tài nguyên bằng cách khởi chạy coroutine thứ hai để làm công việc nền. Chúng ta có thể tối ưu hóa code của mình nếu chúng ta chỉ khởi chạy một coroutine và sử dụng withContext
để chuyển ngữ cảnh coroutine.
Coroutine cha được khởi chạy thông qua launch
với Main
dispatcher.
Background job sẽ được thực thi thông qua withContext
với IO
dispatcher.
val uiScope = CoroutineScope(Dispatchers.Main)
fun loadData() = uiScope.launch {
view.showLoading() // ui thread
val result = withContext(bgDispatcher) { // background thread
// your blocking call
}
view.showData(result) // ui thread
}
launch + withContext (chạy 2 task tuần tự)
Coroutine cha được khởi chạy thông qua launch
với Main
dispatcher.
Background job sẽ được thực thi thông qua withContext
với IO
dispatcher.
Chú ý: result1
và result2
thực hiện tuần tự
Chú ý: Nếu exception ko được kiểm tra, ứng dụng sẽ bị crash
val uiScope = CoroutineScope(Dispatchers.Main)
fun loadData() = uiScope.launch {
view.showLoading() // ui thread
val result1 = withContext(bgDispatcher) { // background thread
// your blocking call
}
val result2 = withContext(bgDispatcher) { // background thread
// your blocking call
}
val result = result1 + result2
view.showData(result) // ui thread
}
launch + async + async (chạy 2 task song song)
Coroutine cha được khởi chạy thông qua launch
với Main
dispatcher.
Các coroutine con chạy thông qua async
với IO
dispatcher
Chú ý: result1
và result2
thực hiện song song
Chú ý: Nếu exception ko được kiểm tra, ứng dụng sẽ bị crash
Xem thêm: Wrap async với coroutineScrope hoặc sử dụng SupervisorJob để handle exceptions
val uiScope = CoroutineScope(Dispatchers.Main)
fun loadData() = uiScope.launch {
view.showLoading() // ui thread
val task1 = async(bgDispatcher) { // background thread
// your blocking call
}
val task2 = async(bgDispatcher) { // background thread
// your blocking call
}
val result = task1.await() + task2.await()
view.showData(result) // ui thread
}
Chạy coroutine với timeout
Nếu bạn muốn set Timeout cho một coroutine job, bọc đoạn code vào trong withTimeoutOrNull, nó sẽ trả về null trong trường hợp timeout.
val uiScope = CoroutineScope(Dispatchers.Main)
fun loadData() = uiScope.launch {
view.showLoading() // ui thread
val task = async(bgDispatcher) { // background thread
// your blocking call
}
// suspend until task is finished or return null in 2 sec
val result = withTimeoutOrNull(2000) { task.await() }
view.showData(result) // ui thread
}
Làm sao để cancel một coroutine
job
Hàm loadData
trả ra một đối tượng Job
, đối tượng Job
này có thể cancel được. Khi coroutine cha bị cancel, thì tất cả các coroutine con sẽ bị cancel đệ qui theo.
Nếu hàm stopPresenting
được gọi trong khi dataProvider.loadData
vẫn đang chạy, thì hàm view.showData
sẽ không bao giờ được gọi.
val uiScope = CoroutineScope(Dispatchers.Main)
var job: Job? = null
fun startPresenting() {
job = loadData()
}
fun stopPresenting() {
job?.cancel()
}
fun loadData() = uiScope.launch {
view.showLoading() // ui thread
val result = withContext(bgDispatcher) { // background thread
// your blocking call
}
view.showData(result) // ui thread
}
parent job
Một cách khác để hủy coroutine là tạo đối tượng SupervisorJob
và chỉ định nó trong hàm scrope contructor
thông qua toán tử +
Đọc thêm về combinding coroutine context tại đây
Lưu ý: nếu bạn hủy parent Job , bạn cần tạo đối tượng Job mới để bắt đầu các coroutines mới, đó là lý do tại sao chúng tôi sử dụng cancelChildren.
Xem thêm: Avoid cancelling scope job.
var job = SupervisorJob()
val uiScope = CoroutineScope(Dispatchers.Main + job)
fun startPresenting() {
loadData()
}
fun stopPresenting() {
scope.coroutineContext.cancelChildren()
}
fun loadData() = uiScope.launch {
view.showLoading() // ui thread
val result = withContext(bgDispatcher) { // background thread
// your blocking call
}
view.showData(result) // ui thread
}
Nhận biết vòng đời coroutine scope
Với việc phát hành ra android architecture components. chúng ta có thể tạo phạm vi coroutine nhận biết vòng đời sẽ tự hủy khi sự kiện Activity # onDestroy xảy ra.
Ví dụ về phạm vi coroutine nhận biết vòng đời cho LifecyclObserver.
class MainScope : CoroutineScope, LifecycleObserver {
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun destroy() = coroutineContext.cancelChildren()
}
// usage
class MainFragment : Fragment() {
private val uiScope = MainScope()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(mainScope)
}
private fun loadData() = uiScope.launch {
val result = withContext(bgDispatcher) {
// your blocking call
}
}
}
Ví dụ về phạm vi coroutine nhận biết vòng đời cho ViewModel
open class ScopedViewModel : ViewModel() {
private val job = SupervisorJob()
protected val uiScope = CoroutineScope(Dispatchers.Main + job)
override fun onCleared() {
super.onCleared()
uiScope.coroutineContext.cancelChildren()
}
}
// usage
class MyViewModel : ScopedViewModel() {
private fun loadData() = uiScope.launch {
val result = withContext(bgDispatcher) {
// your blocking call
}
}
}
Làm thế nào để xử lý các trường hợp exception với coroutines
try-catch block
Coroutine cha được khởi chạy thông qua launch
với Main
dispatcher.
Các coroutine con chạy thông qua async
với IO
dispatcher
Lưu ý: Bạn có thể sử dụng khối try-catch
để bắt ngoại lệ và xử lý chúng.
Để tránh sử dung try-catch
trong presenter class, tốt hơn hết là tạo ra một generic classs Result
và để exception trả ra class đó.
async parent
Coroutine cha được khởi chạy thông qua launch
với Main
dispatcher.
Lưu ý: Để bỏ qua mọi trường hợp ngoại lệ, hãy khởi chạy coroutine cha với async
.
Trong trường hợp này, ngoại lệ sẽ được lưu trữ trong đối tượng Job. Để lấy nó, bạn có thể sử dụng hàm invokeOnCompletion.
launch + coroutine exception handler
Coroutine cha được khởi chạy thông qua launch
với Main
dispatcher.
Các coroutine con chạy thông qua withContext
với IO
dispatcher
Lưu ý: Bạn có thể thêm CoroutineExceptionHandler vào coroutineContext cha để bắt ngoại lệ và xử lý chúng.
Test coroutines
Để chạy coroutines bạn cần chỉ định một CoroutineDispatcher
.
Nếu bạn muốn viết UnitTest cho lớp MainPresenter của mình, bạn cần phải xác định coroutineContext để thực hiện trên UIThread và BackgroundThread
Có lẽ cách dễ nhất là thêm hai tham số vào hàm contructor MainPresenter
: uiDispatcher
với giá trị mặc định là Main
và ioContext
với giá trị IO
mặc định.
Bây giờ bạn có thể dễ dàng kiểm tra lớp MainPresenter của mình bằng cách cung cấp một Unconfined sẽ chỉ thực thi mã trên luồng hiện tại.
Cách log coroutine thread
Để hiểu coroutine nào thực hiện công việc hiện tại của bạn, bạn có thể bật các tiện ích gỡ lỗi thông qua System.setProperty
và đăng nhập tên luồng thông qua tên Thread.currentThread().name.
Kết
Related articles and special thanks: Guide to coroutines by example, Roman Elizarov, Jake Wharton, Andrey Mischenko.
Bài viết được dịch từ : https://proandroiddev.com/android-coroutine-recipes-33467a4302e9
Xin cám ơn đã theo dõi !!
All rights reserved