+4

Kotlin Coroutines trong Android

Hôm nay, mình sẽ trình bày về Kotlin Coroutines trong lập trình Android giải quyết khó khăn trong việc xử lý bất đồng bộ(asynchronous)

I. Trước tiên chúng ta cùng tìm hiểu về Asynchronous Programing (Lập trình bất đồng bộ)

👉️ Trong Android được chia làm 2 luồng chính là Main Thread (UI Thread) và Background Thread.
👉️👉️ Main Thread có nhiệm vụ xử lý tác vụ liên quan đến UI như hiển thị hình ảnh chữ viết,... Tuy nhiên nếu các bạn xử lý các tác vụ nặng tốn thời gian, tài nguyên như đọc ghi files, request network, tính toán,... trên UI Thread thì sẽ gặp hiện tượng ANR, nguyên nhân là Main thread phải đợi xử lý xong tác vụ tiêu tốn thời gian xong mới thực hiện tác vụ tiếp theo -> Cần 1 luồng khác gọi là Background Thread xử lý và cập nhật kết quả trên UI Thread.
img1
📢📢📢 Vẫn có các phương pháp khác như RX, Thread + Callbacks/Asynctask/Handler. Tuy nhiên cái giá phải trả khi sử dụng Thread là khá đắt. Tại sao -> link đây.
Tuy nhiên trong bài viết này chỉ đề cập đến Kotlin Coroutine thôi nhé 😅😅😅.

II. Kotlin Coroutines

  • Coroutines có nghĩa là kết hợp công việc với nhau
    img2
  • Coroutines cơ bản là hiểu nó như một light-weight threed nhưng nó không phải là thread, nó chỉ hoạt động như thread. Hàng ngàn coroutines có thể bắt đầu cùng một lúc, nhưng nếu hàng nghìn thread chạy một lúc thì performance sẽ oẳng.
  • Cấu trúc của đoạn Code sử dụng Coroutines
    img3
fun fetchData(){
// launch a coroutine
 CoroutineScope(Dispatchers.Main).launch { 
                val result = getListString() // hàm getListString() này được chạy bất đồng bộ
               // thế nhưng cách viết code lại giống như đang viết code đồng bộ (code từ trên xuống)
        }
}

suspend fun getListString(): List<String> {
    // makes a request and suspends the coroutine
    return suspendCoroutine {
            apiServie.getListString()
    }
}

Nhìn đoạn code trên tuy chạy bất đồng bộ nhưng nhìn cách viết như tuần tự ✌️

III. Bây giờ đến phần chính. Tìm hiểu các thành phần của Coroutines.

3.1: Suspend Function:

  • Là function có thể start, pause, resume. Cụ thể hơn là nó có thể dừng tại 1 thời điểm nhưng sau đó có thể tiếp tục để thực thi tiếp tác vụ tại thời điểm nó dừng. Cùng xem hình ảnh so sánh giữa Blocking và Suspend để thấy được sự khác biệt.

Đây là hình ảnh Blocking

img4

Đây là hình ảnh dùng suspend function

img5

3.2: Coroutine Builder: launch (), async()

  • launch(): trả về 1 đối tượng Job để có thể cancel coroutines.
  • async(): return kết quả trả về. Nó trả về 1 object Deffered<T>, cần sử dụng đến await() để lấy kết quả trả về.
    Ảnh minh họa

img6

  • Ngoài ra: withContext(): giống như async nhưng không cần phải gọi await().
    👍️👍️👍️👍️👍️👍️Note rules: Tại cùng một thời điểm:
    🍌 Sử dụng withContext() khi KHÔNG CẦN các suspend function chạy song song.
    🍌 Sử dụng withContext() để trả về MỘT single task.
    🍌 Sử dụng async khi CẦN các suspend function chạy song song.
    🍌 Sử dụng async() để trả về NHIỀU task song song - multiple task.

Để chứng minh cho sự bốc phét của mình thì đây là log 🤣🤣🤣

🥒 Với việc dùng async thì thời gian chạy 2 suspend function trên là 5015 milis dù mình đã để delay mỗi function 5000.
img7
img8
🥒 Với việc dùng withContext() thì kết quả nhận được như thế này.
img9
img10
😇😇😇 Wow thời gian gấp đôi hơn 10077 milis. Đây là lý do tại sao nói nên sử dụng withContext() khi làm việc SINGLE Task

3.3: CoroutineContext: là tập hợp các phần tử định nghĩa behavior của coroutines, các tham số khi khởi tạo CoroutineScope gồm:

  • Job: kiểm soát vòng đời của Coroutine.
  • CoroutineDispatcher: xác định thread nào mà coroutines chạy trên nó.
  • CoroutineName: định nghĩa tên của coroutine dùng để Debug.
  • CoroutineExceptionHandler: xử lý uncaught exception.

3.4: CoroutineScope: Có nhiệm vụ theo dõi tất cả coroutine được tạo thông qua nó. từ đó có thể cancel coroutines bằng cách gọi scope.cancel(). Một vài scope phổ biến:

  • GlobalScope: không được khuyến khích sử dụng vì nó sẽ không tự hủy nếu chúng ta gọi job.cancel() nếu job này bao bọc nó ở trong.
  • viewModelScope.
  • lifecycleScope.

3.5: CoroutineDispatcher: Xác định thread mà coroutines sẽ chạy, gồm:

  • Dispatcher.IO: sử dụng cho tác vụ I/O: đọc ghi file, network, etc...
  • Dispatcher.Default: sử dụng cho tác vụ tính toán nặng tiêu tốn CPU.
  • Dispatcher.Main: chạy Main Thread.
  • Dispatcher.Unconfied: chạy trên Thread hiện tại cho đến khi gặp 1 suspend function thì nó sẽ chạy trên Woker Thread.

3.6: Những điều cần lưu ý khi Cancel coroutine:

  • Coroutines sẽ không dừng ngay lập tức khi cancel mà nó vẫn tiếp tục chạy cho đến khi hoàn thành công việc. Cần sử dụng đến isActive() hoặc ensureActive() để check. Với withContext() hoặc delay có thể cancel được lên không cần check.
  • Hoặc có thể dùng try/catch.
  • 📢📢📢📢📢 Một lưu ý nhỏ: Coroutines bị cancel thì không suspend được nữa, nếu muốn thực hiện tác vụ suspend bên trong một finally block cần thêm coroutineContext là NonCancellable.
 val job = launch {
   try {
      work()
   } catch (e: CancellationException){
      println(“Work cancelled!”)
    } finally {
      withContext(NonCancellable){
         delay(1000L) // or some other suspend fun
         println(“Cleanup done!”)
      }
    }
}

3.7: Xử lý Exception trong Coroutines:

img11

Khi một coroutines throw ra exception, exception sẽ đẩy lên cho parent của nó xử lý. khi đó:

👉️👉️ 1. Parent sẽ cancel toàn bộ coroutine con của nó.

👉️👉️ 2. Parent sẽ cancel chính nó.

👉️👉️ 3. Exception sẽ được đẩy lên cao hơn.

=> Để coroutine nằm trong scope bị exception không bị cancel thì sử dụng SupervisorJob().

  • SupervisorJob() : khi dùng thì mô tả trên không xảy ra nên coroutine gây ra exception đó phải dùng try catch hoặc CoroutineContext cần có CoroutineExceptionHandler nếu không có thể sẽ crash app.
  • CoroutineExceptionHandler: là 1 thành phần của CoroutineContext để bắt Exception. Exception được bắt:
    👉️👉️ KHI: Exception bị throw bởi 1 coroutines tự động throw exception (launch thay vì async).
    👉️👉️ TẠI: Handler được khai báo ở root CoroutineScope hoặc root coroutine.
  • Lưu ý: Trong trường hợp tiếp theo exception sẽ không được bắt nếu CoroutineExceptionHandler không được khai báo ở root coroutine hoặc root scope. Để làm được điều này cần khai báo như sau
val scope = CoroutineScope(Job())
scope.launch(handler) {
    launch {
        throw Exception("Failed coroutine")
    }
}
    // TH ko được CoroutineExceptionHandler caught.
val scope = CoroutineScope(Job())
scope.launch {
    launch(handler) {
        throw Exception("Failed coroutine")
    }
}
    
   // Thử 2 cách ở dưới đây.
val scope = CoroutineScope(Job())
scope.launch(handler) {
    launch {
        throw Exception("Failed coroutine")
    }
}
    // Hoặc
val scope = CoroutineScope(Job() + handler)
scope.launch {
    launch {
        throw Exception("Failed coroutine")
    }
}

IV. Tổng kết: trên đây là toàn bộ những kiến thức về Kotlin Coroutines mà mình tìm hiểu cũng như sưu tầm ở nhiều nguồn. Rất mong các bạn VOTE mạnh tay 🖖 để mình có động lực viết các bài tiếp theo. Xin chào và hẹn gặp lại!👋

V. Link tài liệu tham khảo:

https://kotlinlang.org/docs/async-programming.html

https://viblo.asia/s/cung-hoc-kotlin-coroutine-z45bxjBoZxY


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.