Dùng Coroutine của Kotlin trong ứng dụng Android

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách sử dụng Kotlin Coroutines trong ứng dụng Android – một cách mới để quản lý các luồng nền có thể đơn giản hóa mã bằng cách giảm nhu cầu gọi lại. Coroutine là một tính năng Kotlin giúp chuyển đổi các lệnh gọi lại không đồng bộ cho những tác vụ chạy trong thời gian dài, chẳng hạn như cơ sở dữ liệu hoặc quyền truy cập mạng, thành mã tuần tự.

Dưới đây là một đoạn mã giúp bạn hiểu về những việc mình sẽ làm.

// Async callbacks
networkRequest { result ->
   // Successful network request
   databaseSave(result) { rows ->
     // Result saved
   }
}

Mã dựa trên lệnh gọi lại sẽ được chuyển đổi thành mã tuần tự bằng coroutine.

// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved

Bạn sẽ bắt đầu với một ứng dụng hiện có, được tạo bằng Các thành phần cấu trúc, sử dụng kiểu gọi lại cho các tác vụ chạy trong thời gian dài.

Khi kết thúc lớp học lập trình này, bạn sẽ có đủ kinh nghiệm để sử dụng coroutine trong ứng dụng của mình để tải dữ liệu từ mạng và bạn có thể tích hợp coroutine vào một ứng dụng. Bạn cũng sẽ làm quen với các phương pháp hay nhất cho coroutine và cách viết thử nghiệm dựa trên mã sử dụng coroutine.

Điều kiện tiên quyết

  • Đã quen thuộc với các thành phần cấu trúc ViewModel, LiveData, RepositoryRoom.
  • Trải nghiệm về cú pháp Kotlin, bao gồm cả hàm mở rộng và hàm lambda.
  • Hiểu biết cơ bản về cách sử dụng chuỗi trên Android, bao gồm chuỗi chính, chuỗi trong nền và lệnh gọi lại.

Bạn sẽ thực hiện

  • Mã cuộc gọi được viết bằng coroutine và thu được kết quả.
  • Dùng các hàm tạm ngưng để tạo mã không đồng bộ theo trình tự.
  • Sử dụng launchrunBlocking để kiểm soát cách mã thực thi.
  • Tìm hiểu các kỹ thuật chuyển đổi các API hiện có thành coroutine bằng cách sử dụng suspendCoroutine.
  • Dùng coroutine với Thành phần cấu trúc.
  • Tìm hiểu các phương pháp hay nhất để kiểm tra coroutine.

Bạn cần có

  • Android Studio 3.5 (lớp học lập trình có thể hoạt động với các phiên bản khác, nhưng một số tính năng có thể bị thiếu hoặc trông khác).

Nếu bạn gặp phải bất kỳ vấn đề nào (lỗi mã, lỗi ngữ pháp, từ ngữ không rõ ràng, v.v.) khi bạn làm việc qua lớp học lập trình này, vui lòng báo cáo vấn đề qua liên kết Báo cáo lỗi ở góc dưới bên trái của lớp học lập trình.

Tải mã xuống

Nhấp vào đường liên kết sau đây để tải toàn bộ mã nguồn cho lớp học lập trình này:

Tải tệp zip xuống

... hoặc sao chép kho lưu trữ GitHub từ dòng lệnh bằng cách dùng lệnh sau:

$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git

Câu hỏi thường gặp

Trước tiên, hãy xem ứng dụng mẫu ban đầu trông như thế nào. Làm theo các hướng dẫn sau để mở ứng dụng mẫu trong Android Studio.

  1. Nếu bạn đã tải tệp zip kotlin-coroutines xuống, hãy giải nén tệp.
  2. Mở dự án coroutines-codelab trong Android Studio.
  3. Chọn mô-đun ứng dụng start.
  4. Nhấp vào nút tức thì.pngChạy rồi chọn một trình mô phỏng hoặc kết nối thiết bị Android của bạn. Thiết bị này phải có khả năng chạy Android Lollipop (SDK tối thiểu được hỗ trợ là 21). Màn hình Kotlin Coroutines sẽ xuất hiện:

Ứng dụng dành cho người mới bắt đầu này sử dụng chuỗi để tăng số lần đếm ngắn sau khi bạn nhấn vào màn hình. Hệ thống cũng sẽ tìm nạp tiêu đề mới từ mạng và hiển thị tiêu đề đó trên màn hình. Hãy thử ngay bây giờ và bạn sẽ thấy số lượng và thông báo thay đổi sau một chút chậm trễ. Trong lớp học lập trình này, bạn sẽ chuyển đổi ứng dụng này để sử dụng coroutine.

Ứng dụng này sử dụng Thành phần cấu trúc để phân tách mã giao diện người dùng trong MainActivity khỏi logic ứng dụng trong MainViewModel. Hãy dành chút thời gian để làm quen với cấu trúc của dự án.

  1. MainActivity hiển thị giao diện người dùng, đăng ký trình xử lý lượt nhấp và có thể hiển thị Snackbar. Ứng dụng này chuyển các sự kiện đến MainViewModel và cập nhật màn hình dựa trên LiveData trong MainViewModel.
  2. MainViewModel xử lý các sự kiện trong onMainViewClicked và sẽ kết nối với MainActivity bằng LiveData.
  3. Executors xác định BACKGROUND, có thể chạy mọi thứ trên luồng trong nền.
  4. TitleRepository tìm nạp kết quả từ mạng và lưu kết quả đó vào cơ sở dữ liệu.

Thêm coroutine vào dự án

Để sử dụng coroutine trong Kotlin, bạn phải đưa thư viện coroutines-core vào tệp build.gradle (Module: app) của dự án. Các dự án lớp học lập trình đã thực hiện việc này cho bạn, vì vậy, bạn không cần phải làm việc này để hoàn thành lớp học lập trình.

Coroutine trên Android có sẵn dưới dạng thư viện lõi và tiện ích dành riêng cho Android:

  • kotlinx-corountines-core — Giao diện chính để sử dụng coroutine trong Kotlin
  • kotlinx-coroutines-android — Hỗ trợ luồng chính trên Android trong coroutine

Ứng dụng dành cho người mới bắt đầu đã bao gồm các phần phụ thuộc trong build.gradle.Khi tạo dự án ứng dụng mới, bạn cần mở build.gradle (Module: app) và thêm các phần phụ thuộc coroutine vào dự án.

dependencies {
  ...
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}

Trên Android, điều cần thiết để tránh chặn chuỗi chính. Chuỗi chính là một chuỗi duy nhất xử lý tất cả các nội dung cập nhật cho giao diện người dùng. Đây cũng là chuỗi gọi tất cả các trình xử lý lượt nhấp và các lệnh gọi lại giao diện người dùng khác. Do đó, sản phẩm phải chạy suôn sẻ để đảm bảo trải nghiệm người dùng tuyệt vời.

Để ứng dụng của bạn hiển thị cho người dùng mà không có thời gian tạm dừng hiển thị nào, chuỗi chính phải cập nhật màn hình mỗi 16 mili giây trở lên, tức là khoảng 60 khung hình/giây. Nhiều tác vụ phổ biến mất nhiều thời gian hơn thế, chẳng hạn như phân tích cú pháp các tập dữ liệu JSON lớn, ghi dữ liệu vào cơ sở dữ liệu hoặc tìm nạp dữ liệu từ mạng. Do đó, việc gọi mã như thế này từ chuỗi chính có thể khiến ứng dụng bị tạm dừng, bị gián đoạn hoặc thậm chí bị treo. Và nếu bạn chặn chuỗi chính quá lâu, ứng dụng thậm chí có thể gặp sự cố và hiển thị hộp thoại Ứng dụng không phản hồi.

Hãy xem video dưới đây để biết thông tin giới thiệu về cách coroutine giải quyết vấn đề này cho chúng tôi trên Android bằng cách ra mắt tính năng an toàn chính.

Mẫu lệnh gọi lại

Một mẫu để thực hiện các tác vụ chạy trong thời gian dài mà không chặn chuỗi chính là lệnh gọi lại. Bằng cách sử dụng các lệnh gọi lại, bạn có thể bắt đầu các thao tác dài hạn trên một luồng ở chế độ nền. Khi việc cần làm hoàn thành, lệnh gọi lại sẽ được gọi để thông báo cho bạn kết quả trên chuỗi chính.

Hãy xem ví dụ về mẫu gọi lại.

// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
    // The slow network request runs on another thread
    slowFetch { result ->
        // When the result is ready, this callback will get the result
        show(result)
    }
    // makeNetworkRequest() exits after calling slowFetch without waiting for the result
}

Vì mã này được chú thích bằng @UiThread, nên mã phải chạy đủ nhanh để thực thi trên luồng chính. Điều đó có nghĩa là biến thể này cần phải trả về rất nhanh chóng để lần cập nhật màn hình tiếp theo không bị trễ. Tuy nhiên, vì slowFetch sẽ mất vài giây hoặc thậm chí vài phút để hoàn tất, nên chuỗi chính không thể đợi kết quả. Lệnh gọi lại show(result) cho phép slowFetch chạy trên một luồng trong nền và trả về kết quả khi đã sẵn sàng.

Sử dụng coroutine để xóa lệnh gọi lại

Lệnh gọi lại là một mẫu hay, nhưng có một số hạn chế. Mã sử dụng nhiều lệnh gọi lại có thể trở nên khó đọc và khó lý giải hơn. Ngoài ra, các lệnh gọi lại không cho phép sử dụng một số tính năng về ngôn ngữ, chẳng hạn như ngoại lệ.

Coroutine Kotlin cho phép bạn chuyển đổi mã dựa trên lệnh gọi lại thành mã tuần tự. Mã được viết tuần tự thường dễ đọc hơn và thậm chí có thể dùng các tính năng bằng ngôn ngữ như ngoại lệ.

Cuối cùng, họ thực hiện đúng thao tác tương tự: đợi cho đến khi có kết quả từ một tác vụ chạy trong thời gian dài và tiếp tục thực thi. Tuy nhiên, trong mã, chúng trông rất khác.

Từ khóa suspend là cách Kotlin đánh dấu một hàm hoặc loại hàm có sẵn cho coroutine. Khi coroutine gọi một hàm được đánh dấu là suspend, thay vì chặn cho đến khi hàm đó trả về như một lệnh gọi hàm thông thường, hệ thống sẽ tạm ngưng việc thực thi cho đến khi kết quả sẵn sàng rồi tiếp tục tiếp tục từ nơi dừng lại kèm theo kết quả. Trong khi tạm ngưng để chờ kết quả, hệ thống sẽ bỏ chặn luồng mà coroutine đó đang chạy để các hàm hoặc coroutine khác có thể chạy.

Ví dụ: trong mã bên dưới, makeNetworkRequest()slowFetch() đều là các hàm suspend.

// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
    // slowFetch is another suspend function so instead of 
    // blocking the main thread  makeNetworkRequest will `suspend` until the result is 
    // ready
    val result = slowFetch()
    // continue to execute after the result is ready
    show(result)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }

Cũng giống như phiên bản gọi lại, makeNetworkRequest phải quay lại từ chuỗi chính ngay lập tức vì nó được đánh dấu là @UiThread. Điều này có nghĩa là thông thường, phương thức này không thể gọi các phương thức chặn như slowFetch. Đây là nơi từ khóa suspend mang lại hiệu quả.

So với mã dựa trên lệnh gọi lại, mã coroutine đạt được cùng kết quả khi bỏ chặn luồng hiện tại với ít mã hơn. Do kiểu tuần tự, bạn có thể dễ dàng chuỗi một số tác vụ chạy trong thời gian dài mà không tạo nhiều lệnh gọi lại. Ví dụ: mã tìm nạp kết quả từ hai điểm cuối mạng và lưu vào cơ sở dữ liệu có thể được viết dưới dạng hàm trong coroutine mà không có lệnh gọi lại. Chẳng hạn như sau:

// Request data from network and save it to database with coroutines

// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
    // slowFetch and anotherFetch are suspend functions
    val slow = slowFetch()
    val another = anotherFetch()
    // save is a regular function and will block this thread
    database.save(slow, another)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }

Bạn sẽ thêm coroutine vào ứng dụng mẫu trong phần tiếp theo.

Trong bài tập này, bạn sẽ viết coroutine để hiển thị thông báo sau độ trễ. Để bắt đầu, hãy đảm bảo bạn đã mở học phần start trong Android Studio.

Tìm hiểu về CoroutineScope

Trong Kotlin, tất cả coroutine đều chạy trong CoroutineScope. Phạm vi kiểm soát thời gian hoạt động của coroutine thông qua công việc. Khi huỷ công việc trong một phạm vi, thao tác đó sẽ huỷ tất cả các coroutine bắt đầu trong phạm vi đó. Trên Android, bạn có thể dùng phạm vi để hủy tất cả coroutine đang chạy, ví dụ: khi người dùng rời khỏi Activity hoặc Fragment. Phạm vi cũng cho phép bạn chỉ định người điều phối mặc định. Người điều phối kiểm soát luồng nào chạy coroutine.

Đối với coroutine do giao diện người dùng bắt đầu, thông thường, bạn nên bắt đầu coroutine trên Dispatchers.Main – đây là luồng chính trên Android. Coroutine đã bắt đầu vào Dispatchers.Main sẽ không chặn luồng chính trong khi bị tạm ngưng. Vì coroutine của ViewModel hầu như luôn cập nhật giao diện người dùng trên luồng chính, nên việc bắt đầu coroutine trên luồng chính sẽ giúp bạn có được các công tắc khác trên luồng. Coroutine đã bắt đầu trên luồng Chính có thể chuyển người điều phối bất cứ lúc nào sau khi quá trình này bắt đầu. Ví dụ: công cụ có thể sử dụng một người điều phối khác để phân tích cú pháp một kết quả JSON lớn ngoài luồng chính.

Sử dụng viewModelScope

Thư viện lifecycle-viewmodel-ktx trên AndroidX có thêm CoroutineScope vào các ViewModel đã định cấu hình để bắt đầu coroutine liên quan đến giao diện người dùng. Để sử dụng thư viện này, bạn phải đưa thư viện đó vào tệp build.gradle (Module: start) của dự án. Bước đó đã được thực hiện trong các dự án lớp học lập trình.

dependencies {
  ...
  implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x"
}

Thư viện này sẽ thêm viewModelScope làm hàm mở rộng của lớp ViewModel. Phạm vi này liên kết với Dispatchers.Main và sẽ tự động bị hủy khi ViewModel được xóa.

Chuyển từ luồng sang coroutine

Trong MainViewModel.kt, hãy tìm VIỆC CẦN LÀM tiếp theo cùng với mã này:

MainViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   BACKGROUND.submit {
       Thread.sleep(1_000)
       _taps.postValue("$tapCount taps")
   }
}

Mã này sử dụng BACKGROUND ExecutorService (được xác định trong util/Executor.kt) để chạy trong một luồng trong nền. Vì sleep chặn chuỗi hiện tại, nên giao diện người dùng sẽ đóng băng nếu được gọi trên chuỗi chính. Một giây sau khi người dùng nhấp vào chế độ xem chính, hệ thống sẽ yêu cầu một thanh thông báo nhanh.

Bạn có thể thấy điều đó bằng cách xóa BACKGROUND khỏi mã và chạy lại mã này. Vòng quay tải sẽ không hiển thị và mọi thứ sẽ "jump" đến trạng thái cuối cùng một giây sau đó.

MainViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   Thread.sleep(1_000)
   _taps.postValue("$tapCount taps")
}

Thay thế updateTaps bằng mã dựa trên coroutine này có chức năng tương tự. Bạn sẽ phải nhập launchdelay.

MainViewModel.kt

/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
   // launch a coroutine in viewModelScope
   viewModelScope.launch {
       tapCount++
       // suspend this coroutine for one second
       delay(1_000)
       // resume in the main dispatcher
       // _snackbar.value can be called directly from main thread
       _taps.postValue("$tapCount taps")
   }
}

Mã này cũng làm như vậy, đợi một giây trước khi hiển thị thanh thông báo nhanh. Tuy nhiên, có một số điểm khác biệt quan trọng:

  1. viewModelScope.launch sẽ bắt đầu một coroutine trong viewModelScope. Điều này có nghĩa là khi công việc mà chúng ta chuyển đến viewModelScope bị hủy, tất cả coroutine trong công việc/phạm vi này sẽ bị hủy. Nếu người dùng rời khỏi Hoạt động trước khi delay quay lại, coroutine này sẽ tự động bị hủy khi onCleared được gọi khi hủy ViewModel.
  2. viewModelScope có trình điều phối mặc định là Dispatchers.Main, nên coroutine này sẽ được chạy trong luồng chính. Chúng ta sẽ xem cách sử dụng luồng khác nhau ở phần sau.
  3. Hàm delay là một hàm suspend. Biểu tượng này hiển thị trong Android Studio bằng biểu tượng ở rãnh bên trái. Mặc dù coroutine này chạy trên luồng chính, nhưng delay sẽ không chặn luồng trong một giây. Thay vào đó, người điều phối sẽ lên lịch để coroutine tiếp tục sau một giây ở câu lệnh tiếp theo.

Hãy tiếp tục và chạy ứng dụng. Khi nhấp vào chế độ xem chính, bạn sẽ thấy thanh thông báo nhanh sau đó một giây.

Trong phần tiếp theo, chúng tôi sẽ xem xét cách kiểm tra chức năng này.

Trong bài tập này, bạn sẽ viết thử nghiệm cho mã mà bạn vừa viết. Bài tập này hướng dẫn bạn cách kiểm tra coroutine chạy trên Dispatchers.Main bằng thư viện kotlinx-coroutines-test. Sau đó, trong lớp học lập trình này, bạn sẽ triển khai thử nghiệm tương tác trực tiếp với coroutine.

Xem lại mã hiện có

Mở MainViewModelTest.kt trong thư mục androidTest.

MainViewModelTest.kt

class MainViewModelTest {
   @get:Rule
   val coroutineScope =  MainCoroutineScopeRule()
   @get:Rule
   val instantTaskExecutorRule = InstantTaskExecutorRule()

   lateinit var subject: MainViewModel

   @Before
   fun setup() {
       subject = MainViewModel(
           TitleRepository(
                   MainNetworkFake("OK"),
                   TitleDaoFake("initial")
           ))
   }
}

Quy tắc là một cách để chạy mã trước và sau khi thực thi thử nghiệm trong JUnit. Hai quy tắc dùng để cho phép chúng tôi kiểm tra MainViewModel trong một thử nghiệm ngoài thiết bị:

  1. InstantTaskExecutorRule là quy tắc JUnit định cấu hình LiveData để thực thi từng tác vụ một cách đồng bộ
  2. MainCoroutineScopeRule là quy tắc tùy chỉnh trong cơ sở mã này định cấu hình Dispatchers.Main để sử dụng TestCoroutineDispatcher từ kotlinx-coroutines-test. Nhờ đó, các thử nghiệm sẽ chuyển tiếp một đồng hồ ảo cho thử nghiệm và cho phép mã dùng Dispatchers.Main trong các thử nghiệm đơn vị.

Trong phương thức setup, một thực thể mới của MainViewModel được tạo bằng cách sử dụng giả mạo thử nghiệm – đây là những triển khai giả của mạng và cơ sở dữ liệu được cung cấp trong mã khởi động để giúp viết các thử nghiệm mà không cần sử dụng mạng hoặc cơ sở dữ liệu thực.

Đối với thử nghiệm này, tính năng giả mạo chỉ cần thiết để đáp ứng các phần phụ thuộc của MainViewModel. Sau đó, trong phòng thí nghiệm mã này, bạn sẽ cập nhật hàng giả để hỗ trợ coroutine.

Viết một thử nghiệm để kiểm soát coroutine

Thêm một thử nghiệm mới để đảm bảo rằng các lần nhấn được cập nhật một giây sau khi chế độ xem chính được nhấp vào:

MainViewModelTest.kt

@Test
fun whenMainClicked_updatesTaps() {
   subject.onMainViewClicked()
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
   coroutineScope.advanceTimeBy(1000)
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}

Bằng cách gọi onMainViewClicked, coroutine chúng ta vừa tạo sẽ được khởi chạy. Thử nghiệm này sẽ kiểm tra để đảm bảo rằng văn bản nhấn vẫn "0 lần nhấn" ngay sau khi onMainViewClicked được gọi, sau đó 1 giây sau đó sẽ được cập nhật thành "1 lần nhấn".

Thử nghiệm này sử dụng thời gian ảo để kiểm soát việc thực thi coroutine do onMainViewClicked khởi chạy. MainCoroutineScopeRule cho phép bạn tạm dừng, tiếp tục hoặc kiểm soát việc thực thi các coroutine trên Dispatchers.Main. Ở đây chúng ta đang gọi advanceTimeBy(1_000). Điều này sẽ khiến người điều phối chính thực thi ngay lập tức các coroutine được lên lịch tiếp tục 1 giây sau đó.

Thử nghiệm này có tính xác định hoàn toàn, nghĩa là thử nghiệm này sẽ luôn thực hiện theo cách tương tự. Ngoài ra, do có toàn quyền kiểm soát việc thực thi các coroutine trên Dispatchers.Main, nên bạn không cần phải đợi 1 giây để đặt giá trị.

Chạy thử nghiệm hiện tại

  1. Nhấp chuột phải vào tên lớp học MainViewModelTest trong trình chỉnh sửa để mở trình đơn theo bối cảnh.
  2. Trong trình đơn theo bối cảnh, hãy chọn tức thì.pngChạy \39;MainViewModelTest#39;
  3. Đối với các lần chạy sau, bạn có thể chọn cấu hình thử nghiệm này trong cấu hình bên cạnh nút tức thì.png trên thanh công cụ. Theo mặc định, cấu hình sẽ được gọi là MainViewModelTest.

Bạn sẽ thấy thẻ kiểm tra! Và sẽ mất chưa đến một giây để chạy.

Trong bài tập tiếp theo, bạn sẽ tìm hiểu cách chuyển đổi từ các API gọi lại hiện có để sử dụng coroutine.

Ở bước này, bạn sẽ bắt đầu chuyển đổi kho lưu trữ để sử dụng coroutine. Để làm điều này, chúng ta sẽ thêm coroutine vào ViewModel, Repository, RoomRetrofit.

Bạn nên hiểu từng phần của cấu trúc chịu trách nhiệm trước khi chúng ta chuyển sang sử dụng coroutine.

  1. MainDatabasetriển khai cơ sở dữ liệu bằng Phòng lưu và tải Title.
  2. MainNetwork triển khai API mạng tìm nạp tiêu đề mới. Ứng dụng này sử dụng Retrofit để tìm nạp tiêu đề. Retrofit được định cấu hình để trả về ngẫu nhiên lỗi hoặc mô phỏng dữ liệu, nhưng hoạt động như thể đang thực hiện yêu cầu kết nối mạng thực.
  3. TitleRepository triển khai một API duy nhất để tìm nạp hoặc làm mới tiêu đề bằng cách kết hợp dữ liệu từ mạng và cơ sở dữ liệu.
  4. MainViewModel đại diện cho trạng thái của màn hình và xử lý các sự kiện. Thao tác này sẽ yêu cầu kho lưu trữ làm mới tiêu đề khi người dùng nhấn vào màn hình.

Vì yêu cầu mạng được thúc đẩy bởi các sự kiện trên giao diện người dùng và chúng ta muốn bắt đầu một coroutine dựa trên những sự kiện đó, nên vị trí tự nhiên để bắt đầu sử dụng coroutine nằm trong ViewModel.

Phiên bản gọi lại

Hãy mở MainViewModel.kt để xem khai báo của refreshTitle.

MainViewModel.kt

/**
* Update title text via this LiveData
*/
val title = repository.title


// ... other code ...


/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   // TODO: Convert refreshTitle to use coroutines
   _spinner.value = true
   repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
       override fun onCompleted() {
           _spinner.postValue(false)
       }

       override fun onError(cause: Throwable) {
           _snackBar.postValue(cause.message)
           _spinner.postValue(false)
       }
   })
}

Hàm này được gọi mỗi khi người dùng nhấp vào màn hình – và sẽ khiến kho lưu trữ làm mới tiêu đề và ghi tiêu đề mới vào cơ sở dữ liệu.

Cách triển khai này sử dụng lệnh gọi lại để thực hiện một số hành động sau:

  • Trước khi bắt đầu truy vấn, kết quả tìm kiếm sẽ hiển thị một vòng xoay đang tải có _spinner.value = true
  • Khi nhận được kết quả, công cụ này sẽ xóa vòng quay tải bằng _spinner.value = false
  • Nếu bị lỗi, thanh này sẽ yêu cầu thanh thông báo nhanh hiển thị và xóa vòng xoay

Xin lưu ý rằng lệnh gọi lại onCompleted không được chuyển qua title. Vì chúng tôi ghi tất cả tiêu đề vào cơ sở dữ liệu của Room, giao diện người dùng sẽ cập nhật tiêu đề hiện tại bằng cách quan sát LiveData được cập nhật bởi Room.

Trong lần cập nhật coroutine, chúng ta sẽ giữ nguyên hành vi chính xác. Bạn nên sử dụng một nguồn dữ liệu có thể quan sát như cơ sở dữ liệu Room để tự động cập nhật giao diện người dùng.

Phiên bản coroutine

Hãy cùng viết lại refreshTitle bằng coroutine!

Vì chúng ta cần nó ngay lập tức, hãy để hàm tạm ngưng hoạt động trong kho lưu trữ của chúng ta (TitleRespository.kt). Xác định một hàm mới sử dụng toán tử suspend để cho Kotlin biết rằng nó hoạt động với coroutine.

TitleRepository.kt

suspend fun refreshTitle() {
    // TODO: Refresh from network and write to database
    delay(500)
}

Khi hoàn tất lớp học lập trình này, bạn sẽ cập nhật lớp này để sử dụng Retrofit và Phòng để tìm nạp tiêu đề mới và ghi vào cơ sở dữ liệu bằng coroutine. Còn bây giờ, họ chỉ cần 500 mili giây để làm việc rồi tiếp tục.

Trong MainViewModel, hãy thay thế phiên bản gọi lại của refreshTitle bằng phiên bản chạy coroutine mới:

MainViewModel.kt

/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           repository.refreshTitle()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Hãy thực hiện chức năng sau:

viewModelScope.launch {

Giống như coroutine để cập nhật số lần nhấn, hãy bắt đầu bằng cách chạy một coroutine mới trong viewModelScope. Thao tác này sẽ sử dụng Dispatchers.Main như bình thường. Mặc dù refreshTitle sẽ tạo yêu cầu mạng và truy vấn cơ sở dữ liệu, nhưng thực thể này có thể sử dụng coroutine để hiển thị giao diện an toàn chính. Điều này có nghĩa là bạn có thể yên tâm gọi số này từ chuỗi chính.

Vì chúng ta đang dùng viewModelScope, nên khi người dùng rời khỏi màn hình này, công việc mà coroutine này bắt đầu sẽ tự động bị huỷ. Điều đó có nghĩa là sẽ không thực hiện thêm yêu cầu mạng hoặc truy vấn cơ sở dữ liệu.

Một vài dòng mã tiếp theo thực sự gọi refreshTitle trong repository.

try {
    _spinner.value = true
    repository.refreshTitle()
}

Trước khi coroutine này thực hiện bất kỳ hành động nào, nó sẽ bắt đầu vòng xoay tải – sau đó, coroutine này gọi refreshTitle giống như một hàm thông thường. Tuy nhiên, vì refreshTitle là một hàm tạm ngưng nên hàm này thực thi khác với một hàm thông thường.

Chúng ta không phải chuyển lệnh gọi lại. Coroutine sẽ tạm ngưng cho đến khi refreshTitle tiếp tục. Mặc dù có vẻ như là lệnh gọi hàm chặn thông thường, nhưng hệ thống sẽ tự động chờ cho đến khi truy vấn mạng và cơ sở dữ liệu hoàn tất trước khi tiếp tục mà không chặn chuỗi chính.

} catch (error: TitleRefreshError) {
    _snackBar.value = error.message
} finally {
    _spinner.value = false
}

Trường hợp ngoại lệ trong hàm tạm ngưng hoạt động giống như lỗi trong các hàm thông thường. Nếu bạn gửi lỗi trong một hàm tạm ngưng, thì lỗi đó sẽ được gửi đến người gọi. Vì vậy, mặc dù những quy tắc này thực thi khá khác nhau, nhưng bạn có thể sử dụng khối chặn/thử thông thường để xử lý. Điều này hữu ích vì nó cho phép bạn dựa vào chức năng hỗ trợ ngôn ngữ tích hợp để xử lý lỗi thay vì tạo xử lý lỗi tùy chỉnh cho mỗi lệnh gọi lại.

Nếu bạn đặt một trường hợp ngoại lệ khỏi coroutine, thì theo mặc định, coroutine đó sẽ hủy hoạt động của cha mẹ. Điều đó có nghĩa là bạn có thể dễ dàng hủy nhiều nhiệm vụ có liên quan cùng nhau.

Sau đó, trong một khối cuối cùng, chúng ta có thể đảm bảo rằng vòng quay luôn tắt sau khi truy vấn chạy.

Chạy lại ứng dụng bằng cách chọn cấu hình start rồi nhấntức thì.png. Bạn sẽ thấy vòng xoay đang tải khi nhấn vào vị trí bất kỳ. Tiêu đề sẽ vẫn giữ nguyên vì chúng tôi chưa kết nối mạng hoặc cơ sở dữ liệu của mình.

Trong bài tập tiếp theo, bạn sẽ cập nhật kho lưu trữ để thực sự hoạt động.

Trong bài tập này, bạn sẽ tìm hiểu cách chuyển đổi một luồng mà coroutine chạy trên để triển khai phiên bản đang hoạt động của TitleRepository.

Xem lại mã gọi lại hiện có trong refreshTitle

Mở TitleRepository.kt và xem lại cách triển khai dựa trên lệnh gọi lại hiện tại.

TitleRepository.kt

// TitleRepository.kt

fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
   // This request will be run on a background thread by retrofit
   BACKGROUND.submit {
       try {
           // Make network request using a blocking call
           val result = network.fetchNextTitle().execute()
           if (result.isSuccessful) {
               // Save it to database
               titleDao.insertTitle(Title(result.body()!!))
               // Inform the caller the refresh is completed
               titleRefreshCallback.onCompleted()
           } else {
               // If it's not successful, inform the callback of the error
               titleRefreshCallback.onError(
                       TitleRefreshError("Unable to refresh title", null))
           }
       } catch (cause: Throwable) {
           // If anything throws an exception, inform the caller
           titleRefreshCallback.onError(
                   TitleRefreshError("Unable to refresh title", cause))
       }
   }
}

Trong TitleRepository.kt, phương thức refreshTitleWithCallbacks được triển khai với một lệnh gọi lại để thông báo trạng thái tải và trạng thái lỗi cho người gọi.

Hàm này thực hiện khá nhiều việc để triển khai việc làm mới.

  1. Chuyển sang một chuỗi khác có BACKGROUND ExecutorService
  2. Chạy yêu cầu mạng fetchNextTitle bằng phương thức chặn execute(). Thao tác này sẽ chạy yêu cầu mạng trong chuỗi hiện tại, trong trường hợp này là một trong các chuỗi trong BACKGROUND.
  3. Nếu kết quả thành công, hãy lưu kết quả vào cơ sở dữ liệu bằng insertTitle và gọi phương thức onCompleted().
  4. Nếu kết quả không thành công hoặc có ngoại lệ, hãy gọi phương thức onerror để cho người gọi biết về lần làm mới không thành công.

Hoạt động triển khai dựa trên lệnh gọi lại này là main-safe vì không chặn luồng chính. Tuy nhiên, nhân viên hỗ trợ này phải sử dụng lệnh gọi lại để thông báo cho người gọi khi hoàn thành công việc. Trình đọc này cũng gọi các lệnh gọi lại trên chuỗi BACKGROUND đã chuyển đổi.

Gọi lệnh chặn từ coroutine

Nếu không tạo coroutine vào mạng hoặc cơ sở dữ liệu, chúng ta có thể đặt mã này main-safe bằng coroutine. Điều này sẽ cho phép chúng ta loại bỏ lệnh gọi lại và cho phép chúng ta chuyển lại kết quả về chuỗi mà trước đây đã gọi.

Bạn có thể dùng mẫu này bất cứ khi nào bạn cần thực hiện công việc chặn hoặc dùng nhiều CPU từ bên trong một coroutine, chẳng hạn như sắp xếp và lọc một danh sách lớn hoặc đọc từ ổ đĩa.

Để chuyển đổi giữa mọi người điều phối, coroutine sẽ sử dụng withContext. Việc gọi withContext sẽ chuyển sang người điều phối khác chỉ dành cho lambda, sau đó quay lại người điều phối đã gọi cho người đó bằng kết quả của hàm lambda đó.

Theo mặc định, coroutine của Kotlin cung cấp 3 Trình gửi: Main, IODefault. Người điều phối IO được tối ưu hóa cho công việc IO như đọc từ mạng hoặc đĩa, trong khi người điều phối mặc định được tối ưu hóa cho các tác vụ dùng nhiều CPU.

TitleRepository.kt

suspend fun refreshTitle() {
   // interact with *blocking* network and IO calls from a coroutine
   withContext(Dispatchers.IO) {
       val result = try {
           // Make network request using a blocking call
           network.fetchNextTitle().execute()
       } catch (cause: Throwable) {
           // If the network throws an exception, inform the caller
           throw TitleRefreshError("Unable to refresh title", cause)
       }
      
       if (result.isSuccessful) {
           // Save it to database
           titleDao.insertTitle(Title(result.body()!!))
       } else {
           // If it's not successful, inform the callback of the error
           throw TitleRefreshError("Unable to refresh title", null)
       }
   }
}

Cách triển khai này sử dụng các lệnh gọi chặn cho mạng và cơ sở dữ liệu, nhưng cách này vẫn đơn giản hơn một chút so với phiên bản gọi lại.

Mã này vẫn dùng tính năng chặn cuộc gọi. Việc gọi execute()insertTitle(...) sẽ chặn cả luồng mà coroutine này đang chạy. Tuy nhiên, bằng cách chuyển sang Dispatchers.IO bằng withContext, chúng tôi sẽ chặn một trong các chuỗi cuộc trò chuyện trong tài khoản điều phối IO. Coroutine đã gọi lệnh này, có thể đang chạy vào Dispatchers.Main, sẽ bị tạm ngưng cho đến khi hàm lambda withContext hoàn tất.

So với phiên bản gọi lại, có hai điểm khác biệt quan trọng:

  1. withContext trả lại kết quả cho Trình điều phối đã gọi số đó, trong trường hợp này là Dispatchers.Main. Phiên bản gọi lại đã gọi các lệnh gọi lại trên một chuỗi trong dịch vụ trình thực thi BACKGROUND.
  2. Người gọi không phải chuyển lệnh gọi lại đến hàm này. Họ có thể dựa vào tạm ngưng và tiếp tục để nhận được kết quả hoặc lỗi.

Chạy lại ứng dụng

Nếu chạy lại ứng dụng, bạn sẽ thấy rằng quá trình triển khai dựa trên coroutine mới đang tải kết quả từ mạng!

Trong bước tiếp theo, bạn sẽ tích hợp coroutine vào Phòng và Retrofit.

Để tiếp tục tích hợp coroutine, chúng ta sẽ dùng chức năng hỗ trợ cho các hàm tạm ngưng trong phiên bản Phòng và Retrofit ổn định, sau đó đơn giản hóa mã mà chúng ta vừa viết bằng cách sử dụng các hàm tạm ngưng.

Coroutine trong Phòng

Trước tiên, mở MainDatabase.kt và đặt insertTitle làm hàm tạm ngưng:

MainDatabase.kt

// add the suspend modifier to the existing insertTitle

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)

Khi bạn làm việc này, Phòng sẽ tự động đặt truy vấn của bạn thành an toàn và thực thi trên luồng trong nền. Tuy nhiên, điều này cũng có nghĩa là bạn chỉ có thể gọi truy vấn này từ bên trong một coroutine.

Và – đó là tất cả những gì bạn cần làm để sử dụng coroutine trong Phòng. Khá đẹp.

Coroutine trong Retrofit

Tiếp theo, hãy xem cách tích hợp coroutine với Retrofit. Mở MainNetwork.kt và thay đổi fetchNextTitle thành một hàm tạm ngưng.

MainNetwork.kt

// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String

interface MainNetwork {
   @GET("next_title.json")
   suspend fun fetchNextTitle(): String
}

Để sử dụng các hàm tạm ngưng với Retrofit, bạn phải thực hiện hai việc:

  1. Thêm từ khóa xác định suspend vào hàm
  2. Xoá trình bao bọc Call khỏi loại dữ liệu trả về. Ở đây chúng tôi sẽ trả về String, nhưng bạn cũng có thể trả về loại phức tạp dựa trên json. Nếu vẫn muốn cấp quyền truy cập vào Result đầy đủ hơn, bạn có thể trả về Result<String> thay vì String từ hàm tạm ngưng.

Retrofit sẽ tự động đặt các hàm tạm ngưng main-safe để bạn có thể gọi trực tiếp từ Dispatchers.Main.

Sử dụng Phòng và Retrofit

Giờ đây, Phòng và Retrofit hỗ trợ các hàm tạm ngưng, nên chúng ta có thể dùng các hàm này từ kho lưu trữ. Mở TitleRepository.kt và xem việc sử dụng các hàm tạm ngưng giúp đơn giản hóa logic một cách đáng kể, ngay cả so với phiên bản chặn:

Tiêu đềRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Ôi, ngắn hơn 39 giây. Điều gì đã xảy ra? Hóa ra việc dựa vào tạm ngưng và tiếp tục cho phép mã ngắn hơn nhiều. Retrofit cho phép chúng ta dùng các loại dữ liệu trả về như String hoặc đối tượng User ở đây, thay vì Call. Điều này là an toàn để làm vì vì bên trong hàm tạm ngưng, Retrofit có thể chạy yêu cầu mạng trên luồng trong nền và tiếp tục coroutine khi lệnh gọi hoàn tất.

Điều tuyệt vời hơn nữa là chúng tôi đã loại bỏ withContext. Vì cả Phòng và Retrofit đều cung cấp các chức năng tạm ngưng an toàn chính, nên bạn có thể sắp xếp công việc không đồng bộ này từ Dispatchers.Main một cách an toàn.

Sửa lỗi trình biên dịch

Khi chuyển sang coroutine, bạn sẽ phải thay đổi chữ ký của các hàm vì bạn không thể gọi một hàm tạm ngưng từ một hàm thông thường. Khi bạn thêm từ khóa xác định suspend ở bước này, một vài lỗi trình biên dịch đã được tạo ra để cho bạn biết điều gì sẽ xảy ra nếu bạn thay đổi một hàm để tạm ngưng trong một dự án thực tế.

Xem qua dự án và sửa lỗi trình biên dịch bằng cách thay đổi hàm để tạm ngưng đã tạo. Dưới đây là các giải pháp nhanh cho từng trường hợp:

Testings.kt

Hãy cập nhật địa chỉ thử nghiệm giả để hỗ trợ công cụ sửa đổi tạm ngưng mới.

Tiêu đề giả mạo

  1. Nhấn phím Enter-Enter để thêm các từ khóa xác định tạm ngưng vào tất cả các hàm trong nhật ký

MainNetworkfalse

  1. Nhấn phím Enter-Enter để thêm các từ khóa xác định tạm ngưng vào tất cả các hàm trong nhật ký
  2. Thay thế fetchNextTitle bằng hàm này
override suspend fun fetchNextTitle() = result

MainNetworkCompletable giả mạo

  1. Nhấn phím Enter-Enter để thêm các từ khóa xác định tạm ngưng vào tất cả các hàm trong nhật ký
  2. Thay thế fetchNextTitle bằng hàm này
override suspend fun fetchNextTitle() = completable.await()

TitleRepository.kt

  • Xóa hàm refreshTitleWithCallbacks vì hàm này không còn được dùng nữa.

Chạy ứng dụng

Chạy lại ứng dụng. Sau khi được biên dịch, bạn sẽ thấy rằng ứng dụng đang tải dữ liệu bằng coroutine từ ViewModel đến Room và Retrofit!

Xin chúc mừng, bạn đã đổi hoàn toàn ứng dụng này sang sử dụng coroutine! Để kết thúc, chúng ta sẽ nói một chút về cách kiểm tra những gì chúng ta vừa làm.

Trong bài tập này, bạn sẽ viết một thử nghiệm gọi trực tiếp hàm suspend.

refreshTitle được hiển thị dưới dạng một API công khai nên API này sẽ được kiểm tra trực tiếp và cho thấy cách gọi hàm coroutine từ các thử nghiệm.

Đây là hàm refreshTitle mà bạn đã triển khai trong bài tập trước:

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Viết bài kiểm tra gọi chức năng tạm ngưng

Mở TitleRepositoryTest.kt trong thư mục test có hai VIỆC CẦN LÀM.

Thử gọi refreshTitle từ thử nghiệm whenRefreshTitleSuccess_insertsRows đầu tiên.

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   subject.refreshTitle()
}

refreshTitle là một hàm suspend mà Kotlin không biết cách gọi hàm đó từ một coroutine hoặc một hàm tạm ngưng khác, và bạn sẽ nhận được lỗi trình biên dịch như: "suspend title refreshTitle chỉ được gọi từ coroutine hoặc hàm tạm ngưng khác.

Người chạy thử nghiệm không biết gì về coroutine, vì vậy, chúng tôi không thể biến thử nghiệm này thành hàm tạm ngưng. Chúng ta có thể launch một coroutine bằng cách dùng CoroutineScope như trong ViewModel, tuy nhiên, các thử nghiệm cần chạy coroutine để hoàn tất trước khi trả về. Sau khi một hàm kiểm tra trả về, kết quả kiểm tra sẽ kết thúc. Coroutine bắt đầu bằng launch là mã không đồng bộ, có thể hoàn tất vào một thời điểm nào đó trong tương lai. Vì vậy, để kiểm tra mã không đồng bộ đó, bạn cần có một số cách để yêu cầu thử nghiệm đợi cho đến khi coroutine hoàn tất. Vì launch là một lệnh gọi không chặn, tức là lệnh gọi đó trả về ngay lập tức và có thể tiếp tục chạy coroutine sau khi hàm trả về – không thể dùng để thử nghiệm được. Ví dụ:

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   // launch starts a coroutine then immediately returns
   GlobalScope.launch {
       // since this is asynchronous code, this may be called *after* the test completes
       subject.refreshTitle()
   }
   // test function returns immediately, and
   // doesn't see the results of refreshTitle
}

Thử nghiệm này sẽ đôi khi không thành công. Lệnh gọi đến launch sẽ quay lại ngay lập tức và thực thi cùng lúc với phần còn lại của trường hợp thử nghiệm. Quá trình kiểm tra không có cách nào để biết refreshTitle có chạy hay không – và mọi lời khẳng định như kiểm tra xem cơ sở dữ liệu đã cập nhật sẽ không chính xác. Và nếu refreshTitle cho ra một trường hợp ngoại lệ, thì trường hợp này sẽ không bị đưa vào ngăn xếp lệnh gọi thử nghiệm. Thay vào đó, tệp này sẽ được đưa vào trình xử lý ngoại lệ chưa được nắm bắt của GlobalScope.

Thư viện kotlinx-coroutines-test có hàm runBlockingTest chặn trong khi gọi các hàm tạm ngưng. Khi gọi một hàm tạm ngưng hoặc launches một coroutine mới, runBlockingTest sẽ thực thi hàm này ngay lập tức theo mặc định. Bạn có thể coi đây là một cách để chuyển đổi các hàm tạm ngưng và coroutine thành các lệnh gọi hàm thông thường.

Ngoài ra, runBlockingTest sẽ loại bỏ các trường hợp ngoại lệ chưa được giải quyết cho bạn. Điều này giúp bạn dễ dàng kiểm tra hơn khi coroutine đang gửi một ngoại lệ.

Triển khai thử nghiệm bằng một coroutine

Kết thúc lệnh gọi đến refreshTitle bằng runBlockingTest và xóa trình bao bọc GlobalScope.launch khỏi subject.refreshTitle().

TitleRepositoryTest.kt

@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
   val titleDao = TitleDaoFake("title")
   val subject = TitleRepository(
           MainNetworkFake("OK"),
           titleDao
   )

   subject.refreshTitle()
   Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}

Thử nghiệm này sử dụng hàng giả được cung cấp để kiểm tra xem "OK" được chèn vào cơ sở dữ liệu bởi refreshTitle.

Khi thử nghiệm gọi runBlockingTest, phương thức này sẽ chặn cho đến khi coroutine do runBlockingTest bắt đầu hoàn tất. Sau đó, khi chúng ta gọi refreshTitle, hàm này sử dụng cơ chế tạm ngưng và tiếp tục thông thường để đợi hàng cơ sở dữ liệu được thêm vào hàng giả.

Sau khi coroutine thử nghiệm hoàn tất, runBlockingTest sẽ trả về.

Viết thử nghiệm thời gian chờ

Chúng tôi muốn thêm một thời gian chờ ngắn vào yêu cầu mạng. Hãy viết thử nghiệm trước rồi triển khai thời gian chờ. Tạo thử nghiệm mới:

TitleRepositoryTest.kt

@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
   val network = MainNetworkCompletableFake()
   val subject = TitleRepository(
           network,
           TitleDaoFake("title")
   )

   launch {
       subject.refreshTitle()
   }

   advanceTimeBy(5_000)
}

Thử nghiệm này sử dụng MainNetworkCompletableFake giả mạo đã được cung cấp, đây là một mạng giả mạo được thiết kế để tạm ngưng người gọi cho đến khi thử nghiệm tiếp tục. Khi refreshTitle cố gắng thực hiện một yêu cầu mạng, yêu cầu này sẽ bị treo vĩnh viễn vì chúng tôi muốn kiểm tra thời gian chờ.

Sau đó, phương thức này sẽ chạy một coroutine riêng biệt để gọi refreshTitle. Đây là phần quan trọng của thời gian chờ thử nghiệm, thời gian chờ phải xảy ra trong một coroutine khác với coroutine runBlockingTest tạo. Bằng cách này, chúng ta có thể gọi dòng tiếp theo, advanceTimeBy(5_000), dòng này sẽ kéo dài thời gian thêm 5 giây và khiến coroutine khác hết thời gian chờ.

Đây là thử nghiệm hết thời gian chờ và sẽ vượt qua sau khi chúng tôi triển khai thời gian chờ.

Hãy chạy ngay bây giờ và xem điều gì sẽ xảy ra:

Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]

Một trong những tính năng của runBlockingTest là bạn sẽ không bị rò rỉ coroutine sau khi thử nghiệm hoàn tất. Nếu có bất kỳ coroutine nào chưa hoàn tất, chẳng hạn như coroutine chạy của chúng ta, thì khi kết thúc thử nghiệm, nó sẽ không thực hiện được thử nghiệm.

Thêm thời gian chờ

Mở TitleRepository và thêm thời gian chờ 5 giây vào phương thức tìm nạp mạng. Bạn có thể thực hiện việc này bằng cách dùng hàm withTimeout:

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = withTimeout(5_000) {
           network.fetchNextTitle()
       }
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Chạy thử nghiệm. Khi chạy thử nghiệm, bạn sẽ thấy tất cả thử nghiệm vượt qua!

Trong bài tập tiếp theo, bạn sẽ tìm hiểu cách viết các hàm bậc cao hơn bằng coroutine.

Trong bài tập này, bạn sẽ tái cấu trúc refreshTitle trong MainViewModel để sử dụng hàm tải dữ liệu chung. Thao tác này sẽ hướng dẫn bạn cách tạo các hàm bậc cao hơn bằng coroutine.

Cách triển khai hiện tại của refreshTitle hoạt động, nhưng chúng ta có thể tạo coroutine tải dữ liệu chung luôn hiển thị vòng quay. Điều này có thể hữu ích trong cơ sở mã tải dữ liệu để phản hồi một số sự kiện và muốn đảm bảo vòng quay tải được hiển thị nhất quán.

Xem lại cách triển khai hiện tại mỗi dòng ngoại trừ repository.refreshTitle() nguyên mẫu để hiển thị vòng quay và lỗi hiển thị.

// MainViewModel.kt

fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           // this is the only part that changes between sources
           repository.refreshTitle() 
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Sử dụng coroutine trong các hàm bậc cao hơn

Thêm mã này vào MainViewModel.kt

MainViewModel.kt

private fun launchDataLoad(block: suspend () -> Unit): Job {
   return viewModelScope.launch {
       try {
           _spinner.value = true
           block()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Bây giờ, hãy tái cấu trúc refreshTitle() để sử dụng hàm bậc cao hơn này.

MainViewModel.kt

fun refreshTitle() {
   launchDataLoad {
       repository.refreshTitle()
   }
}

Bằng cách trừu tượng hóa logic về việc hiển thị vòng quay tải và hiển thị lỗi, chúng tôi đã đơn giản hóa mã thực tế cần thiết để tải dữ liệu. Hiển thị vòng quay hoặc hiển thị lỗi là điều dễ dàng để khái quát hoá bất kỳ việc tải dữ liệu nào, trong khi nguồn dữ liệu và đích thực tế cần phải được chỉ định mỗi lần.

Để tạo phần trừu tượng này, launchDataLoad sẽ lấy một đối số block là hàm lambda tạm ngưng. Hàm lambda tạm ngưng cho phép bạn gọi các hàm tạm ngưng. Đó là cách Kotlin triển khai trình tạo coroutine launchrunBlocking mà chúng ta đang dùng trong lớp học lập trình này.

// suspend lambda

block: suspend () -> Unit

Để tạo một hàm lambda, hãy bắt đầu bằng từ khóa suspend. Mũi tên hàm và loại dữ liệu trả về Unit sẽ hoàn tất nội dung khai báo.

Bạn thường không phải khai báo hàm lambda tạm ngưng của riêng mình, nhưng chúng có thể hữu ích để tạo các khái niệm trừu tượng giống như hàm này nhằm đóng gói logic lặp lại!

Trong bài tập này, bạn sẽ tìm hiểu cách sử dụng mã dựa trên coroutine từ WorkManager.

WorkManager là gì

Có nhiều tùy chọn trên Android để làm việc trong nền. Bài tập này sẽ hướng dẫn bạn cách tích hợp WorkManager với các coroutine. WorkManager là một thư viện tương thích, linh hoạt và đơn giản để thực hiện các tác vụ trong nền. WorkManager là giải pháp được đề xuất cho các trường hợp sử dụng này trên Android.

WorkManager là một phần của Android JetpackThành phần cấu trúc dành cho tác vụ trong nền cần kết hợp giữa quá trình thực thi cơ hội và đảm bảo. Thực thi theo cơ hội có nghĩa là WorkManager sẽ thực hiện tác vụ trong nền của bạn ngay khi có thể. Thực thi được đảm bảo có nghĩa là WorkManager sẽ xử lý logic để bắt đầu tác vụ của bạn trong nhiều tình huống, ngay cả khi bạn điều hướng khỏi ứng dụng của mình.

Do đó, WorkManager là một lựa chọn tốt cho các tác vụ phải hoàn tất cuối cùng.

Một số ví dụ về nhiệm vụ sử dụng tốt WorkManager:

  • Tải nhật ký lên
  • Áp dụng bộ lọc cho hình ảnh và lưu hình ảnh
  • Định kỳ đồng bộ hóa dữ liệu cục bộ với mạng

Dùng coroutine với WorkManager

WorkManager cung cấp nhiều cách triển khai lớp ListanableWorker cơ sở cho nhiều trường hợp sử dụng.

Lớp Worker đơn giản nhất cho phép chúng ta thực hiện một số thao tác đồng bộ do WorkManager thực thi. Tuy nhiên, từ trước đến nay, chúng tôi đã cố gắng chuyển đổi cơ sở mã của chúng tôi để sử dụng coroutine và các hàm tạm ngưng. Cách tốt nhất để sử dụng WorkManager là dùng lớp CoroutineWorker cho phép xác định hàm doWork() của chúng ta làm hàm tạm ngưng.

Để bắt đầu, hãy mở RefreshMainDataWork. Tiện ích này đã mở rộng CoroutineWorker và bạn cần triển khai doWork.

Bên trong hàm suspend doWork, hãy gọi refreshTitle() từ kho lưu trữ và trả về kết quả phù hợp!

Sau khi bạn hoàn thành VIỆC CẦN LÀM, mã sẽ trông giống như sau:

override suspend fun doWork(): Result {
   val database = getDatabase(applicationContext)
   val repository = TitleRepository(network, database.titleDao)

   return try {
       repository.refreshTitle()
       Result.success()
   } catch (error: TitleRefreshError) {
       Result.failure()
   }
}

Xin lưu ý rằng CoroutineWorker.doWork() là hàm tạm ngưng. Không giống như lớp Worker đơn giản hơn, mã này KHÔNG chạy trên Trình thực thi được chỉ định trong cấu hình WorkManager của bạn, mà thay vào đó sử dụng trình điều phối trong thành viên coroutineContext (theo mặc định là Dispatchers.Default).

Kiểm tra CoroutineWorker của chúng tôi

Bạn không nên hoàn tất cơ sở mã nào mà không thử nghiệm.

WorkManager cung cấp một số cách kiểm tra các lớp Worker của bạn. Để tìm hiểu thêm về cơ sở hạ tầng thử nghiệm ban đầu, bạn có thể đọc tài liệu này.

WorkManager v2.1 giới thiệu một bộ API mới để hỗ trợ một cách đơn giản hơn để kiểm tra các lớp ListenableWorker và do đó, CoroutineWorker. Trong mã của chúng tôi, chúng tôi sẽ sử dụng một trong các API mới sau: TestListenableWorkerBuilder.

Để thêm thử nghiệm mới, hãy cập nhật tệp RefreshMainDataWorkTest trong thư mục androidTest.

Nội dung của tệp là:

package com.example.android.kotlincoroutines.main

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4


@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {

@Test
fun testRefreshMainDataWork() {
   val fakeNetwork = MainNetworkFake("OK")

   val context = ApplicationProvider.getApplicationContext<Context>()
   val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
           .setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
           .build()

   // Start the work synchronously
   val result = worker.startWork().get()

   assertThat(result).isEqualTo(Result.success())
}

}

Trước khi tiến hành thử nghiệm, chúng tôi thông báo cho WorkManager về trạng thái ban đầu để chúng tôi có thể đưa mạng giả vào mạng.

Bản thân thử nghiệm sử dụng TestListenableWorkerBuilder để tạo nhân viên của chúng tôi, sau đó chúng tôi có thể chạy phương thức startWork() này.

WorkManager chỉ là một ví dụ về cách dùng coroutine để đơn giản hóa thiết kế API.

Trong lớp học lập trình này, chúng tôi đã đề cập đến những thông tin cơ bản mà bạn cần phải bắt đầu sử dụng coroutine trong ứng dụng của mình!

Chúng tôi đã đề cập đến:

  • Cách tích hợp coroutine vào ứng dụng Android từ cả công việc trên giao diện người dùng và WorkManager để đơn giản hóa hoạt động lập trình không đồng bộ,
  • Cách sử dụng coroutine bên trong ViewModel để tìm nạp dữ liệu từ mạng và lưu dữ liệu đó vào cơ sở dữ liệu mà không chặn chuỗi chính.
  • Và cách hủy tất cả coroutine khi ViewModel kết thúc.

Đối với mã thử nghiệm coroutine, chúng tôi đã đề cập cả bằng cách hoạt động của thử nghiệm và gọi trực tiếp các hàm suspend từ thử nghiệm.

Tìm hiểu thêm

Hãy xem "Coroutines nâng cao với Kotlin Flow và LiveData" codec

Coroutine của Kotlin có nhiều tính năng không thuộc phạm vi của lớp học lập trình này. Nếu bạn muốn tìm hiểu thêm về coroutine của Kotlin, hãy đọc hướng dẫn về coroutine do JetBrains xuất bản. Ngoài ra, hãy xem "Cải thiện hiệu suất của ứng dụng bằng coroutine của Kotlin" để biết thêm các cách sử dụng coroutine trên Android.