Giới thiệu về coroutine trong Android Studio

1. Trước khi bắt đầu

Trong lớp học lập trình trước, bạn đã tìm hiểu về Coroutine. Bạn đã sử dụng Kotlin Playground để viết mã đồng thời bằng coroutine. Trong lớp học lập trình này, bạn sẽ áp dụng kiến thức về coroutine trong một ứng dụng Android và vòng đời của ứng dụng đó. Bạn sẽ thêm mã để khởi chạy đồng thời các coroutine mới và tìm hiểu cách kiểm thử những coroutine này.

Điều kiện tiên quyết

  • Có kiến thức cơ bản về ngôn ngữ Kotlin, bao gồm cả hàm và lambda
  • Có thể tạo bố cục trong Jetpack Compose
  • Có thể viết mã kiểm thử đơn vị trong Kotlin (xem lớp học lập trình Viết mã kiểm thử đơn vị cho ViewModel)
  • Biết cách hoạt động của luồng (thread) và mô hình đồng thời
  • Kiến thức cơ bản về Coroutine và CoroutineScope

Sản phẩm bạn sẽ tạo ra

  • Ứng dụng Race Tracker mô phỏng tiến trình cuộc đua giữa 2 người chơi. Hãy xem ứng dụng này là một cơ hội để thử nghiệm và tìm hiểu thêm về các khía cạnh của coroutine.

Kiến thức bạn sẽ học được

  • Sử dụng coroutine trong vòng đời của ứng dụng Android.
  • Nguyên tắc của mô hình đồng thời có cấu trúc.
  • Cách viết mã kiểm thử đơn vị để kiểm thử coroutine.

Những gì bạn cần

  • Phiên bản ổn định mới nhất của Android Studio

2. Tổng quan về ứng dụng

Ứng dụng Race Tracker mô phỏng 2 người chơi chạy đua. Giao diện người dùng của ứng dụng này bao gồm 2 nút là Start/Pause (Bắt đầu/Tạm dừng) và Reset (Đặt lại), cùng 2 thanh tiến trình để hiển thị tiến trình của những tay đua. Người chơi 1 và 2 được thiết lập để "chạy" đua ở tốc độ khác nhau. Khi cuộc đua bắt đầu, Người chơi 2 chạy nhanh gấp đôi so với Người chơi 1.

Bạn sẽ sử dụng coroutine trong ứng dụng này để đảm bảo:

  • Cả hai người chơi đồng thời "chạy đua".
  • Giao diện người dùng của ứng dụng có tính thích ứng và các thanh tiến trình tăng lên trong suốt cuộc đua.

Mã khởi đầu có sẵn mã giao diện người dùng cho ứng dụng Race Tracker. Mục tiêu chính trong phần này của lớp học lập trình là làm quen với coroutine của Kotlin trong một ứng dụng Android.

Tải đoạn mã khởi đầu

Để bắt đầu, hãy tải mã khởi đầu xuống:

Ngoài ra, bạn có thể sao chép kho lưu trữ GitHub cho mã:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-race-tracker.git
$ cd basic-android-kotlin-compose-training-race-tracker
$ git checkout starter

Bạn có thể duyệt tìm mã khởi đầu trong kho lưu trữ Race Tracker trên GitHub.

Hướng dẫn từng bước về mã khởi đầu

Bạn có thể bắt đầu cuộc đua bằng cách nhấp vào nút Start (Bắt đầu). Nội dung của nút Start (Bắt đầu) sẽ chuyển thành Pause (Tạm dừng) trong khi cuộc đua diễn ra.

2ee492f277625f0a.png

Tại bất kỳ thời điểm nào, bạn cũng có thể sử dụng nút này để tạm dừng hoặc tiếp tục cuộc đua.

50e992f4cf6836b7.png

Khi cuộc đua bắt đầu, bạn có thể xem tiến trình của từng người chơi thông qua chỉ báo trạng thái. Hàm có khả năng kết hợp StatusIndicator hiển thị trạng thái tiến trình của từng người chơi. Hàm này sử dụng thành phần kết hợp (composable) LinearProgressIndicator để hiển thị thanh tiến trình. Bạn sẽ sử dụng coroutine để cập nhật giá trị cho tiến trình.

79cf74d82eacae6f.png

RaceParticipant cung cấp dữ liệu về mức độ gia tăng của tiến trình. Lớp này là phần tử giữ trạng thái cho mỗi người chơi và duy trì name của người tham gia, maxProgress để đạt đến mục tiêu hoàn thành cuộc đua, khoảng thời gian trễ giữa các lần tiến trình tăng lên, currentProgress trong cuộc đua và initialProgress.

Trong phần tiếp theo, bạn sẽ sử dụng coroutine để triển khai chức năng mô phỏng tiến trình cuộc đua mà không chặn giao diện người dùng của ứng dụng.

3. Triển khai tiến trình cuộc đua

Bạn cần có hàm run() so sánh currentProgress của người chơi với maxProgress để phản ánh toàn bộ tiến trình của cuộc đua. Đồng thời, hãy sử dụng hàm tạm ngưng delay() để thêm độ trễ ngắn giữa các lần tiến trình tăng lên. Hàm này phải là hàm suspend vì đang gọi một hàm tạm ngưng khác là delay(). Ngoài ra, bạn sẽ sử dụng một coroutine để gọi hàm này sau trong lớp học lập trình này. Hãy làm theo các bước dưới đây để triển khai hàm đó:

  1. Mở lớp RaceParticipant nằm trong mã khởi đầu.
  2. Bên trong lớp RaceParticipant, hãy xác định một hàm suspend mới có tên là run().
class RaceParticipant(
    ...
) {
    var currentProgress by mutableStateOf(initialProgress)
        private set

    suspend fun run() {
        
    }
    ...
}
  1. Để mô phỏng tiến trình của cuộc đua, hãy thêm vòng lặp while chạy cho đến khi currentProgress đạt đến giá trị maxProgress và đặt giá trị này là 100.
class RaceParticipant(
    ...
    val maxProgress: Int = 100,
    ...
) {
    var currentProgress by mutableStateOf(initialProgress)
        private set

    suspend fun run() {
        while (currentProgress < maxProgress) {
            
        }
    }
    ...
}
  1. Giá trị của currentProgress được đặt là initialProgress, tức là 0. Để mô phỏng tiến trình của người tham gia, hãy tăng giá trị currentProgress lên giá trị của thuộc tính progressIncrement trong vòng lặp while. Lưu ý rằng giá trị mặc định của progressIncrement1.
class RaceParticipant(
    ...
    val maxProgress: Int = 100,
    ...
    private val progressIncrement: Int = 1,
    private val initialProgress: Int = 0
) {
    ...
    var currentProgress by mutableStateOf(initialProgress)
        private set

    suspend fun run() {
        while (currentProgress < maxProgress) {
            currentProgress += progressIncrement
        }
    }
}
  1. Để mô phỏng nhiều khoảng tiến trình trong cuộc đua, hãy sử dụng hàm tạm ngưng delay(). Truyền giá trị của thuộc tính progressDelayMillis làm đối số.
suspend fun run() {
    while (currentProgress < maxProgress) {
        delay(progressDelayMillis)
        currentProgress += progressIncrement
    }
}

Khi xem mã bạn vừa thêm, bạn sẽ thấy một biểu tượng ở bên trái của lệnh gọi đến hàm delay() trong Android Studio, như minh hoạ trong ảnh chụp màn hình dưới đây: 11b5df57dcb744dc.png

Biểu tượng này cho biết điểm tạm ngưng khi hàm có thể tạm ngưng và tiếp tục lại sau.

Luồng chính không bị chặn trong lúc coroutine đang chờ hết khoảng thời gian trễ, như minh hoạ trong sơ đồ dưới đây:

a3c314fb082a9626.png

Coroutine tạm ngưng (nhưng không chặn) quá trình thực thi sau khi gọi hàm delay() có giá trị khoảng thời gian mong muốn. Sau khi hết khoảng thời gian trễ, coroutine đó sẽ tiếp tục thực thi và cập nhật giá trị của thuộc tính currentProgress.

4. Bắt đầu cuộc đua

Khi người dùng nhấn nút Start (Bắt đầu), bạn cần "bắt đầu cuộc đua" bằng cách gọi hàm tạm ngưng run() trên từng thực thể của 2 người chơi. Để làm việc này, bạn cần khởi chạy một coroutine để gọi hàm run().

Khi khởi chạy một coroutine để kích hoạt cuộc đua, bạn cần đảm bảo các chương trình thành phần sau đây cho cả hai người tham gia:

  • Người tham gia bắt đầu chạy ngay khi bạn nhấp vào nút Start (Bắt đầu) – tức là coroutine khởi chạy.
  • Người tham gia tạm dừng hoặc ngừng chạy khi bạn nhấp vào nút Pause (Tạm dừng) hoặc nút Reset (Đặt lại) – tức là coroutine bị huỷ.
  • Khi người dùng đóng ứng dụng, việc huỷ được quản lý đúng cách – tức là mọi coroutine đều bị huỷ và liên kết với một vòng đời.

Trong lớp học lập trình đầu tiên, bạn đã biết rằng mình chỉ có thể gọi hàm tạm ngưng từ một hàm tạm ngưng khác. Để gọi các hàm tạm ngưng theo cách an toàn từ bên trong một thành phần kết hợp, bạn cần sử dụng thành phần kết hợp LaunchedEffect(). Thành phần kết hợp LaunchedEffect() sẽ chạy hàm tạm ngưng được cung cấp miễn là hàm đó vẫn có trong thành phần. Bạn có thể sử dụng hàm có khả năng kết hợp LaunchedEffect() để thực hiện mọi thao tác dưới đây:

  • Thành phần kết hợp LaunchedEffect() cho phép bạn gọi các hàm tạm ngưng một cách an toàn từ những thành phần kết hợp.
  • Khi hàm LaunchedEffect() nhập một Thành phần, hàm này sẽ khởi chạy một coroutine với khối mã được truyền dưới dạng tham số. Khối mã này sẽ chạy hàm tạm ngưng được cung cấp miễn là hàm đó vẫn có trong thành phần. Khi người dùng nhấp vào nút Start (Bắt đầu) trong ứng dụng Race Tracker, LaunchedEffect() sẽ nhập thành phần đó và khởi chạy một coroutine để cập nhật tiến trình.
  • Coroutine bị huỷ khi LaunchedEffect() thoát khỏi thành phần. Trong ứng dụng, nếu người dùng nhấp vào nút Reset/Pause (Đặt lại/Tạm dừng) thì LaunchedEffect() sẽ bị xoá khỏi thành phần và các coroutine cơ bản sẽ bị huỷ.

Đối với ứng dụng Race Tracker, bạn không cần phải cung cấp rõ ràng Trình điều phối (Dispatcher) vì LaunchedEffect() sẽ xử lý vấn đề này.

Để bắt đầu cuộc đua, hãy gọi hàm run() cho từng người tham gia và thực hiện các bước sau đây:

  1. Mở tệp RaceTrackerApp.kt nằm trong gói com.example.racetracker.ui.
  2. Chuyển đến thành phần kết hợp RaceTrackerApp() và thêm một lệnh gọi vào thành phần kết hợp LaunchedEffect() trên dòng sau định nghĩa raceInProgress.
@Composable
fun RaceTrackerApp() {
    ...
    var raceInProgress by remember { mutableStateOf(false) }

    LaunchedEffect {
    
    }
    RaceTrackerScreen(...)
}
  1. Để đảm bảo rằng nếu thực thể của playerOne hoặc playerTwo được thay thế bằng các thực thể khác, thì LaunchedEffect() cần huỷ và khởi chạy lại những coroutine cơ bản, thêm các đối tượng playerOneplayerTwo làm key vào LaunchedEffect. Tương tự như cách kết hợp lại một thành phần kết hợp Text() khi giá trị văn bản của thành phần này thay đổi, nếu bất kỳ đối số chính nào của LaunchedEffect() thay đổi thì coroutine cơ bản sẽ bị huỷ và khởi chạy lại.
LaunchedEffect(playerOne, playerTwo) {
}
  1. Thêm một lệnh gọi vào các hàm playerOne.run()playerTwo.run().
@Composable
fun RaceTrackerApp() {
    ...
    var raceInProgress by remember { mutableStateOf(false) }

    LaunchedEffect(playerOne, playerTwo) {
        playerOne.run()
        playerTwo.run()
    }
    RaceTrackerScreen(...)
}
  1. Gói khối LaunchedEffect() bằng một điều kiện if. Giá trị ban đầu của trạng thái này là false. Giá trị của trạng thái raceInProgress được cập nhật thành true khi người dùng nhấp vào nút Start (Bắt đầu) và LaunchedEffect() thực thi.
if (raceInProgress) {
    LaunchedEffect(playerOne, playerTwo) {
        playerOne.run()
        playerTwo.run() 
    }
}
  1. Cập nhật cờ raceInProgress thành false để kết thúc cuộc đua. Giá trị này được đặt thành false khi người dùng nhấp vào Pause (Tạm dừng). Khi giá trị này được đặt thành false, LaunchedEffect() đảm bảo rằng mọi coroutine đã khởi chạy đều bị huỷ.
LaunchedEffect(playerOne, playerTwo) {
    playerOne.run()
    playerTwo.run()
    raceInProgress = false 
}
  1. Chạy ứng dụng rồi nhấp vào Start (Bắt đầu). Bạn sẽ thấy người chơi 1 hoàn thành cuộc đua trước khi người chơi 2 bắt đầu chạy, như minh hoạ trong video dưới đây:

fa0630395ee18f21.gif

Có vẻ như cuộc đua này không công bằng! Trong phần tiếp theo, bạn sẽ tìm hiểu cách khởi chạy các nhiệm vụ đồng thời để cả hai người chơi có thể chạy cùng lúc, tìm hiểu các khái niệm và triển khai hành vi này.

5. Mô hình đồng thời có cấu trúc

Cách bạn viết mã bằng coroutine được gọi là mô hình đồng thời có cấu trúc. Kiểu lập trình này giúp cải thiện khả năng đọc và thời gian phát triển mã. Ý tưởng về mô hình đồng thời có cấu trúc là coroutine có một hệ phân cấp – tác vụ có thể khởi chạy các tác vụ phụ và tác vụ phụ cũng có thể khởi chạy tiếp các tác vụ phụ. Đơn vị của hệ phân cấp này được gọi là phạm vi coroutine. Phạm vi coroutine phải luôn liên kết với một vòng đời.

Theo thiết kế, các Coroutines API tuân thủ mô hình đồng thời có cấu trúc này. Bạn không thể gọi hàm tạm ngưng từ một hàm không được đánh dấu là tạm ngưng. Giới hạn này đảm bảo rằng bạn gọi hàm tạm ngưng từ các trình tạo coroutine, chẳng hạn như launch. Các trình tạo này lần lượt được liên kết với một CoroutineScope.

6. Chạy các tác vụ đồng thời

  1. Để cho phép cả hai người tham gia chạy đồng thời, bạn cần khởi chạy 2 coroutine riêng biệt và di chuyển từng lệnh gọi đến hàm run() bên trong các coroutine đó. Gói lệnh gọi đến playerOne.run() bằng trình tạo launch.
LaunchedEffect(playerOne, playerTwo) {
    launch { playerOne.run() }
    playerTwo.run()
    raceInProgress = false 
}
  1. Tương tự, hãy gói lệnh gọi đến hàm playerTwo.run() bằng trình tạo launch. Sự thay đổi này giúp ứng dụng khởi chạy 2 coroutine thực thi đồng thời. Giờ đây, cả hai người chơi có thể chạy cùng lúc.
LaunchedEffect(playerOne, playerTwo) {
    launch { playerOne.run() }
    launch { playerTwo.run() }
    raceInProgress = false 
}
  1. Chạy ứng dụng rồi nhấp vào Start (Bắt đầu). Trong khi bạn ngóng chờ cuộc đua bắt đầu, thì nội dung của nút này lập tức lại chuyển thành Start (Bắt đầu) một cách đột ngột.

c46c2aa7c580b27b.png

Khi cả hai người chơi chạy xong, ứng dụng Race Tracker sẽ đặt lại nội dung của nút Pause (Tạm dừng) thành Start (Bắt đầu). Tuy nhiên, lúc này ứng dụng sẽ cập nhật raceInProgress ngay sau khi khởi chạy coroutine mà không cần chờ người chơi hoàn thành cuộc đua:

LaunchedEffect(playerOne, playerTwo) {
    launch {playerOne.run() }
    launch {playerTwo.run() }
    raceInProgress = false // This will update the state immediately, without waiting for players to finish run() execution.
}

Cờ raceInProgress được cập nhật ngay lập tức vì:

  • Hàm tạo launch sẽ khởi chạy một coroutine để thực thi playerOne.run() và lập tức trả về để thực thi dòng tiếp theo trong khối mã.
  • Luồng thực thi tương tự cũng diễn ra với hàm tạo launch thứ hai thực thi hàm playerTwo.run().
  • Ngay khi trình tạo launch thứ hai trả về, cờ raceInProgress sẽ được cập nhật. Việc này lập tức thay đổi nội dung của nút thành Start (Bắt đầu) và cuộc đua sẽ không bắt đầu.

Phạm vi coroutine

Hàm tạm ngưng coroutineScope tạo một CoroutineScope và gọi khối tạm ngưng được chỉ định bằng phạm vi hiện tại. Phạm vi này kế thừa coroutineContext từ phạm vi LaunchedEffect().

Phạm vi sẽ trả về ngay sau khi khối đã cho và mọi coroutine con đã hoàn tất. Đối với ứng dụng RaceTracker, phạm vi này sẽ trả về sau khi cả hai đối tượng người tham gia hoàn tất quá trình thực thi hàm run().

  1. Để đảm bảo hàm run() của playerOneplayerTwo hoàn tất quá trình thực thi trước khi cập nhật cờ raceInProgress, hãy gói cả hai trình tạo khởi chạy bằng một khối coroutineScope.
LaunchedEffect(playerOne, playerTwo) {
    coroutineScope {
        launch { playerOne.run() }
        launch { playerTwo.run() }
    }
    raceInProgress = false
}
  1. Chạy ứng dụng trên trình mô phỏng hoặc thiết bị Android. Bạn sẽ thấy màn hình dưới đây:

598ee57f8ba58a52.png

  1. Nhấp vào nút Start (Bắt đầu). Người chơi 2 chạy nhanh hơn Người chơi 1. Sau khi cuộc đua kết thúc, tức là khi tiến trình của cả hai người chơi đạt 100%, nhãn của nút Pause (Tạm dừng) sẽ thay đổi thành Start (Bắt đầu). Bạn có thể nhấp vào nút Reset (Đặt lại) để đặt lại cuộc đua và thực hiện lại quá trình mô phỏng. Cuộc đua được minh hoạ trong video dưới đây.

c1035eecc5513c58.gif

Luồng thực thi được minh hoạ trong sơ đồ dưới đây:

cf724160fd66ff21.png

  • Khi khối LaunchedEffect() thực thi, đối tượng điều khiển sẽ được truyền sang khối coroutineScope{..}.
  • Khối coroutineScope khởi chạy đồng thời cả hai coroutine và chờ các coroutine này hoàn tất quá trình thực thi.
  • Sau khi quá trình thực thi hoàn tất, cờ raceInProgress sẽ cập nhật.

Khối coroutineScope chỉ trả về và tiếp tục sau khi tất cả mã bên trong khối này hoàn tất quá trình thực thi. Đối với mã bên ngoài khối đó, việc có mô hình đồng thời hay không sẽ trở thành chi tiết triển khai đơn thuần. Kiểu lập trình này mang đến một phương pháp có cấu trúc để lập trình đồng thời và được gọi là mô hình đồng thời có cấu trúc.

Khi bạn nhấp vào nút Reset (Đặt lại) sau khi cuộc đua hoàn tất, các coroutine sẽ bị huỷ và tiến trình của cả hai người chơi sẽ được đặt lại thành 0.

Để xem cách coroutine bị huỷ khi người dùng nhấp vào nút Reset (Đặt lại), hãy làm theo các bước sau đây:

  1. Gói nội dung của phương thức run() trong một khối try-catch như minh hoạ trong mã dưới đây:
suspend fun run() {
    try {
        while (currentProgress < maxProgress) {
            delay(progressDelayMillis)
            currentProgress += progressIncrement
        }
    } catch (e: CancellationException) {
        Log.e("RaceParticipant", "$name: ${e.message}")
        throw e // Always re-throw CancellationException.
    }
}
  1. Chạy ứng dụng rồi nhấp vào nút Start (Bắt đầu).
  2. Sau khi tiến trình tăng lên, hãy nhấp vào nút Reset (Đặt lại).
  3. Đảm bảo bạn thấy thông báo dưới đây được in trong Logcat:
Player 1: StandaloneCoroutine was cancelled
Player 2: StandaloneCoroutine was cancelled

7. Viết mã kiểm thử đơn vị để kiểm thử coroutine

Cần chú trọng hơn mã kiểm thử đơn vị sử dụng coroutine, vì quá trình thực thi có thể không đồng bộ và xảy ra trên nhiều luồng.

Để gọi các hàm tạm ngưng trong khi kiểm thử, bạn cần phải ở trong một coroutine. Vì bản thân hàm kiểm thử JUnit không phải là hàm tạm ngưng, nên bạn cần sử dụng trình tạo coroutine runTest. Trình tạo này nằm trong thư viện kotlinx-coroutines-test và được thiết kế để thực thi kiểm thử. Trình tạo này thực thi phần nội dung kiểm thử trong một coroutine mới.

runTest là nằm trong thư viện kotlinx-coroutines-test, nên bạn cần thêm phần phụ thuộc của thư viện đó.

Để thêm phần phụ thuộc này, hãy hoàn tất các bước sau đây:

  1. Mở tệp build.gradle.kts của mô-đun ứng dụng, nằm ở thư mục app trong ngăn Project (Dự án).

e7c9e573c41199c6.png

  1. Bên trong tệp này, hãy cuộn xuống cho đến khi bạn thấy khối dependencies{}.
  2. Sử dụng cấu hình testImplementation để thêm phần phụ thuộc vào thư viện kotlinx-coroutines-test.
plugins {
    ...
}

android {
    ...
}

dependencies {
    ...
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
}
  1. Trong thanh thông báo ở đầu tệp build.gradle.kts, hãy nhấp vào Sync Now (Đồng bộ hoá ngay) để cho phép hoàn tất các thao tác nhập và tạo bản dựng như trong ảnh chụp màn hình sau:

1c20fc10750ca60c.png

Sau khi quá trình tạo bản dựng hoàn tất, bạn có thể bắt đầu viết mã kiểm thử.

Triển khai mã kiểm thử đơn vị để bắt đầu và kết thúc cuộc đua

Để đảm bảo tiến trình cuộc đua cập nhật chính xác trong các giai đoạn của cuộc đua, các mã kiểm thử đơn vị của bạn cần phải phù hợp với nhiều tình huống. Lớp học lập trình này sẽ đề cập đến 2 tình huống sau đây:

  • Tiến trình sau khi cuộc đua bắt đầu.
  • Tiến trình sau khi cuộc đua kết thúc.

Để kiểm tra xem tiến trình cuộc đua có cập nhật chính xác sau khi bắt đầu cuộc đua hay không, bạn cần xác nhận rằng tiến trình hiện tại được đặt thành 1 sau khi hết khoảng thời gian raceParticipant.progressDelayMillis.

Để triển khai tình huống kiểm thử, hãy làm theo các bước sau đây:

  1. Chuyển đến tệp RaceParticipantTest.kt nằm trong nhóm tài nguyên kiểm thử.
  2. Để định nghĩa kiểm thử, hãy tạo một hàm raceParticipant_RaceStarted_ProgressUpdated() sau định nghĩa raceParticipant và chú thích hàm đó bằng chú thích @Test. Vì khối kiểm thử cần được đặt trong trình tạo runTest, hãy sử dụng cú pháp biểu thức để trả về kết quả kiểm thử là khối runTest().
class RaceParticipantTest {
    private val raceParticipant = RaceParticipant(
        ...
    )

    @Test
    fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
    }
}
  1. Thêm biến expectedProgress chỉ có thể đọc và đặt biến đó thành 1.
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
    val expectedProgress = 1
}
  1. Để mô phỏng cuộc đua bắt đầu, hãy sử dụng trình tạo launch để khởi chạy một coroutine mới và gọi hàm raceParticipant.run().
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
    val expectedProgress = 1
    launch { raceParticipant.run() }
}

Giá trị của thuộc tính raceParticipant.progressDelayMillis xác định khoảng thời gian mà sau đó tiến trình cuộc đua cập nhật. Để kiểm thử tiến trình sau khi hết thời gian progressDelayMillis, bạn cần thêm một dạng độ trễ vào quy trình kiểm thử.

  1. Sử dụng hàm trợ giúp advanceTimeBy() để đẩy nhanh tiến độ theo giá trị của raceParticipant.progressDelayMillis. Hàm advanceTimeBy() giúp giảm thời gian thực thi kiểm thử.
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
    val expectedProgress = 1
    launch { raceParticipant.run() }
    advanceTimeBy(raceParticipant.progressDelayMillis)
}
  1. advanceTimeBy() không chạy tác vụ đã lên lịch trong một khoảng thời gian nhất định, nên bạn cần gọi hàm runCurrent(). Hàm này thực thi mọi thao tác đang chờ xử lý ở thời điểm hiện tại.
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
    val expectedProgress = 1
    launch { raceParticipant.run() }
    advanceTimeBy(raceParticipant.progressDelayMillis)
    runCurrent()
}
  1. Để đảm bảo tiến trình này sẽ cập nhật, hãy thêm một lệnh gọi vào hàm assertEquals() để kiểm tra xem giá trị của thuộc tính raceParticipant.currentProgress có khớp với giá trị của biến expectedProgress hay không.
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
    val expectedProgress = 1
    launch { raceParticipant.run() }
    advanceTimeBy(raceParticipant.progressDelayMillis)
    runCurrent()
    assertEquals(expectedProgress, raceParticipant.currentProgress)
}
  1. Chạy kiểm thử để xác nhận là kiểm thử đạt.

Để kiểm tra xem tiến trình cuộc đua có cập nhật chính xác sau khi cuộc đua kết thúc hay không, bạn cần xác nhận rằng khi cuộc đua kết thúc, tiến trình hiện tại sẽ được đặt thành 100.

Hãy thực hiện theo các bước sau đây để triển khai kiểm thử:

  1. Sau khi tạo hàm kiểm thử raceParticipant_RaceStarted_ProgressUpdated(), hãy tạo một hàm raceParticipant_RaceFinished_ProgressUpdated() và chú giải hàm đó bằng @Test. Hàm này sẽ trả về kết quả kiểm thử từ khối runTest{}.
class RaceParticipantTest {
    ...

    @Test
    fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
        ...
    }

    @Test
    fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
    }
}
  1. Sử dụng trình tạo launch để chạy một coroutine mới và thêm một lệnh gọi vào hàm raceParticipant.run() trong coroutine đó.
@Test
fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
    launch { raceParticipant.run() }
}
  1. Để mô phỏng cuộc đua kết thúc, hãy sử dụng hàm advanceTimeBy() để đẩy nhanh tiến độ của trình điều phối thêm raceParticipant.maxProgress * raceParticipant.progressDelayMillis:
@Test
fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
    launch { raceParticipant.run() }
    advanceTimeBy(raceParticipant.maxProgress * raceParticipant.progressDelayMillis)
}
  1. Thêm lệnh gọi vào hàm runCurrent() để thực thi mọi thao tác đang chờ xử lý.
@Test
fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
    launch { raceParticipant.run() }
    advanceTimeBy(raceParticipant.maxProgress * raceParticipant.progressDelayMillis)
    runCurrent()
}
  1. Để đảm bảo tiến trình sẽ cập nhật chính xác, hãy thêm một lệnh gọi vào hàm assertEquals() để kiểm tra xem giá trị của thuộc tính raceParticipant.currentProgress có bằng 100 hay không.
@Test
fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
    launch { raceParticipant.run() }
    advanceTimeBy(raceParticipant.maxProgress * raceParticipant.progressDelayMillis)
    runCurrent()
    assertEquals(100, raceParticipant.currentProgress)
}
  1. Chạy kiểm thử để xác nhận là kiểm thử đạt.

Tham gia thử thách này

Áp dụng các chiến lược kiểm thử được thảo luận trong lớp học lập trình Viết mã kiểm thử đơn vị cho ViewModel. Thêm mã kiểm thử để xác định lộ trình phù hợp, trường hợp lỗi và trường hợp ranh giới.

So sánh mã kiểm thử bạn viết với mã kiểm thử có sẵn trong mã giải pháp.

8. Lấy mã nguồn giải pháp

Để tải mã này xuống khi lớp học lập trình đã kết thúc, bạn có thể sử dụng các lệnh git sau:

git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-race-tracker.git
cd basic-android-kotlin-compose-training-race-tracker

Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp zip rồi giải nén và mở trong Android Studio.

Nếu bạn muốn xem mã giải pháp, hãy xem mã đó trên GitHub.

9. Kết luận

Xin chúc mừng! Bạn vừa tìm hiểu cách sử dụng coroutine để xử lý đồng thời. Coroutine giúp quản lý các tác vụ chạy trong thời gian dài có thể chặn luồng chính và khiến ứng dụng của bạn không phản hồi. Bạn cũng đã tìm hiểu cách viết mã kiểm thử đơn vị để kiểm thử coroutine.

Các đặc điểm dưới đây là một số lợi ích của coroutine:

  • Dễ đọc: Mã bạn viết bằng coroutine giúp bạn hiểu rõ trình tự thực thi các dòng mã.
  • Tích hợp Jetpack: Nhiều thư viện Jetpack, chẳng hạn như Compose và ViewModel, bao gồm các tiện ích hỗ trợ đầy đủ cho coroutine. Một số thư viện cũng cung cấp phạm vi coroutine riêng mà bạn có thể dùng cho mô hình xử lý đồng thời có cấu trúc.
  • Mô hình đồng thời có cấu trúc: Coroutine giúp triển khai mã đồng thời an toàn và dễ dàng, loại bỏ mã nguyên mẫu không cần thiết và đảm bảo rằng các coroutine do ứng dụng khởi chạy không bị mất hoặc lãng phí tài nguyên.

Tóm tắt

  • Với coroutine, bạn có thể viết mã chạy trong thời gian dài để chạy đồng thời mà không cần tìm hiểu kiểu lập trình mới. Quá trình thực thi coroutine tuần tự theo thiết kế.
  • Từ khoá suspend được dùng để đánh dấu một hàm hoặc loại hàm, cho biết hàm đó có sẵn để thực thi, tạm dừng và tiếp tục thực hiện một tập hợp các lệnh mã hay không.
  • Hệ thống chỉ có thể gọi một hàm suspend từ một hàm tạm ngưng khác.
  • Bạn có thể bắt đầu một coroutine mới bằng cách sử dụng hàm tạo launch hoặc async.
  • Ngữ cảnh coroutine, trình tạo coroutine, Tác vụ, phạm vi coroutine và Trình điều phối là các thành phần chính để triển khai coroutine.
  • Coroutine sử dụng trình điều phối để xác định luồng cần sử dụng cho quá trình thực thi.
  • Tác vụ đóng vai trò quan trọng trong việc đảm bảo mô hình đồng thời có cấu trúc bằng cách quản lý vòng đời của coroutine và duy trì mối quan hệ mẹ con.
  • CoroutineContext xác định hành vi của một coroutine thông qua Tác vụ và một trình điều phối coroutine.
  • CoroutineScope kiểm soát toàn bộ thời gian của coroutine thông qua Tác vụ, đồng thời thực thi việc huỷ và các quy tắc khác đối với các thành phần con cũng như con của các thành phần con đó theo cách đệ quy.
  • Khởi chạy, hoàn tất, huỷ và lỗi là 4 thao tác phổ biến trong quá trình thực thi coroutine.
  • Coroutine tuân theo nguyên tắc của mô hình đồng thời có cấu trúc.

Tìm hiểu thêm