Coroutine của Kotlin trên Android

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 các khái niệm sẵn có trong những 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 cơ chế xử lý đồ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ỷ: Thao tác huỷ được tự động truyền đi thông qua hệ thống 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 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 trong nhiều giải pháp có 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ột CoroutineScope được xác định trước, đi kèm với các tiện ích ViewModel của KTX. Lưu ý rằng tất cả các coroutine phải chạy trong một phạm vi. Một CoroutineScope 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ầng View 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ỷ.

Một vấn đề với ví dụ trước 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ền Dispatcher đến launch, 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ừ tầng View trên luồng chính.
  • launch tạo một coroutine mới để tạo yêu cầu mạng 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() tạm ngưng việc thực thi coroutine cho đến khi khối withContext trong makeLoginRequest() chạy xong.
  • Sau khi khối withContext kết thúc, coroutine trong login() 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à tầng Repository có thể gửi, hãy sử 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 makeLoginRequest(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: