-2

Coroutine trong Android

Coroutines đã được giới thiệu rất nhiều trong Kotlin 1.1. Nó thực sự mạnh mẽ và cộng đồng vẫn đang tìm hiểu làm thế nào để tận dụng tối đa nó.

Để hiểu đơn giản, coroutines là một cách để viết code không đồng bộ một cách tuần tự. Thay vì tạo ra một mớ lộn xộn với callback, bạn có thể viết các code một lần sau tất cả. Một số trong số chúng sẽ có khả năng đình chỉ việc thực hiện và đợi cho đến khi kết quả sẵn sàng.

Nếu như bạn đã từng code C# thì async/await là một định nghĩa gần gũi với bạn, nhưng coroutines trong Kotlin thì mạnh mẽ hơn, thay vì là một triển khai cụ thể của ý tưởng, chúng là một tính năng ngôn ngữ có thể được thực hiện theo những cách khác nhau để giải quyết các vấn đề khác nhau.

Bạn có thể viết implementation của riêng mình, hoặc sử dụng một trong những lựa chọn mà nhóm Kotlin và các nhà phát triển độc lập khác đã xây dựng.

Bạn cần phải hiểu rằng coroutines là một tính năng thử nghiệm trong Kotlin 1.1. Điều này có nghĩa là việc triển khai có thể thay đổi trong tương lai và mặc dù bản cũ vẫn sẽ được hỗ trợ, bạn có thể muốn chuyển sang định nghĩa mới. Như chúng ta sẽ thấy sau, bạn cần opt cho tính năng này, nếu không bạn sẽ thấy cảnh báo khi bạn sử dụng nó.

1. Hiểu cách coroutine hoạt động

Coroutines dựa trên ý tưởng của suspending functions: chức năng có thể dừng thực hiện khi chúng được gọi và làm cho nó tiếp tục một khi nó đã hoàn thành chạy nhiệm vụ của mình.

Suspending functions được đánh dấu bằng từ 'suspend', và chỉ có thể được gọi bên trong các suspending function khác hoặc bên trong một coroutine.

Có nghĩa là bạn không thể gọi suspending function ở mọi nơi. Cần có một chức năng xung quanh để xây dựng coroutine và cung cấp bối cảnh cần thiết cho việc này. Một cái gì đó như thế này:

fun <T> async(block: suspend () -> T)

Sau đó, bạn có thể thực hiện một chức năng đình chỉ và gọi nó bên trong khối đó:

suspend fun mySuspendingFun(x: Int) : Result {
 …
}
async { 
 val res = mySuspendingFun(20)
 print(res)
}

Coroutines là các thread ư? Không chính xác. Chúng hoạt động theo cách tương tự, nhưng nhẹ và hiệu quả hơn nhiều. Bạn có thể có hàng triệu coroutines chạy trên một vài thread, mở ra một thế giới của các khả năng.

Có 3 cách để sử dụng các coroutine feature:

  • Raw implementation: nó có nghĩa là xây dựng theo cách riêng của bạn để sử dụng coroutines. Điều này khá phức tạp và thường không bắt buộc.
  • Low-level implementations: Kotlin cung cấp một bộ thư viện mà bạn có thể tìm thấy trong kho kotlinx.coroutines, giải quyết một số phần khó nhất và cung cấp một sự thực hiện cụ thể cho các tình huống khác nhau. Có một ví dụ cho Android.
  • Higher-level implementations: Nếu bạn chỉ muốn có một giải pháp cung cấp tất cả mọi thứ bạn cần để bắt đầu sử dụng coroutines ngay lập tức, có một số thư viện ngoài đã làm tất cả những công việc khó khăn cho bạn. Ở đây chúng ta sẽ nói tới Anko, như là 1 ví dụ.

2. Sử dụng Anko cho coroutines

Từ bản 0.10, Anko đã cung cấp một số cách để sử dụng coroutine trong Android.

Cách đầu tiên tương tự như những gì chúng ta đã thấy trong ví dụ trên, và cũng tương tự như những gì các thư viện khác làm.

Đầu tiên, bạn cần tạo một async block, nơi suspension function có thể được gọi:

async(UI) {
 …
}

Tham số UI là execution context cho async block.

Sau đó, bạn có thể tạo các block mà chạy trong background thread và trả về kết quả cho UI thread. Các blocks đó đã được định nghĩa sử dụng hàm bg:

async(UI) {
 val r1: Deferred<Result> = bg { fetchResult1() }
 val r2: Deferred<Result> = bg { fetchResult2() }
 updateUI(r1.await(), r2.await())
}

Bg trả về một đối tượngDeferred, sẽ tạm ngừng coroutine khi hàm await () được gọi, cho đến khi kết quả được trả về. Chúng ta sẽ sử dụng giải pháp này trong ví dụ dưới đây.

Như bạn đã biết, như Kotlin compiler có thể suy ra kiểu của các biến, điều này có thể là đơn giản:

async(UI) {
 val r1 = bg { fetchResult1() }
 val r2 = bg { fetchResult2() }
 updateUI(r1.await(), r2.await())
}

Cách thứ hai là sử dụng sự tích hợp với các listener được cung cấp trên các thư viện con cụ thể, tùy thuộc vào listener nào bạn sẽ sử dụng.

Ví dụ, trên anko-sdk15-coroutines, có một listener onClick mà lambda thực sự là một coroutine. Vì vậy, bạn có thể bắt đầu sử dụng suspending functions ngay bên trong listener block:

textView.onClick {
 val r1 = bg { fetchResult1() }
 val r2 = bg { fetchResult2() }
 updateUI(r1.await(), r2.await())
}

Như bạn thấy, kết quả rất giống với kết quả trước đó. Bạn còn tiết kiệm được code.

Để sử dụng nó, bạn sẽ cần thêm một số các dependencies này, tùy thuộc vào listener bạn muốn sử dụng:

compile “org.jetbrains.anko:anko-sdk15-coroutines:$anko_version”
compile “org.jetbrains.anko:anko-appcompat-v7-coroutines:$anko_version”
compile “org.jetbrains.anko:anko-design-coroutines:$anko_version”

3. Sử dụng coroutines trong một ví dụ

Đầu tiên, để sử dụng Anko coroutines, chúng ta cần include dependency mới:

compile “org.jetbrains.anko:anko-coroutines:$anko_version”

Sau đó, chúng ta cần opt, nếu không, sẽ bị show warning. Đơn giản, chỉ cần thêm dòng sau vào file gradle.properties trong thư mục gốc:

kotlin.coroutines=enable

Và bây giờ, chúng ta đã sẵn sàng để bắt đầu sử dụng coroutines. Chúng ta hãy đi đầu tiên vào activity chi tiết. Nó chỉ cần gọi cơ sở dữ liệu (đó là bộ nhớ đệm dự báo hàng tuần) bằng cách sử dụng một lệnh cụ thể.

Đây là code:

async(UI) {
    val id = intent.getLongExtra(ID, -1)
    val result = bg { RequestDayForecastCommand(id)
        .execute() }
    bindForecast(result.await())
}

Dự báo được yêu cầu trong một backgroung thread nhờ hàm bg, sẽ trả về kết quả deferred. Kết quả đó được chờ đợi trong cuộc gọi bindForecast, cho đến khi nó sẵn sàng để được trả lại.

Nhưng không phải mọi thứ đều tuyệt vời. Điều gì đang xảy ra ở đây? Coroutines có một vấn đề: nos đang giữ một tham chiếu đến DetailActivity, leaking nếu yêu cầu không bao giờ kết thúc.

Đừng lo lắng, bởi vì Anko có một giải pháp. Bạn có thể tạo một weak reference đến activity của mình, và sử dụng nó để thay thế:

val ref = asReference()
val id = intent.getLongExtra(ID, -1)
async(UI) {
 val result = bg { RequestDayForecastCommand(id).execute() }
 ref().bindForecast(result.await())
}

Reference này sẽ cho phép gọi các activity khi nó có sẵn, và sẽ hủy bỏ coroutine trong trường hợp activity đã bị kill. Hãy cẩn thận để đảm bảo rằng tất cả các cuộc gọi đến các activity method hoặc properties đã được thực hiện thông qua đối tượng ref này.

Nhưng điều này có thể có một chút phức tạp nếu coroutine tương tác nhiều lần với activity. Trong MainActivity, ví dụ, việc sử dụng giải pháp này sẽ trở nên phức tạp hơn một chút.

Activity này sẽ gọi một điểm cuối để yêu cầu một dự báo tuần dựa trên một zipCode:

private fun loadForecast() {
val ref = asReference()
 val localZipCode = zipCode
async(UI) {
 val result = bg { RequestForecastCommand(localZipCode).execute() }
 val weekForecast = result.await()
 ref().updateUI(weekForecast)
 }
}

Bạn không thể sử dụng ref () bên trong khối bg, bởi vì code bên trong khối đó không phải là một suspension context, vì vậy bạn cần phải lưuzipCode vào một biến địa phương khác.

Những thay đổi đối với MainActivity sẽ đơn giản hơn:

private fun loadForecast() = async(UI) {
 val result = bg { RequestForecastCommand(zipCode).execute() }
 updateUI(result.await())
}

Và đó là tất cả, hiện giờ chúng ta đã có một asynchronous code viết theo một cách dễ dàng hơn. Mong rằng nó sẽ hữu ích cho các bạn.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí