Dùng coroutine của Kotlin với các thành phần nhận biết vòng đời

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ụng androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 trở lên.
  • Đối với LifecycleScope, hãy sử dụng androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 trở lên.
  • Đối với liveData, hãy sử dụng androidx.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, LifecycleLifecycleOwner 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.whenStartedlifecycle.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 LifecycleDESTROYED:

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:

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