Trong lớp học lập trình này, bạn sẽ tìm hiểu cách sử dụng Coroutine của Kotlin trong một ứng dụng Android – một cách mới để quản lý các luồng trong nền, có thể đơn giản hoá mã bằng cách giảm nhu cầu về lệnh gọi lại. Coroutine là một tính năng của Kotlin, chuyển đổi các lệnh gọi lại không đồng bộ cho các tác vụ chạy trong thời gian dài (chẳng hạn như quyền truy cập vào cơ sở dữ liệu hoặc mạng) thành mã tuần tự.
Dưới đây là một đoạn mã để giúp bạn hình dung được những việc bạn 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 cách sử dụ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 Architecture Components, sử dụng kiểu lệnh 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 nhằm tải dữ liệu từ mạng và bạn sẽ có thể tích hợp coroutine vào một ứng dụng. Bạn cũng sẽ nắm được các phương pháp hay nhất cho coroutine và cách viết một bài kiểm thử đối với 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
,Repository
vàRoom
. - Kinh nghiệm về cú pháp Kotlin, bao gồm cả hàm mở rộng và lambda.
- Có kiến thức cơ bản về cách sử dụng các luồng trên Android, bao gồm cả luồng chính, luồng nền và lệnh gọi lại.
Bạn sẽ thực hiện
- Gọi mã được viết bằng coroutine và nhận kết quả.
- Sử dụng các hàm tạm ngưng để tạo mã không đồng bộ theo tuần tự.
- Sử dụng
launch
vàrunBlocking
để kiểm soát cách thực thi mã. - Tìm hiểu các kỹ thuật chuyển đổi API hiện có thành coroutine bằng cách sử dụng
suspendCoroutine
. - Sử dụng coroutine với các Thành phần kiến trúc.
- Tìm hiểu các phương pháp hay nhất để kiểm thử 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ố nội dung có thể bị thiếu hoặc trông khác).
Nếu bạn gặp vấn đề (lỗi trong đoạn mã, lỗi ngữ pháp, từ ngữ không rõ ràng, v.v.) khi thực hành theo lớp học lập trình này, vui lòng báo cáo vấn đề thông qua đường liên kết Báo cáo lỗi ở góc dưới bên trái 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:
... hoặc sao chép kho lưu trữ GitHub từ dòng lệnh bằng cách sử 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 minh hoạ bắt đầu trông như thế nào. Hãy làm theo các hướng dẫn sau để mở ứng dụng mẫu trong Android Studio.
- Nếu bạn đã tải tệp zip
kotlin-coroutines
xuống, hãy giải nén tệp đó. - Mở dự án
coroutines-codelab
trong Android Studio. - Chọn mô-đun ứng dụng
start
. - Nhấp vào nút
Run (Chạ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 Coroutine của Kotlin sẽ xuất hiện:
Ứng dụng khởi động này sử dụng các luồng để tăng số lượng sau một khoảng thời gian ngắn kể từ khi bạn nhấn vào màn hình. Thao tác này cũng sẽ tìm nạp một 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ờ, bạn sẽ thấy số lượng và thông báo thay đổi sau một khoảng thời gian ngắn. 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 các coroutine.
Ứng dụng này sử dụng các Thành phần cấu trúc để 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.
MainActivity
hiển thị giao diện người dùng, đăng ký trình nghe lượt nhấp và có thể hiển thị mộtSnackbar
. Thành phần này truyền các sự kiện đếnMainViewModel
và cập nhật màn hình dựa trênLiveData
trongMainViewModel
.MainViewModel
xử lý các sự kiện trongonMainViewClicked
và sẽ giao tiếp vớiMainActivity
bằng cách sử dụngLiveData.
Executors
xác địnhBACKGROUND,
có thể chạy các thao tác trên một luồng trong nền.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 các coroutine vào dự án
Để sử dụng coroutine trong Kotlin, bạn phải thêm thư viện coroutines-core
vào tệp build.gradle (Module: app)
của dự án. Các dự án trong lớp học lập trình đã thực hiện việc này cho bạn, nên bạn không cần làm vậy để hoàn tất lớp học lập trình.
Các coroutine trên Android có sẵn dưới dạng một thư viện cốt lõi và các tiện ích dành riêng cho Android:
- kotlinx-coroutines-core – Giao diện chính để sử dụng coroutine trong Kotlin
- kotlinx-coroutines-android – Hỗ trợ luồng chính của Android trong coroutine
Ứng dụng khởi động đã bao gồm các phần phụ thuộc trong build.gradle.
Khi tạo một 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, bạn phải tránh việc chặn luồng chính. Luồng chính là một luồng duy nhất xử lý tất cả bản cập nhật cho giao diện người dùng. Đây cũng là luồng gọi tất 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 đó, luồng này phải chạy mượt mà để đảm bảo mang lại trải nghiệm người dùng tuyệt vời.
Để người dùng nhìn thấy ứng dụng mà không gặp phải tình trạng tạm dừng, luồng chính phải cập nhật màn hình chậm nhất sau mỗi 16 mili giây, tức là khoảng 60 khung hình/giây. Nhiều thao tác phổ biến mất nhiều thời gian hơn, 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ừ luồng chính có thể khiến ứng dụng tạm dừng, bị giật hoặc thậm chí bị treo. Nếu bạn chặn luồng chính trong thời gian quá dài, ứng dụng có thể gặp sự cố và cho thấy hộp thoại Ứng dụng không phản hồi.
Hãy xem video bên dưới để biết thông tin giới thiệu về cách coroutine giải quyết vấn đề này cho chúng ta trên Android bằng cách giới thiệu tính năng an toàn cho luồng chính.
Mẫu gọi lại
Có một mẫu để thực hiện các thao tác dài hạn mà không chặn luồng 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 thao tác hoàn tất, lệnh gọi lại sẽ được gọi để thông báo cho bạn về kết quả trên luồng 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à nó cần trả về rất nhanh để lần cập nhật màn hình tiếp theo không bị chậm 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 luồng chính không thể chờ kết quả. Lệnh gọi lại show(result)
cho phép slowFetch
chạy trên một luồng nền và trả về kết quả khi sẵn sàng.
Sử dụng coroutine để xoá lệnh gọi lại
Lệnh gọi lại là một mẫu tuyệt vời, nhưng cũng có một vài hạn chế. Mã sử dụng nhiều lệnh gọi lại có thể trở nên khó đọc và khó hiểu hơn. Ngoài ra, lệnh gọi lại không cho phép sử dụng một số tính năng 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ể sử dụng các tính năng ngôn ngữ như ngoại lệ.
Rốt cục thì chúng hoạt động giống hệt nhau: chờ cho đến khi có kết quả từ một thao tác dài hạn rồi tiếp tục thực thi. Tuy nhiên, trong mã, chúng trông rất khác nhau.
Từ khoá suspend
là cách Kotlin đánh dấu một hàm hoặc loại hàm có sẵn cho các coroutine. Khi một coroutine gọi một hàm được đánh dấu 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 từ nơi dừng lại kèm theo kết quả. Trong khi tạm ngưng để chờ kết quả, coroutine 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ã dưới đây, makeNetworkRequest()
và slowFetch()
đều là 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 { ... }
Giống như phiên bản lệnh gọi lại, makeNetworkRequest
phải trả về ngay từ luồng chính vì được đánh dấu là @UiThread
. Điều này có nghĩa là thường thì nó không thể gọi các phương thức chặn như slowFetch
. Đây là nơi từ khoá suspend
phát huy tác dụng.
So với mã dựa trên lệnh gọi lại, mã coroutine đạt được kết quả tương tự là bỏ chặn luồng hiện tại với ít mã hơn. Do có kiểu tuần tự, nên bạn có thể dễ dàng liên kết một số tác vụ chạy trong thời gian dài mà không cần tạo nhiều lệnh gọi lại. Ví dụ: mã tìm nạp kết quả từ 2 điểm cuối mạng và lưu kết quả đó vào cơ sở dữ liệu có thể được viết dưới dạng một hàm trong các coroutine mà không có lệnh gọi lại. Chẳng hạn như:
// 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ẽ giới thiệu coroutine cho ứng dụng mẫu trong phần tiếp theo.
Trong bài tập này, bạn sẽ viết một coroutine để hiển thị thông báo sau một khoảng thời gian trễ. Để bắt đầu, hãy đảm bảo bạn đã mở mô-đun start
trong Android Studio.
Tìm hiểu về CoroutineScope
Trong Kotlin, tất cả coroutine đều chạy bên trong một CoroutineScope
. Phạm vi kiểm soát toàn bộ thời gian của coroutine thông qua tác vụ. 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 một phạm vi để huỷ tất cả các coroutine đang chạy, chẳng hạn như khi người dùng di chuyển khỏi một Activity
hoặc Fragment
. Phạm vi cũng cho phép bạn chỉ định một trình điều phối mặc định. Trình điều phối kiểm soát luồng nào chạy một coroutine.
Đối với các coroutine do giao diện người dùng khởi động, bạn thường nên khởi động chúng trên Dispatchers.Main
(luồng chính trên Android). Một coroutine bắt đầu trên Dispatchers.Main
sẽ không chặn luồng chính trong khi bị tạm ngưng. Vì coroutine 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 các coroutine trên luồng chính sẽ giúp bạn tiết kiệm các lần chuyển đổi luồng bổ sung. Một coroutine bắt đầu trên luồng chính có thể chuyển đổi trình điều phối bất cứ lúc nào sau khi bắt đầu. Ví dụ: nó có thể sử dụng một trình điều phối khác để phân tích cú pháp một kết quả JSON lớn bên ngoài luồng chính.
Sử dụng viewModelScope
Thư viện AndroidX lifecycle-viewmodel-ktx
sẽ thêm một CoroutineScope vào ViewModel được định cấu hình để bắt đầu các 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 này vào tệp build.gradle (Module: start)
của dự án. Bước đó đã được thực hiện trong các dự án của lớp học lập trình.
dependencies { ... implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x" }
Thư viện này thêm viewModelScope
làm hàm tiện ích của lớp ViewModel
. Phạm vi này được liên kết với Dispatchers.Main
và sẽ tự động bị huỷ khi ViewModel
bị xoá.
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 nền. Vì sleep
chặn luồng hiện tại, nên nếu được gọi trên luồng chính, hàm này sẽ làm treo giao diện người dùng. Một giây sau khi người dùng nhấp vào khung hiển thị chính, ứng dụng sẽ yêu cầu một thanh thông báo.
Bạn có thể thấy điều đó bằng cách xoá BACKGROUND khỏi mã và chạy lại. Vòng quay tải sẽ không xuất hiện và mọi thứ sẽ "nhảy" đến trạng thái cuối cùng sau một giây.
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 đoạn mã dựa trên đồng thời này để thực hiện cùng một việc. Bạn sẽ phải nhập launch
và delay
.
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")
}
}
Đoạn mã này cũng làm điều tương tự, tức là đợi một giây trước khi hiện một thanh thông báo. Tuy nhiên, có một số điểm khác biệt quan trọng:
viewModelScope.
launch
sẽ khởi động một coroutine trongviewModelScope
. Điều này có nghĩa là khi công việc mà chúng ta truyền đếnviewModelScope
bị huỷ, tất cả coroutine trong công việc/phạm vi này sẽ bị huỷ. Nếu người dùng rời khỏi Hoạt động trước khidelay
trả về, thì coroutine này sẽ tự động bị huỷ khionCleared
được gọi khi ViewModel bị huỷ.- Vì
viewModelScope
có trình điều phối mặc định làDispatchers.Main
, nên coroutine này sẽ được khởi chạy trong luồng chính. Sau này, chúng ta sẽ tìm hiểu cách sử dụng các luồng khác nhau. - Hàm
delay
là hàmsuspend
. Điều này được thể hiện 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 đó, trình điều phối sẽ lên lịch để coroutine tiếp tục sau một giây tại câu lệnh tiếp theo.
Hãy tiếp tục và chạy chương trình này. Khi nhấp vào chế độ xem chính, bạn sẽ thấy một thanh thông báo sau một giây.
Trong phần tiếp theo, chúng ta sẽ xem xét cách kiểm thử hàm này.
Trong bài tập này, bạn sẽ viết một bài kiểm thử 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 thử các coroutine đang chạy trên Dispatchers.Main
bằng cách sử dụng thư viện kotlinx-coroutines-test. Trong phần sau của lớp học lập trình này, bạn sẽ triển khai một kiểm thử tương tác trực tiếp với các 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 một kiểm thử trong JUnit. Chúng ta sử dụng 2 quy tắc để kiểm thử MainViewModel trong một quy trình kiểm thử ngoài thiết bị:
InstantTaskExecutorRule
là một quy tắc JUnit định cấu hìnhLiveData
để thực thi từng tác vụ một cách đồng bộMainCoroutineScopeRule
là một quy tắc tuỳ chỉnh trong cơ sở mã này, quy tắc này định cấu hìnhDispatchers.Main
để sử dụngTestCoroutineDispatcher
từkotlinx-coroutines-test
. Điều này cho phép các kiểm thử chuyển đồng hồ ảo để kiểm thử và cho phép mã sử dụngDispatchers.Main
trong các kiểm thử đơ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 các đối tượng giả để kiểm thử – đây là các triển khai giả của mạng và cơ sở dữ liệu được cung cấp trong mã khởi đầu để giúp viết các kiểm thử mà không cần sử dụng mạng hoặc cơ sở dữ liệu thực.
Đối với kiểm thử này, các đối tượng giả chỉ cần thiết để đáp ứng các phần phụ thuộc của MainViewModel
. Sau đó trong lớp học lập trình này, bạn sẽ cập nhật các dữ liệu giả để hỗ trợ coroutine.
Viết một bài kiểm thử kiểm soát các coroutine
Thêm một kiểm thử mới để đảm bảo rằng các thao tác nhấn được cập nhật một giây sau khi nhấp vào khung hiển thị chính:
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 mà chúng ta vừa tạo sẽ được khởi chạy. Bài kiểm thử này kiểm tra để đảm bảo văn bản về số lần nhấn vẫn là "0 lần nhấn" ngay sau khi onMainViewClicked
được gọi, sau đó 1 giây sau, văn bản này sẽ được cập nhật thành "1 lần nhấn".
Bài kiểm thử này sử dụng thời gian ảo để kiểm soát quá trình 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 được khởi chạy trên Dispatchers.Main
. Ở đây, chúng ta đang gọi advanceTimeBy(1_000)
. Thao tác này sẽ khiến trình đ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 sau 1 giây.
Thử nghiệm này hoàn toàn có tính xác định, tức là thử nghiệm sẽ luôn thực thi theo cùng một cách. Ngoài ra, vì có toàn quyền kiểm soát việc thực thi các coroutine được khởi chạy trên Dispatchers.Main
, nên không cần phải đợi một giây để đặt giá trị.
Chạy kiểm thử hiện có
- Nhấp chuột phải vào tên lớp
MainViewModelTest
trong trình chỉnh sửa để mở trình đơn theo bối cảnh. - Trong trình đơn theo bối cảnh, hãy chọn
Run "MainViewModelTest" (Chạy "MainViewModelTest")
- Đối với các lần chạy sau này, bạn có thể chọn cấu hình kiểm thử này trong các cấu hình bên cạnh nút
trên thanh công cụ. Theo mặc định, cấu hình này sẽ được gọi là MainViewModelTest.
Bạn sẽ thấy chương trình kiểm thử thành công! Và quá trình này sẽ mất ít hơ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.
Trong bước này, bạn sẽ bắt đầu chuyển đổi một kho lưu trữ để sử dụng coroutine. Để làm việc này, chúng ta sẽ thêm các coroutine vào ViewModel
, Repository
, Room
và Retrofit
.
Bạn nên hiểu rõ trách nhiệm của từng phần trong cấu trúc trước khi chuyển chúng sang sử dụng coroutine.
MainDatabase
triển khai một cơ sở dữ liệu bằng Room để lưu và tải mộtTitle
.MainNetwork
triển khai một 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ề lỗi hoặc dữ liệu mô phỏng một cách ngẫu nhiên, nhưng nếu không thì sẽ hoạt động như thể đang gửi yêu cầu mạng thực.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.MainViewModel
biểu thị 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 điều khiển 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 các sự kiện đó, nên vị trí tự nhiên để bắt đầu sử dụng coroutine là trong ViewModel
.
Phiên bản lệnh gọi lại
Mở MainViewModel.kt
để xem khai báo về 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à hàm này 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.
Hoạt động triển khai này sử dụng một lệnh gọi lại để thực hiện một số việc:
- Trước khi bắt đầu một truy vấn, ứng dụng sẽ hiển thị một vòng quay tải có
_spinner.value = true
- Khi nhận được kết quả, phương thức này sẽ xoá biểu tượng tải bằng
_spinner.value = false
- Nếu gặp lỗi, nó sẽ yêu cầu thanh thông báo nhanh hiển thị và xoá trình quay
Xin lưu ý rằng lệnh gọi lại onCompleted
không được truyền title
. Vì chúng ta ghi tất cả tiêu đề vào cơ sở dữ liệu Room
, nên giao diện người dùng sẽ cập nhật tiêu đề hiện tại bằng cách theo dõi một LiveData
do Room
cập nhật.
Trong bản cập nhật cho coroutine, chúng ta sẽ giữ nguyên hành vi này. Đây là một mẫu hay để sử dụng nguồn dữ liệu có khả năng ghi nhận 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 viết lại refreshTitle
bằng coroutine!
Vì chúng ta sẽ cần đến nó ngay lập tức, nên hãy tạo một hàm tạm ngưng trống trong kho lưu trữ (TitleRespository.kt
). Xác định một hàm mới dùng toán tử suspend
để cho Kotlin biết rằng hàm này 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 để sử dụng Retrofit và Room nhằm tìm nạp một tiêu đề mới và ghi tiêu đề đó vào cơ sở dữ liệu bằng cách sử dụng coroutine. Hiện tại, nó sẽ chỉ mất 500 mili giây để giả vờ thực hiện công việc rồi tiếp tục.
Trong MainViewModel
, hãy thay thế phiên bản callback của refreshTitle
bằng một phiên bản khởi chạy một 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 xem xét hàm này:
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
, điều này là chấp nhận được. Mặc dù refreshTitle
sẽ thực hiện một yêu cầu mạng và truy vấn cơ sở dữ liệu, nhưng nó có thể sử dụng coroutine để hiển thị một giao diện an toàn cho luồng chính. Điều này có nghĩa là bạn có thể gọi hàm này một cách an toàn từ luồng chính.
Vì chúng ta đang sử dụng viewModelScope
, nên khi người dùng rời khỏi màn hình này, công việc do coroutine này bắt đầu sẽ tự động bị huỷ. Điều đó có nghĩa là nó sẽ không đưa ra các yêu cầu mạng hoặc truy vấn cơ sở dữ liệu bổ sung.
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 thực hiện bất kỳ thao tác nào, coroutine này sẽ bắt đầu vòng quay tải – sau đó 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 sẽ thực thi khác với một hàm thông thường.
Chúng ta không cần truyền một lệnh gọi lại. Coroutine sẽ tạm ngưng cho đến khi được refreshTitle
tiếp tục. Mặc dù trông giống như một lệnh gọi hàm chặn thông thường, nhưng hàm này sẽ tự động đợi cho đến khi truy vấn mạng và cơ sở dữ liệu hoàn tất rồi mới tiếp tục mà không chặn luồng chính.
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
Các 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 một lỗi trong hàm tạm ngưng, lỗi đó sẽ được gửi đến phương thức gọi. Vì vậy, mặc dù chúng thực thi theo cách khác nhau, nhưng bạn có thể dùng các khối try/catch thông thường để xử lý chúng. Điều này rất hữu ích vì bạn có thể dựa vào tính năng hỗ trợ ngôn ngữ tích hợp để xử lý lỗi thay vì tạo tính năng xử lý lỗi tuỳ chỉnh cho mọi lệnh gọi lại.
Ngoài ra, nếu bạn khai báo một trường hợp ngoại lệ ngoài một coroutine, thì theo mặc định, coroutine đó sẽ huỷ tác vụ mẹ. Điều đó có nghĩa là bạn có thể dễ dàng huỷ nhiều việc có liên quan cùng một lúc.
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 (bắt đầu) rồi nhấn , bạn sẽ thấy một biểu tượng tải xoay tròn khi nhấn vào bất kỳ vị trí nào. Tiêu đề sẽ không thay đổi vì chúng ta chưa kết nối mạng hoặc cơ sở dữ liệu.
Trong bài tập tiếp theo, bạn sẽ cập nhật kho lưu trữ để thực sự thực hiện công việc.
Trong bài tập này, bạn sẽ tìm hiểu cách chuyển đổi luồng mà một coroutine chạy để triển khai phiên bản hoạt động của TitleRepository
.
Xem lại mã gọi lại hiện có trong refreshTitle
Mở TitleRepository.kt
rồi xem lại cách triển khai hiện có dựa trên lệnh gọi lạ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 bằng một lệnh gọi lại để truyền đạt trạng thái tải và lỗi cho phương thức gọi.
Hàm này thực hiện khá nhiều việc để triển khai thao tác làm mới.
- Chuyển sang một chuỗi tin nhắn khác bằng cách nhấn
BACKGROUND
ExecutorService
- Chạy yêu cầu mạng
fetchNextTitle
bằng phương thức chặnexecute()
. Thao tác này sẽ chạy yêu cầu mạng trong luồng hiện tại, trong trường hợp này là một trong các luồng trongBACKGROUND
. - 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ứconCompleted()
. - Nếu kết quả không thành công hoặc có một trường hợp ngoại lệ, hãy gọi phương thức onError để thông báo cho phương thức gọi về việc làm mới không thành công.
Phương thức triển khai dựa trên lệnh gọi lại này là an toàn cho luồng chính vì phương thức này sẽ không chặn luồng chính. Tuy nhiên, nó phải sử dụng một lệnh gọi lại để thông báo cho phương thức gọi khi công việc hoàn tất. Thao tác này cũng gọi các lệnh gọi lại trên luồng BACKGROUND
mà nó đã chuyển sang.
Gọi các cuộc gọi chặn từ các coroutine
Nếu không giới thiệu coroutine cho mạng hoặc cơ sở dữ liệu, chúng ta có thể làm cho mã này an toàn cho luồng chính bằng cách sử dụng coroutine. Điều này sẽ giúp chúng ta loại bỏ lệnh gọi lại và cho phép chúng ta truyền kết quả trở lại luồng đã gọi lệnh gọi lại đó ban đầu.
Bạn có thể sử dụng mẫu này bất cứ khi nào cần thực hiện công việc chặn hoặc công việc nặng về 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 bất kỳ trình điều phối nào, coroutine sẽ dùng withContext
. Việc gọi withContext
sẽ chuyển sang điều phối viên khác chỉ cho hàm lambda, sau đó quay lại điều phối viên đã gọi hàm đó cùng với kết quả của hàm lambda đó.
Theo mặc định, các coroutine Kotlin cung cấp 3 Dispatcher: Main
, IO
và Default
. Trình điều phối IO được tối ưu hoá cho công việc IO, chẳng hạn như đọc từ mạng hoặc ổ đĩa, trong khi Trình điều phối Default được tối ưu hoá cho các tác vụ cần 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)
}
}
}
Phương thức 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 vẫn đơn giản hơn một chút so với phiên bản lệnh gọi lại.
Mã này vẫn sử dụng các lệnh gọi chặn. Việc gọi execute()
và insertTitle(...)
sẽ chặn 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 ta đang chặn một trong các luồng trong trình điều phối IO. Coroutine đã gọi hàm này (có thể đang chạy trên Dispatchers.Main
) sẽ bị tạm ngưng cho đến khi lambda withContext
hoàn tất.
So với phiên bản lệnh gọi lại, có 2 điểm khác biệt quan trọng:
withContext
trả về kết quả cho Dispatcher đã gọi nó, trong trường hợp này làDispatchers.Main
. Phiên bản lệnh gọi lại đã gọi các lệnh gọi lại trên một luồng trong dịch vụ thực thiBACKGROUND
.- Người gọi không cần phải truyền một lệnh gọi lại đến hàm này. Họ có thể dựa vào thao tác tạm dừ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 việc 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 các coroutine vào Room và Retrofit.
Để tiếp tục tích hợp coroutine, chúng ta sẽ sử dụng tính năng hỗ trợ cho các hàm tạm ngưng trong phiên bản ổn định của Room và Retrofit, sau đó đơn giản hoá đáng kể 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 Room
Trước tiên, hãy mở MainDatabase.kt
và đặt insertTitle
làm một 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 như vậy, Room sẽ làm cho truy vấn của bạn trở nên an toàn cho luồng chính và tự động thực thi truy vấn đó trên một luồng 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 phải làm để sử dụng coroutine trong Room. Khá hay.
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 làm 2 việc sau:
- Thêm từ khoá xác định suspend vào hàm
- Xoá trình bao bọc
Call
khỏi loại trả về. Ở đây, chúng ta đang trả vềString
, nhưng bạn cũng có thể trả về loại phức tạp được hỗ trợ bằng JSON. Nếu vẫn muốn cung cấp quyền truy cập vàoResult
đầy đủ của retrofit, bạn có thể trả vềResult<String>
thay vìString
từ hàm tạm ngưng.
Retrofit sẽ tự động tạo các hàm tạm ngưng an toàn cho luồng chính để bạn có thể gọi trực tiếp các hàm này từ Dispatchers.Main
.
Sử dụng Room và Retrofit
Giờ đây, Room và Retrofit đều hỗ trợ các hàm tạm ngưng, nên chúng ta có thể sử dụng các hàm này từ kho lưu trữ. Mở TitleRepository.kt
và xem cách sử dụng các hàm tạm ngưng giúp đơn giản hoá đáng kể logic, ngay cả khi so với phiên bản chặn:
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)
}
}
Wow, ngắn hơn rất nhiều. Điều gì đã xảy ra? Hoá ra việc dựa vào các thao tác tạm dừng và tiếp tục giúp mã ngắn hơn nhiều. Retrofit cho phép chúng ta sử dụng các loại dữ liệu trả về như String
hoặc đối tượng User
tại đây, thay vì Call
. Bạn có thể làm như vậy một cách an toàn vì bên trong hàm tạm ngưng, Retrofit
có thể chạy yêu cầu mạng trên một luồng trong nền và tiếp tục coroutine khi lệnh gọi hoàn tất.
Thậm chí còn tốt hơn nữa là chúng tôi đã loại bỏ withContext
. Vì cả Room và Retrofit đều cung cấp các hàm tạm ngưng an toàn cho luồng chính, nên bạn có thể điều phối công việc không đồng bộ này một cách an toàn từ Dispatchers.Main
.
Khắc phục lỗi trình biên dịch
Việc chuyển sang coroutine sẽ liên quan đến việc 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 đối tượng sửa đổi suspend
trong bước này, một số lỗi trình biên dịch đã được tạo để cho 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.
Xem qua dự án và khắc phục các lỗi trình biên dịch bằng cách thay đổi hàm thành hàm tạm ngưng đã tạo. Sau đây là các giải pháp nhanh cho từng vấn đề:
TestingFakes.kt
Cập nhật các dữ liệu kiểm thử giả để hỗ trợ các đối tượng sửa đổi mới của hàm tạm ngưng.
TitleDaoFake
- Nhấn Alt+Enter để thêm đối tượng sửa đổi tạm ngưng vào tất cả các hàm trong hệ thống phân cấp
MainNetworkFake
- Nhấn Alt+Enter để thêm đối tượng sửa đổi tạm ngưng vào tất cả các hàm trong hệ thống phân cấp
- Thay thế
fetchNextTitle
bằng hàm này
override suspend fun fetchNextTitle() = result
MainNetworkCompletableFake
- Nhấn Alt+Enter để thêm đối tượng sửa đổi tạm ngưng vào tất cả các hàm trong hệ thống phân cấp
- Thay thế
fetchNextTitle
bằng hàm này
override suspend fun fetchNextTitle() = completable.await()
TitleRepository.kt
- Xoá 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 ứng dụng biên dịch, bạn sẽ thấy ứng dụng đang tải dữ liệu bằng cách sử dụng coroutine từ ViewModel đến Room và Retrofit!
Xin chúc mừng, bạn đã hoàn toàn chuyển đổi ứng dụng này sang dùng coroutine! Để kết thúc, chúng ta sẽ nói một chút về cách kiểm thử những gì chúng ta vừa làm.
Trong bài tập này, bạn sẽ viết một bài kiểm thử gọi trực tiếp hàm suspend
.
Vì refreshTitle
được hiển thị dưới dạng một API công khai, nên API này sẽ được kiểm thử trực tiếp, cho biết cách gọi các hàm coroutine từ các quy trình kiểm thử.
Sau đâ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 một chương trình kiểm thử gọi một hàm tạm ngưng
Mở TitleRepositoryTest.kt
trong thư mục test
có 2 việc CẦN LÀM.
Hãy thử gọi refreshTitle
từ whenRefreshTitleSuccess_insertsRows
kiểm thử đầu tiên.
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
subject.refreshTitle()
}
Vì refreshTitle
là một hàm suspend
nên Kotlin không biết cách gọi hàm này, trừ phi hàm này được gọi từ một coroutine hoặc một hàm tạm ngưng khác. Do đó, bạn sẽ gặp phải lỗi trình biên dịch như "Chỉ nên gọi hàm tạm ngưng refreshTitle từ một coroutine hoặc một hàm tạm ngưng khác."
Trình chạy kiểm thử không biết gì về coroutine nên chúng ta không thể thiết lập kiểm thử này thành một hàm tạm ngưng. Chúng ta có thể launch
một coroutine bằng cách sử dụng CoroutineScope
như trong ViewModel
, tuy nhiên, các kiểm thử cần chạy coroutine cho đến khi hoàn tất trước khi trả về. Sau khi một hàm kiểm thử trả về, quá trình kiểm thử sẽ kết thúc. Cá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. Do đó, để kiểm thử mã không đồng bộ, bạn cần có cách nào đó để yêu cầu kiểm thử đợi cho đến khi coroutine của bạn hoàn tất. Vì launch
là một lệnh gọi không chặn, tức là lệnh gọi này sẽ trả về ngay lập tức và có thể tiếp tục chạy một coroutine sau khi hàm trả về – lệnh gọi này không thể dùng trong các kiểm thử. 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
}
Đôi khi, kiểm thử này sẽ không thành công. Lệnh gọi đến launch
sẽ trả về 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 kiểm thử. Bài kiểm thử không có cách nào biết liệu refreshTitle
đã chạy hay chưa – và mọi câu lệnh khẳng định như kiểm tra xem cơ sở dữ liệu đã được cập nhật hay chưa đều sẽ không ổn định. Ngoài ra, nếu refreshTitle
gửi một trường hợp ngoại lệ, thì trường hợp đó sẽ không được gửi trong ngăn xếp lệnh gọi kiểm thử. Thay vào đó, nó sẽ được chuyển vào trình xử lý ngoại lệ chưa được xử lý của GlobalScope
.
Thư viện kotlinx-coroutines-test
có hàm runBlockingTest
. Hàm này chặn nội dung trong khi gọi các hàm tạm ngưng. Khi runBlockingTest
gọi một hàm tạm ngưng hoặc launches
một coroutine mới, hàm này sẽ thực thi 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ẽ gửi lại các ngoại lệ chưa được xử lý cho bạn. Điều này giúp bạn dễ dàng kiểm thử khi một coroutine đang gửi một ngoại lệ.
Triển khai một chương trình kiểm thử bằng một coroutine
Gói lệnh gọi đến refreshTitle
bằng runBlockingTest
và xoá 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")
}
Bài kiểm thử này sử dụng các dữ liệu giả được cung cấp để kiểm tra xem "OK" có được refreshTitle
chèn vào cơ sở dữ liệu hay không.
Khi kiểm thử gọi runBlockingTest
, kiểm thử sẽ chặn cho đến khi coroutine do runBlockingTest
bắt đầu hoàn tất. Sau đó, bên trong, khi chúng ta gọi refreshTitle
, nó sẽ sử dụng cơ chế tạm ngưng và tiếp tục thông thường để chờ hàng trong cơ sở dữ liệu được thêm vào dữ liệu giả của chúng ta.
Sau khi coroutine kiểm thử hoàn tất, runBlockingTest
sẽ trả về.
Viết bài kiểm thử thời gian chờ
Chúng ta muốn thêm một thời gian chờ ngắn vào yêu cầu mạng. Trước tiên, hãy viết bài kiểm thử rồi triển khai thời gian chờ. Tạo một bài kiểm tra 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)
}
Bài kiểm thử này sử dụng MainNetworkCompletableFake
giả được cung cấp. Đây là một mạng giả được thiết kế để tạm ngưng các phương thức gọi cho đến khi bài kiểm thử tiếp tục. Khi refreshTitle
cố gắng tạo một yêu cầu mạng, yêu cầu đó sẽ bị treo mãi mãi vì chúng ta muốn kiểm thử thời gian chờ.
Sau đó, nó sẽ khởi chạy một coroutine riêng biệt để gọi refreshTitle
. Đây là một phần quan trọng của việc kiểm thử thời gian chờ, thời gian chờ phải xảy ra trong một coroutine khác với coroutine mà 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ẽ tăng thời gian thêm 5 giây và khiến coroutine kia hết thời gian chờ.
Đây là một bài kiểm thử hết thời gian hoàn chỉnh và sẽ vượt qua khi chúng tôi triển khai thời gian chờ.
Chạy ứng dụng 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à tính năng này sẽ không cho phép bạn rò rỉ coroutine sau khi quá trình kiểm thử 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 khởi chạy của chúng ta) vào cuối quá trình kiểm thử, thì quá trình kiểm thử sẽ không thành công.
Thêm thời gian chờ
Mở TitleRepository
và thêm thời gian chờ 5 giây vào quá trình tìm nạp mạng. Bạn có thể thực hiện việc này bằng cách sử 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 kiểm thử. Khi chạy kiểm thử, bạn sẽ thấy tất cả kiểm thử đều đạt!
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 bằng cách sử dụng coroutine.
Trong bài tập này, bạn sẽ tái cấu trúc refreshTitle
trong MainViewModel
để sử dụng một hàm tải dữ liệu chung. Lớp học lập trình này sẽ hướng dẫn bạn cách tạo các hàm bậc cao sử dụng coroutine.
Việc triển khai refreshTitle
hiện tại vẫn hoạt động, nhưng chúng ta có thể tạo một coroutine tải dữ liệu chung luôn hiển thị chỉ báo tiến trình. Điều này có thể hữu ích trong một 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 luôn hiển thị.
Xem xét việc triển khai hiện tại, mọi dòng ngoại trừ repository.refreshTitle()
đều là mã chuẩn để hiện chỉ báo xoay và hiển thị lỗi.
// 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
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 này.
MainViewModel.kt
fun refreshTitle() {
launchDataLoad {
repository.refreshTitle()
}
}
Bằng cách trừu tượng hoá logic xung quanh việc hiển thị một chỉ báo tải và hiển thị lỗi, chúng ta đã đơn giản hoá mã thực tế cần thiết để tải dữ liệu. Việc hiện một chỉ báo xoay hoặc hiển thị lỗi là điều dễ dàng khái quát hoá cho mọi hoạt động tải dữ liệu, trong khi nguồn và đích dữ liệu thực tế cần được chỉ định mỗi lần.
Để tạo lớp trừu tượng này, launchDataLoad
sẽ lấy một đối số block
là một lambda tạm ngưng. Một 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 các trình tạo coroutine launch
và runBlocking
mà chúng ta đã sử dụng trong lớp học lập trình này.
// suspend lambda
block: suspend () -> Unit
Để tạo một lambda tạm ngưng, hãy bắt đầu bằng từ khoá suspend
. Mũi tên hàm và kiểu dữ liệu trả về Unit
hoàn tất khai báo.
Bạn không thường xuyên phải khai báo lambda tạm ngưng của riêng mình, nhưng chúng có thể hữu ích để tạo các trừu tượng như thế 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 lựa chọn cho thao tác cần thiết trong nền có thể trì hoãn trên Android. Bài tập này hướng dẫn bạn cách tích hợp WorkManager với coroutine. WorkManager là một thư viện đơn giản, linh hoạt và có khả năng tương thích dành cho công việc có thể trì hoãn ở chế độ nền. WorkManager là giải pháp được đề xuất cho những trường hợp sử dụng này trên Android.
WorkManager là một phần của Android Jetpack và là một Thành phần kiến trúc cho công việc ở chế độ nền cần kết hợp khả năng thực thi theo cơ hội và khả năng thực thi được đảm bảo. Thực thi theo cơ hội có nghĩa là WorkManager sẽ thực hiện công việc trong nền của bạn ngay khi có thể. Thực thi được đảm bảo tức là WorkManager sẽ xử lý logic để bắt đầu công việc của bạn trong nhiều tình huống, ngay cả khi bạn rời khỏi ứng dụng.
Do đó, WorkManager là lựa chọn phù hợp cho những tác vụ phải hoàn thành.
Dưới đây là một số ví dụ về tác vụ sử dụng hiệu quả 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ộ hoá dữ liệu cục bộ với mạng
Sử dụng coroutine với WorkManager
WorkManager cung cấp nhiều cách triển khai lớp cơ sở ListanableWorker
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, sau khi đã chuyển đổi cơ sở mã để sử dụng coroutine và hàm tạm ngưng, cách tốt nhất để sử dụng WorkManager là thông qua lớp CoroutineWorker
. Lớp này cho phép xác định hàm doWork()
dưới dạng hàm tạm ngưng.
Để bắt đầu, hãy mở RefreshMainDataWork
. Nó đã 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ả thích hợp!
Sau khi bạn hoàn tất việc cần làm, mã sẽ có dạ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à một 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 Executor đượ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 phần coroutineContext
(theo mặc định là Dispatchers.Default
).
Kiểm thử CoroutineWorker
Không có cơ sở mã nào hoàn chỉnh mà không có kiểm thử.
WorkManager cung cấp một số cách để kiểm thử các lớp Worker
. Để tìm hiểu thêm về cơ sở hạ tầng kiểm thử ban đầu, bạn có thể đọc tài liệu.
WorkManager phiên bản 2.1 ra mắt một bộ API mới để hỗ trợ cách đơn giản hơn nhằm kiểm thử các lớp ListenableWorker
và do đó là CoroutineWorker. Trong mã của mình, chúng ta sẽ sử dụng một trong những API mới này: TestListenableWorkerBuilder
.
Để thêm kiểm thử 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 đến phần kiểm thử, chúng ta sẽ cho WorkManager
biết về nhà máy để có thể chèn mạng giả.
Bản thân bài kiểm thử sử dụng TestListenableWorkerBuilder
để tạo worker mà sau đó chúng ta có thể chạy bằng cách gọi phương thức startWork()
.
WorkManager chỉ là một ví dụ về cách sử dụng coroutine để đơn giản hoá thiết kế API.
Trong lớp học lập trình này, chúng ta đã tìm hiểu những kiến thức cơ bản mà bạn cần để 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 các ứng dụng Android từ cả giao diện người dùng và các tác vụ WorkManager để đơn giản hoá quy trình 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 luồng chính. - Và cách huỷ tất cả coroutine khi
ViewModel
hoàn tất.
Đối với hoạt động kiểm thử mã dựa trên coroutine, chúng ta đã đề cập đến cả hai bằng cách kiểm thử hành vi cũng như trực tiếp gọi các hàm suspend
từ các quy trình kiểm thử.
Tìm hiểu thêm
Hãy xem lớp học lập trình "Coroutine nâng cao với LiveData và Flow Kotlin" để tìm hiểu thêm về cách sử dụng coroutine nâng cao trên Android.
Các coroutine của Kotlin có nhiều tính năng mà lớp học lập trình này chưa đề cập đến. Nếu bạn muốn tìm hiểu thêm về coroutine của Kotlin, hãy đọc các hướng dẫn về coroutine do JetBrains xuất bản. Bạn cũng có thể xem bài viết "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 mẫu sử dụng coroutine trên Android.