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.
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.
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.
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 đó:
- Mở lớp
RaceParticipant
nằm trong mã khởi đầu. - Bên trong lớp
RaceParticipant
, hãy xác định một hàmsuspend
mới có tên làrun()
.
class RaceParticipant(
...
) {
var currentProgress by mutableStateOf(initialProgress)
private set
suspend fun run() {
}
...
}
- Để 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 khicurrentProgress
đạ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) {
}
}
...
}
- 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ínhprogressIncrement
trong vòng lặp while. Lưu ý rằng giá trị mặc định củaprogressIncrement
là1
.
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
}
}
}
- Để 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ínhprogressDelayMillis
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:
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:
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:
- Mở tệp
RaceTrackerApp.kt
nằm trong góicom.example.racetracker.ui
. - 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ợpLaunchedEffect()
trên dòng sau định nghĩaraceInProgress
.
@Composable
fun RaceTrackerApp() {
...
var raceInProgress by remember { mutableStateOf(false) }
LaunchedEffect {
}
RaceTrackerScreen(...)
}
- Để đảm bảo rằng nếu thực thể của
playerOne
hoặcplayerTwo
đượ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ượngplayerOne
vàplayerTwo
làmkey
vàoLaunchedEffect
. Tương tự như cách kết hợp lại một thành phần kết hợpText()
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ủaLaunchedEffect()
thay đổi thì coroutine cơ bản sẽ bị huỷ và khởi chạy lại.
LaunchedEffect(playerOne, playerTwo) {
}
- Thêm một lệnh gọi vào các hàm
playerOne.run()
vàplayerTwo.run()
.
@Composable
fun RaceTrackerApp() {
...
var raceInProgress by remember { mutableStateOf(false) }
LaunchedEffect(playerOne, playerTwo) {
playerOne.run()
playerTwo.run()
}
RaceTrackerScreen(...)
}
- Gói khối
LaunchedEffect()
bằng một điều kiệnif
. Giá trị ban đầu của trạng thái này làfalse
. Giá trị của trạng tháiraceInProgress
được cập nhật thànhtrue
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()
}
}
- Cập nhật cờ
raceInProgress
thànhfalse
để kết thúc cuộc đua. Giá trị này được đặt thànhfalse
khi người dùng nhấp vào Pause (Tạm dừng). Khi giá trị này được đặt thànhfalse
,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
}
- 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:
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
- Để 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 đếnplayerOne.run()
bằng trình tạolaunch
.
LaunchedEffect(playerOne, playerTwo) {
launch { playerOne.run() }
playerTwo.run()
raceInProgress = false
}
- Tương tự, hãy gói lệnh gọi đến hàm
playerTwo.run()
bằng trình tạolaunch
. 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
}
- 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.
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 thiplayerOne.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àmplayerTwo.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()
.
- Để đảm bảo hàm
run()
củaplayerOne
vàplayerTwo
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ốicoroutineScope
.
LaunchedEffect(playerOne, playerTwo) {
coroutineScope {
launch { playerOne.run() }
launch { playerTwo.run() }
}
raceInProgress = false
}
- 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:
- 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.
Luồng thực thi được minh hoạ trong sơ đồ dưới đây:
- Khi khối
LaunchedEffect()
thực thi, đối tượng điều khiển sẽ được truyền sang khốicoroutineScope{..}
. - 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:
- 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.
}
}
- Chạy ứng dụng rồi nhấp vào nút Start (Bắt đầu).
- Sau khi tiến trình tăng lên, hãy nhấp vào nút Reset (Đặt lại).
- Đả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.
Vì 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:
- Mở tệp
build.gradle.kts
của mô-đun ứng dụng, nằm ở thư mụcapp
trong ngăn Project (Dự án).
- Bên trong tệp này, hãy cuộn xuống cho đến khi bạn thấy khối
dependencies{}
. - Sử dụng cấu hình
testImplementation
để thêm phần phụ thuộc vào thư việnkotlinx-coroutines-test
.
plugins {
...
}
android {
...
}
dependencies {
...
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
}
- 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:
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:
- Chuyển đến tệp
RaceParticipantTest.kt
nằm trong nhóm tài nguyên kiểm thử. - Để định nghĩa kiểm thử, hãy tạo một hàm
raceParticipant_RaceStarted_ProgressUpdated()
sau định nghĩaraceParticipant
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ạorunTest
, hãy sử dụng cú pháp biểu thức để trả về kết quả kiểm thử là khốirunTest()
.
class RaceParticipantTest {
private val raceParticipant = RaceParticipant(
...
)
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
}
}
- Thêm biến
expectedProgress
chỉ có thể đọc và đặt biến đó thành1
.
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
val expectedProgress = 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àmraceParticipant.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ử.
- Sử dụng hàm trợ giúp
advanceTimeBy()
để đẩy nhanh tiến độ theo giá trị củaraceParticipant.progressDelayMillis
. HàmadvanceTimeBy()
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)
}
- Vì
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àmrunCurrent()
. 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()
}
- Để đả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ínhraceParticipant.currentProgress
có khớp với giá trị của biếnexpectedProgress
hay không.
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
val expectedProgress = 1
launch { raceParticipant.run() }
advanceTimeBy(raceParticipant.progressDelayMillis)
runCurrent()
assertEquals(expectedProgress, raceParticipant.currentProgress)
}
- 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ử:
- Sau khi tạo hàm kiểm thử
raceParticipant_RaceStarted_ProgressUpdated()
, hãy tạo một hàmraceParticipant_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ốirunTest{}
.
class RaceParticipantTest {
...
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
...
}
@Test
fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
}
}
- 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àmraceParticipant.run()
trong coroutine đó.
@Test
fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
launch { raceParticipant.run() }
}
- Để 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êmraceParticipant.maxProgress * raceParticipant.progressDelayMillis
:
@Test
fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
launch { raceParticipant.run() }
advanceTimeBy(raceParticipant.maxProgress * raceParticipant.progressDelayMillis)
}
- 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()
}
- Để đả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ínhraceParticipant.currentProgress
có bằng100
hay không.
@Test
fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
launch { raceParticipant.run() }
advanceTimeBy(raceParticipant.maxProgress * raceParticipant.progressDelayMillis)
runCurrent()
assertEquals(100, raceParticipant.currentProgress)
}
- 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ặcasync
. - 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.