+4

Bí kiếp thần công Coroutine

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ới exception, 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ú ý: result1result2 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ú ý: result1result2 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à MainioContext 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí