Coroutine của Kotlin cung cấp một API cho phép bạn viết mã không đồng bộ. Với coroutine của Kotlin, bạn có thể xác định một CoroutineScope
để giúp bạn quản lý thời điểm coroutine sẽ chạy. Mỗi thao tác không đồng bộ chạy trong một phạm vi cụ thể.
Các thành phần nhận biết vòng đời cung cấp sự hỗ trợ tốt nhất cho coroutine trong phạm vi logic của ứng dụng, cùng với một lớp tương tác có LiveData
.
Chủ đề này giải thích cách sử dụng coroutine một cách hiệu quả thông qua các thành phần nhận biết vòng đời.
Thêm các phần phụ thuộc KTX
Các phạm vi coroutine tích hợp sẵn được mô tả trong chủ đề này nằm trong chức năng KTX mở rộng đối với từng thành phần tương ứng. Hãy nhớ thêm phần phụ thuộc thích hợp khi sử dụng các phạm vi này.
- Đối với
ViewModelScope
, hãy sử dụngandroidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0
trở lên. - Đối với
LifecycleScope
, hãy sử dụngandroidx.lifecycle:lifecycle-runtime-ktx:2.4.0
trở lên. - Đối với
liveData
, hãy sử dụngandroidx.lifecycle:lifecycle-livedata-ktx:2.4.0
trở lên.
Phạm vi coroutine nhận biết vòng đời
Thành phần nhận biết vòng đời xác định các phạm vi tích hợp mà bạn có thể dùng trong ứng dụng.
ViewModelScope
Một ViewModelScope
được khai báo cho từng ViewModel
trong ứng dụng của bạn. Hệ thống sẽ tự động huỷ mọi coroutine chạy trong phạm vi này nếu ViewModel
bị xoá. Coroutine rất hữu ích trong trường hợp này khi bạn chỉ cần hoàn thành công việc nếu ViewModel
đang hoạt động. Ví dụ: nếu bạn đang tính toán một số dữ liệu cho một bố cục, bạn nên đặt phạm vi của công việc thành ViewModel
để nếu ViewModel
bị xoá, công việc này sẽ tự động bị huỷ để tránh tiêu tốn tài nguyên.
Bạn có thể truy cập vào CoroutineScope
của ViewModel
thông qua thuộc tính viewModelScope
của ViewModel, như trong ví dụ sau:
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
}
LifecycleScope
LifecycleScope
được khai báo cho từng đối tượng Lifecycle
. Hệ thống sẽ tự động huỷ mọi coroutine chạy trong phạm vi này khi Lifecycle
bị huỷ bỏ. Bạn có thể truy cập vào CoroutineScope
của Lifecycle
thông qua thuộc tính lifecycle.coroutineScope
hoặc thuộc tính lifecycleOwner.lifecycleScope
.
Ví dụ bên dưới minh hoạ cách sử dụng lifecycleOwner.lifecycleScope
để tạo văn bản được tính toán trước một cách không đồng bộ:
class MyFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
val params = TextViewCompat.getTextMetricsParams(textView)
val precomputedText = withContext(Dispatchers.Default) {
PrecomputedTextCompat.create(longTextContent, params)
}
TextViewCompat.setPrecomputedText(textView, precomputedText)
}
}
}
Coroutine nhận biết vòng đời có thể khởi động lại
Mặc dù lifecycleScope
cung cấp cách thích hợp để tự động huỷ các thao tác chạy trong thời gian dài khi Lifecycle
ở trạng thái DESTROYED
, nhưng có thể vẫn có trường hợp bạn muốn bắt đầu thực thi một khối mã khi Lifecycle
đang ở một trạng thái nhất định và huỷ khi thành phần này ở trạng thái khác. Ví dụ: Bạn có thể muốn thu thập một luồng khi Lifecycle
ở trạng thái STARTED
và huỷ khi quá trình thu thập ở trạng thái STOPPED
. Phương pháp này chỉ xử lý các giá trị mà luồng xuất ra khi giao diện người dùng xuất hiện trên màn hình, qua đó tiết kiệm tài nguyên và có thể tránh sự cố trên ứng dụng.
Đối với những trường hợp như vậy, Lifecycle
và LifecycleOwner
sẽ cung cấp API repeatOnLifecycle
tạm ngưng để thực hiện việc đó. Khối mã trong ví dụ sau đây sẽ chạy mỗi khi Lifecycle
liên kết tối thiểu đang ở trạng thái STARTED
và huỷ khi Lifecycle
ở trạng thái STOPPED
:
class MyFragment : Fragment() {
val viewModel: MyViewModel by viewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Create a new coroutine in the lifecycleScope
viewLifecycleOwner.lifecycleScope.launch {
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Trigger the flow and start listening for values.
// This happens when lifecycle is STARTED and stops
// collecting when the lifecycle is STOPPED
viewModel.someDataFlow.collect {
// Process item
}
}
}
}
}
Thu thập quy trình nhận biết vòng đời
Nếu chỉ cần thực hiện thu thập nhận biết vòng đời trên một luồng duy nhất, bạn có thể sử dụng phương thức Flow.flowWithLifecycle()
để đơn giản hóa mã của mình:
viewLifecycleOwner.lifecycleScope.launch {
exampleProvider.exampleFlow()
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect {
// Process the value.
}
}
Tuy nhiên, nếu cần thực hiện thu thập nhận biết vòng đời trên nhiều luồng một lúc, bạn phải thu thập từng luồng trong các coroutine khác nhau. Trong trường hợp đó, sử dụng trực tiếp repeatOnLifecycle()
sẽ hiệu quả hơn:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Because collect is a suspend function, if you want to
// collect multiple flows in parallel, you need to do so in
// different coroutines.
launch {
flow1.collect { /* Process the value. */ }
}
launch {
flow2.collect { /* Process the value. */ }
}
}
}
Coroutine nhận biết vòng đời tạm ngưng
Mặc dù CoroutineScope
cung cấp cách thích hợp để tự động huỷ các thao tác chạy trong thời gian dài, nhưng có thể vẫn có trường hợp bạn muốn tạm ngưng việc thực thi khối mã, trừ phi Lifecycle
đang ở một trạng thái nhất định. Ví dụ: để chạy một FragmentTransaction
, bạn phải đợi cho đến khi Lifecycle
ít nhất đang ở trạng thái STARTED
. Đối với những trường hợp như vậy, Lifecycle
cung cấp các phương thức bổ sung: lifecycle.whenCreated
, lifecycle.whenStarted
và lifecycle.whenResumed
. Mọi coroutine chạy bên trong các khối này đều bị tạm ngưng nếu Lifecycle
không ở trạng thái mong muốn tối thiểu.
Khối mã trong ví dụ bên dưới chỉ chạy khi Lifecycle
được liên kết đang ở trạng thái STARTED
(tối thiểu):
class MyFragment: Fragment {
init { // Notice that we can safely launch in the constructor of the Fragment.
lifecycleScope.launch {
whenStarted {
// The block inside will run only when Lifecycle is at least STARTED.
// It will start executing when fragment is started and
// can call other suspend methods.
loadingView.visibility = View.VISIBLE
val canAccess = withContext(Dispatchers.IO) {
checkUserAccess()
}
// When checkUserAccess returns, the next line is automatically
// suspended if the Lifecycle is not *at least* STARTED.
// We could safely run fragment transactions because we know the
// code won't run unless the lifecycle is at least STARTED.
loadingView.visibility = View.GONE
if (canAccess == false) {
findNavController().popBackStack()
} else {
showContent()
}
}
// This line runs only after the whenStarted block above has completed.
}
}
}
Nếu Lifecycle
bị huỷ bỏ trong khi một coroutine đang hoạt động thông qua một trong các phương thức when
, thì coroutine này sẽ tự động bị huỷ. Trong ví dụ bên dưới, khối finally
sẽ chạy khi trạng thái của Lifecycle
là DESTROYED
:
class MyFragment: Fragment {
init {
lifecycleScope.launchWhenStarted {
try {
// Call some suspend functions.
} finally {
// This line might execute after Lifecycle is DESTROYED.
if (lifecycle.state >= STARTED) {
// Here, since we've checked, it is safe to run any
// Fragment transactions.
}
}
}
}
}
Sử dụng coroutine với LiveData
Khi sử dụng LiveData
, bạn có thể sẽ phải tính toán các giá trị một cách không đồng bộ. Ví dụ: bạn có thể sẽ muốn truy xuất các lựa chọn ưu tiên của người dùng và cung cấp các lựa chọn đó đến giao diện người dùng. Trong những trường hợp này, bạn có thể sử dụng hàm tạo liveData
để gọi hàm suspend
và cung cấp kết quả dưới dạng đối tượng LiveData
.
Trong ví dụ bên dưới, loadUser()
là hàm tạm ngưng được khai báo ở nơi khác. Sử dụng hàm tạo liveData
để gọi loadUser()
một cách không đồng bộ, sau đó sử dụng emit()
để xuất kết quả:
val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}
Khối liveData
đóng vai trò là thành phần cơ bản đồng thời có cấu trúc (structured concurrency primitive) giữa các coroutine và LiveData
. Khối mã này bắt đầu thực thi khi LiveData
bắt đầu hoạt động và tự động bị huỷ sau khi hết thời gian chờ trong cấu hình khi LiveData
ngừng hoạt động. Nếu bị huỷ trước khi hoàn tất thì khối mã này sẽ khởi động lại trong trường hợp LiveData
hoạt động trở lại. Nếu đã hoàn tất thành công trong lần chạy trước đó thì khối mã sẽ không khởi động lại. Xin lưu ý rằng khối mã này chỉ khởi động lại trong trường hợp bị huỷ tự động. Nếu bị huỷ vì bất kỳ lý do nào khác (ví dụ: gửi một CancellationException
), thì khối sẽ không khởi động lại.
Bạn cũng có thể xuất nhiều giá trị từ khối này ra. Mỗi lệnh gọi emit()
sẽ tạm ngưng quá trình thực thi của khối này cho đến khi bạn đặt giá trị LiveData
trên chuỗi chính.
val user: LiveData<Result> = liveData {
emit(Result.loading())
try {
emit(Result.success(fetchUser()))
} catch(ioException: Exception) {
emit(Result.error(ioException))
}
}
Bạn cũng có thể kết hợp liveData
với Transformations
, như trong ví dụ sau:
class MyViewModel: ViewModel() {
private val userId: LiveData<String> = MutableLiveData()
val user = userId.switchMap { id ->
liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
emit(database.loadUserById(id))
}
}
}
Bạn có thể xuất nhiều giá trị từ LiveData
ra bằng cách gọi hàm emitSource()
bất cứ khi nào bạn muốn xuất một giá trị mới. Xin lưu ý rằng mỗi lệnh gọi đến emit()
hoặc emitSource()
sẽ xoá nguồn đã thêm trước đó.
class UserDao: Dao {
@Query("SELECT * FROM User WHERE id = :id")
fun getUser(id: String): LiveData<User>
}
class MyRepository {
fun getUser(id: String) = liveData<User> {
val disposable = emitSource(
userDao.getUser(id).map {
Result.loading(it)
}
)
try {
val user = webservice.fetchUser(id)
// Stop the previous emission to avoid dispatching the updated user
// as `loading`.
disposable.dispose()
// Update the database.
userDao.insert(user)
// Re-establish the emission with success type.
emitSource(
userDao.getUser(id).map {
Result.success(it)
}
)
} catch(exception: IOException) {
// Any call to `emit` disposes the previous one automatically so we don't
// need to dispose it here as we didn't get an updated value.
emitSource(
userDao.getUser(id).map {
Result.error(exception, it)
}
)
}
}
}
Để biết thêm thông tin liên quan đến coroutine, hãy xem các đường liên kết sau:
- Cải thiện hiệu suất của ứng dụng bằng cách sử dụng coroutine của Kotlin
- Tổng quan về coroutine
- Tạo luồng thực thi trong CoroutineWorker
Tài nguyên khác
Để tìm hiểu thêm về cách sử dụng coroutine bằng các thành phần nhận biết vòng đời, hãy tham khảo thêm các tài nguyên như bên dưới.
Mẫu
Blog
- Coroutines trên Android: Mẫu ứng dụng
- Dễ dàng sử dụng coroutine trên Android: viewModelScope
- Kiểm thử hai lượt tạo LiveData liên tiếp trong coroutine
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Tổng quan về LiveData
- Xử lý vòng đời bằng các thành phần nhận biết vòng đời
- Tải và hiện dữ liệu được phân trang