Coroutine là một mẫu thiết kế cho cơ chế xử lý đồng thời mà bạn có thể dùng trên Android để đơn giản hoá mã nguồn thực thi không đồng bộ. Coroutine đã được thêm vào Kotlin trong phiên bản 1.3 và dựa trên dữ liệu đã được thiết lập khái niệm bằng các ngôn ngữ khác.
Trên Android, 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. Hơn 50% nhà phát triển chuyên nghiệp sử dụng coroutine cho biết rằng năng suất làm việc của họ tăng lên. Chủ đề này trình bày cách bạn có thể sử dụng coroutine của Kotlin để giải quyết các vấn đề này, qua đó cho phép bạn viết mã ứng dụng rõ ràng và súc tích hơn.
Tính năng
Coroutine là giải pháp chúng tôi đề xuất để lập trình không đồng bộ trên Android. Những tính năng đáng chú ý bao gồm:
- Dung lượng nhẹ: Bạn có thể chạy nhiều coroutine trên một luồng nhờ tính năng hỗ trợ tạm ngưng, tính năng này không chặn luồng mà coroutine đang chạy. Việc tạm ngưng giúp tiết kiệm bộ nhớ hơn là chặn, trong khi vẫn hỗ trợ được nhiều thao tác đồng thời.
- Ít rò rỉ bộ nhớ hơn: Sử dụng mô hình đồng thời có cấu trúc để chạy các thao tác trong một phạm vi.
- Tích hợp sẵn tính năng hỗ trợ huỷ: Huỷ được truyền tự động thông qua hệ phân cấp coroutine đang chạy.
- Tích hợp Jetpack: Nhiều thư viện Jetpack có 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 cơ chế xử lý đồng thời có cấu trúc.
Tổng quan về ví dụ
Dựa trên Hướng dẫn về kiến trúc ứng dụng, các ví dụ trong chủ đề này sẽ đưa ra một yêu cầu mạng và trả về kết quả cho luồng chính, tại đó ứng dụng có thể hiển thị kết quả cho người dùng.
Cụ thể, thành phần Kiến trúc ViewModel
gọi tầng lưu trữ trên luồng chính để
kích hoạt yêu cầu mạng. Hướng dẫn này lặp lại qua nhiều giải pháp
sử dụng coroutine để đảm bảo luồng chính không bị chặn.
ViewModel
bao gồm một tập hợp các tiện tích KTX, hoạt động trực tiếp với coroutine. Các tiện ích này là
thư viện lifecycle-viewmodel-ktx
và được dùng
trong hướng dẫn này.
Thông tin về phần phụ thuộc
Để sử dụng coroutine trong dự án Android, hãy thêm phần phụ thuộc sau vào tệp build.gradle
của ứng dụng:
Groovy
dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' }
Kotlin
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") }
Thực thi trong luồng ở chế độ nền
Việc tạo một yêu cầu mạng trên luồng chính sẽ khiến luồng này chờ hoặc bị chặn, cho đến khi nhận được phản hồi. Vì luồng này đã bị chặn, nên hệ điều hành sẽ không thể
gọi onDraw()
, điều này sẽ khiến ứng dụng của bạn bị treo và có thể
gây ra hộp thoại Ứng dụng không phản hồi (ANR). Để có trải nghiệm tốt hơn
cho người dùng, hãy chạy thao tác này trên luồng ở chế độ nền.
Trước tiên, hãy xem lớp Repository
của chúng tôi để thấy cách lớp này tạo yêu cầu mạng:
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
class LoginRepository(private val responseParser: LoginResponseParser) {
private const val loginUrl = "https://example.com/login"
// Function that makes the network request, blocking the current thread
fun makeLoginRequest(
jsonBody: String
): Result<LoginResponse> {
val url = URL(loginUrl)
(url.openConnection() as? HttpURLConnection)?.run {
requestMethod = "POST"
setRequestProperty("Content-Type", "application/json; utf-8")
setRequestProperty("Accept", "application/json")
doOutput = true
outputStream.write(jsonBody.toByteArray())
return Result.Success(responseParser.parse(inputStream))
}
return Result.Error(Exception("Cannot open HttpURLConnection"))
}
}
makeLoginRequest
đồng bộ và chặn luồng lệnh gọi. Để lập mô hình
phản hồi của yêu cầu mạng, chúng tôi có lớp Result
riêng.
ViewModel
kích hoạt yêu cầu mạng khi người dùng nhấp vào một nút:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
Ở mã trước, LoginViewModel
đang chặn luồng giao diện người dùng khi
đưa ra yêu cầu mạng. Giải pháp đơn giản nhất để di chuyển
quá trình thực thi luồng chính là tạo một coroutine mới và thực hiện yêu cầu
mạng trên luồng I/O:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
// Create a new coroutine to move the execution off the UI thread
viewModelScope.launch(Dispatchers.IO) {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
}
Hãy phân tích mã coroutine trong hàm login
:
viewModelScope
là mộtCoroutineScope
được xác định trước, đi kèm với các tiện íchViewModel
của KTX. Lưu ý rằng tất cả các coroutine phải chạy trong một phạm vi. MộtCoroutineScope
quản lý một hoặc nhiều coroutine có liên quan.launch
là hàm tạo ra coroutine và gửi quá trình thực thi nội dung của hàm đó cho trình điều phối tương ứng.Dispatchers.IO
cho biết rằng coroutine này sẽ được thực thi trên một luồng dành riêng cho các thao tác I/O.
Hàm login
được thực thi như sau:
- Ứng dụng này gọi hàm
login
từ tầngView
trên luồng chính. launch
tạo ra một coroutine mới và yêu cầu mạng được thực hiện độc lập trên một luồng dành riêng cho các thao tác I/O.- Khi coroutine đang chạy, hàm
login
sẽ tiếp tục thực thi và quay về, có thể là trước khi yêu cầu mạng kết thúc. Xin lưu ý rằng để đơn giản, phản hồi mạng hiện sẽ bị bỏ qua.
Vì coroutine này được bắt đầu bằng viewModelScope
nên sẽ được thực thi trong
phạm vi của ViewModel
. Nếu ViewModel
bị huỷ do
người dùng rời khỏi màn hình, thì viewModelScope
sẽ tự động bị huỷ
và tất cả các coroutine đang chạy cũng sẽ bị huỷ.
Ví dụ trước có một vấn đề là bất cứ hàm nào gọi makeLoginRequest
đều phải nhớ di chuyển rõ ràng quá trình thực thi đó khỏi luồng chính. Hãy xem cách chúng tôi có thể sửa đổi Repository
để giải quyết vấn đề này.
Dùng coroutine để đảm bảo an toàn cho luồng chính
Chúng tôi coi một hàm là main-safe (an toàn cho luồng chính) khi hàm này không chặn các bản cập nhật giao diện người dùng trên
luồng chính. Hàm makeLoginRequest
không an toàn cho luồng chính vì lệnh gọi makeLoginRequest
từ luồng chính sẽ chặn giao diện người dùng. Bạn có thể sử dụng hàm
withContext()
từ thư viện coroutine để di chuyển quy trình thực thi
của coroutine sang một luồng khác:
class LoginRepository(...) {
...
suspend fun makeLoginRequest(
jsonBody: String
): Result<LoginResponse> {
// Move the execution of the coroutine to the I/O dispatcher
return withContext(Dispatchers.IO) {
// Blocking network request code
}
}
}
withContext(Dispatchers.IO)
sẽ di chuyển quy trình thực thi coroutine sang một luồng I/O, giúp hàm gọi của chúng tôi an toàn cho luồng chính và cho phép giao diện người dùng
cập nhật khi cần.
makeLoginRequest
cũng được đánh dấu bằng từ khoá suspend
. Từ khoá này là cách để Kotlin thực thi hàm được gọi trong một coroutine.
Trong ví dụ sau, coroutine được tạo trong LoginViewModel
.
Khi makeLoginRequest
di chuyển quy trình thực thi ra khỏi luồng chính, thì giờ đây,
coroutine trong hàm login
có thể được thực thi trong luồng chính:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
// Create a new coroutine on the UI thread
viewModelScope.launch {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
// Make the network call and suspend execution until it finishes
val result = loginRepository.makeLoginRequest(jsonBody)
// Display result of the network request to the user
when (result) {
is Result.Success<LoginResponse> -> // Happy path
else -> // Show error in UI
}
}
}
}
Xin lưu ý rằng bạn vẫn cần có coroutine trong trường hợp này vì makeLoginRequest
là một hàm suspend
và tất cả các hàm suspend
phải được thực thi trong coroutine.
Mã này khác với ví dụ trước đó về login
ở một số khía cạnh như sau:
launch
không lấy tham sốDispatchers.IO
. Khi bạn không truyềnDispatcher
đếnlaunch
, bất kỳ coroutine nào khởi chạy từviewModelScope
sẽ chạy trong luồng chính.- Kết quả của yêu cầu mạng được xử lý để hiện giao diện người dùng thành công hoặc không thành công.
Hàm đăng nhập thực thi như sau:
- Ứng dụng này gọi hàm
login()
từ layer (lớp)View
trên luồng chính. launch
tạo một coroutine mới trên luồng chính và coroutine này bắt đầu thực thi.- Trong coroutine, lệnh gọi đến
loginRepository.makeLoginRequest()
lúc này sẽ tạm ngưng việc thực thi coroutine cho đến khi khốiwithContext
trongmakeLoginRequest()
chạy xong. - Sau khi khối
withContext
kết thúc, coroutine tronglogin()
sẽ tiếp tục thực thi trên luồng chính bằng kết quả của yêu cầu mạng.
Xử lý ngoại lệ
Để xử lý ngoại lệ mà lớp Repository
có thể gửi, hãy dùng tính năng hỗ trợ tích hợp sẵn của Kotlin cho ngoại lệ.
Trong ví dụ sau, chúng tôi sử dụng khối try-catch
:
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
viewModelScope.launch {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
val result = try {
loginRepository.makeLoginRequest(jsonBody)
} catch(e: Exception) {
Result.Error(Exception("Network request failed"))
}
when (result) {
is Result.Success<LoginResponse> -> // Happy path
else -> // Show error in UI
}
}
}
}
Trong ví dụ này, mọi ngoại lệ không mong muốn do lệnh gọi makeLoginRequest()
gửi
sẽ được coi là một lỗi trong giao diện người dùng.
Tài nguyên khác về coroutine
Để biết thêm thông tin chi tiết về coroutine trên Android, hãy xem phần Cải thiện hiệu năng của ứng dụng bằng coroutine của Kotlin.
Để xem thêm tài nguyên về coroutine, hãy xem các đường liên kết sau:
- Tổng quan coroutine (JetBrains)
- Hướng dẫn về coroutine (JetBrains)
- Tài nguyên khác về coroutine và flow (luồng) trong Kotlin