Trong khi lớp giao diện người dùng chứa trạng thái liên quan đến giao diện người dùng và logic giao diện người dùng, thì lớp dữ liệu chứa dữ liệu ứng dụng và logic kinh doanh. Logic kinh doanh là những gì mang lại giá trị cho ứng dụng của bạn—được tạo ra từ các quy tắc kinh doanh trong thế giới thực xác định cách tạo, lưu trữ và thay đổi dữ liệu ứng dụng.
Việc phân tách các mối quan ngại này cho phép sử dụng lớp dữ liệu trên nhiều màn hình, chia sẻ thông tin giữa các phần khác nhau của ứng dụng và tái tạo logic kinh doanh bên ngoài giao diện người dùng để thử nghiệm đơn vị. Để biết thêm thông tin về các lợi ích của lớp dữ liệu, hãy xem trang Tổng quan về cấu trúc.
Kiến trúc lớp dữ liệu
Lớp dữ liệu được tạo thành từ các kho lưu trữ, mỗi kho dữ liệu có thể chứa từ 0 đến nhiều
nguồn dữ liệu. Bạn nên tạo một lớp kho lưu trữ cho từng loại dữ liệu
khác nhau mà bạn xử lý trong ứng dụng. Ví dụ: bạn có thể tạo một lớp MoviesRepository
cho dữ liệu liên quan đến phim hoặc một lớp PaymentsRepository
cho dữ liệu
liên quan đến các khoản thanh toán.
![Trong một cấu trúc thông thường, kho lưu trữ của lớp dữ liệu cung cấp dữ liệu cho phần còn lại của ứng dụng và phụ thuộc vào các nguồn dữ liệu.](https://developer.android.com/static/topic/libraries/architecture/images/mad-arch-data-overview.png?authuser=5&hl=vi)
Các lớp kho lưu trữ chịu trách nhiệm về:
- Hiển thị dữ liệu cho phần còn lại của ứng dụng.
- Tập trung các thay đổi vào dữ liệu.
- Giải quyết xung đột giữa nhiều nguồn dữ liệu.
- Tóm tắt các nguồn dữ liệu từ phần còn lại của ứng dụng.
- Chứa logic nghiệp vụ.
Mỗi lớp nguồn dữ liệu nên có trách nhiệm làm việc với chỉ một nguồn dữ liệu duy nhất, có thể là một tệp, nguồn mạng hoặc cơ sở dữ liệu cục bộ. Các lớp nguồn dữ liệu là cầu nối giữa ứng dụng và hệ thống để thao tác dữ liệu.
Các lớp khác trong hệ thống phân cấp không được trực tiếp truy cập vào các nguồn dữ liệu; điểm truy cập cho lớp dữ liệu luôn là các lớp kho lưu trữ. Các lớp chủ thể trạng thái (xem hướng dẫn về lớp giao diện người dùng) hoặc các lớp trường hợp sử dụng (xem hướng dẫn về lớp trên miền) không được có nguồn dữ liệu như một phần phụ thuộc trực tiếp. Việc sử dụng các lớp kho lưu trữ làm điểm truy cập cho phép các lớp khác nhau của kiến trúc mở rộng quy mô độc lập.
Dữ liệu mà lớp này hiển thị phải là bất biến để các lớp khác không thể củng cố dữ liệu. Việc này sẽ có nguy cơ khiến các giá trị của lớp bị chuyển sang trạng thái không nhất quán. Dữ liệu bất biến cũng có thể được xử lý một cách an toàn thông qua nhiều luồng. Hãy xem mục về luồng này để biết thêm chi tiết.
Sau khi áp dụng các phương pháp hay nhất về thao tác chèn phần phụ thuộc, kho lưu trữ sẽ lấy nguồn dữ liệu làm các phần phụ thuộc trong hàm tạo:
class ExampleRepository(
private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
private val exampleLocalDataSource: ExampleLocalDataSource // database
) { /* ... */ }
Hiển thị các API
Các lớp trong lớp dữ liệu thường hiển thị các hàm để thực hiện lệnh gọi một lần: Tạo, Đọc, Cập nhật và Xóa (CRUD) hoặc để được thông báo về sự thay đổi dữ liệu theo thời gian. Lớp dữ liệu phải hiển thị những nội dung sau cho từng trường hợp sau:
- Thao tác một lần: Lớp dữ liệu cần hiển thị các hàm tạm ngưng trong Kotlin; và đối với ngôn ngữ lập trình Java, lớp dữ liệu cần hiển thị các hàm cung cấp lệnh gọi lại để thông báo kết quả của thao tác hoặc các loại
Single
,Maybe
hayCompletable
của RxJava. - Để nhận thông báo về các thay đổi của dữ liệu theo thời gian: Lớp dữ liệu cần hiển thị
các luồng trong Kotlin; và đối với ngôn ngữ lập trình Java,
lớp dữ liệu cần hiển thị một lệnh gọi lại phát ra dữ liệu mới hoặc loại RxJava
Observable
hoặcFlowable
.
class ExampleRepository(
private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
private val exampleLocalDataSource: ExampleLocalDataSource // database
) {
val data: Flow<Example> = ...
suspend fun modifyData(example: Example) { ... }
}
Quy ước đặt tên trong hướng dẫn này
Trong hướng dẫn này, các lớp kho lưu trữ được đặt tên theo dữ liệu mà chúng chịu trách nhiệm. Quy ước như sau:
loại dữ liệu + Kho lưu trữ.
Ví dụ: NewsRepository
, MoviesRepository
hoặc PaymentsRepository
.
Các lớp nguồn dữ liệu được đặt tên theo dữ liệu mà các lớp này chịu trách nhiệm và nguồn mà chúng sử dụng. Quy ước như sau:
loại dữ liệu + loại nguồn + DataSource.
Đối với loại dữ liệu, hãy sử dụng Điều khiển từ xa hoặc Địa phương chung hơn vì
cách triển khai có thể thay đổi. Ví dụ: NewsRemoteDataSource
hoặc
NewsLocalDataSource
. Để cụ thể hơn trong trường hợp nguồn là quan trọng, hãy sử dụng
loại nguồn. Ví dụ: NewsNetworkDataSource
hoặc
NewsDiskDataSource
.
Không đặt tên nguồn dữ liệu dựa trên thông tin triển khai—ví dụ:
UserSharedPreferencesDataSource
—vì các kho lưu trữ mà sử dụng nguồn dữ liệu đó
sẽ không biết cách lưu dữ liệu. Nếu tuân theo quy tắc này, bạn có thể thay đổi
cách triển khai nguồn dữ liệu (ví dụ: di chuyển từ
SharedPreferences tới
DataStore ) mà không ảnh hưởng đến
lớp gọi nguồn đó.
Nhiều cấp độ của các kho lưu trữ
Trong một số trường hợp liên quan đến các yêu cầu kinh doanh phức tạp hơn, một kho lưu trữ có thể cần phải phụ thuộc vào những kho lưu trữ khác. Điều này có thể là do dữ liệu liên quan là tổng hợp từ nhiều nguồn dữ liệu hoặc do trách nhiệm cần được đóng gói trong một lớp kho lưu trữ khác.
Ví dụ: một kho lưu trữ xử lý dữ liệu xác thực người dùng,
UserRepository
, có thể phụ thuộc vào các kho lưu trữ khác như LoginRepository
và RegistrationRepository
để đáp ứng các yêu cầu của kho lưu trữ đó.
![Trong ví dụ này, UserRepository phụ thuộc vào hai lớp kho lưu trữ khác:
LoginRepository, phụ thuộc vào các nguồn dữ liệu đăng nhập khác; và
RegistrationRepository, phụ thuộc vào các nguồn dữ liệu đăng ký khác.](https://developer.android.com/static/topic/libraries/architecture/images/mad-arch-data-multiple-repos.png?authuser=5&hl=vi)
Nguồn đáng tin cậy
Điều quan trọng là mỗi kho lưu trữ xác định được một nguồn dữ liệu đáng tin cậy. Nguồn đáng tin cậy luôn chứa dữ liệu nhất quán, chính xác và mới nhất. Trên thực tế, dữ liệu do kho lưu trữ hiển thị phải luôn là dữ liệu đến trực tiếp từ nguồn đáng tin cậy.
Nguồn đáng tin cậy có thể là nguồn dữ liệu – ví dụ: cơ sở dữ liệu – hoặc thậm chí là một bộ nhớ đệm trong bộ nhớ có thể thuộc kho lưu trữ. Các kho lưu trữ kết hợp nhiều nguồn dữ liệu khác nhau và giải quyết mọi xung đột tiềm ẩn giữa các nguồn dữ liệu để cập nhật nguồn đáng tin cậy thường xuyên hoặc do sự kiện nhập của người dùng.
Các kho lưu trữ khác nhau trong ứng dụng của bạn có thể có các nguồn đáng tin cậy khác nhau. Ví dụ: lớp LoginRepository
có thể sử dụng bộ nhớ đệm làm nguồn đáng tin cậy và lớp PaymentsRepository
có thể sử dụng nguồn dữ liệu mạng.
Để cung cấp hỗ trợ ưu tiên ngoại tuyến, một nguồn dữ liệu địa phương—chẳng hạn như cơ sở dữ liệu—là nguồn đáng tin cậy được đề xuất.
Luồng
Tác vụ gọi nguồn và kho lưu trữ cần an toàn cho luồng chính – an toàn để gọi từ luồng chính. Các lớp này chịu trách nhiệm chuyển phương thức thực thi logic sang luồng phù hợp khi thực hiện các thao tác chặn dài hạn. Ví dụ: nguồn dữ liệu phải an toàn cho luồng chính để đọc từ một tệp hoặc để kho lưu trữ thực hiện việc lọc tốn kém trên một danh sách lớn.
Xin lưu ý rằng hầu hết các nguồn dữ liệu đều đã cung cấp các API an toàn cho luồng chính, chẳng hạn như các lệnh gọi phương thức tạm ngưng do Room, Retrofit hoặc Ktor cung cấp. Kho lưu trữ của bạn có thể tận dụng các API này khi có sẵn.
Để tìm hiểu thêm về xử lý luồng, hãy xem hướng dẫn xử lý nền. Đối với người dùng Kotlin, họ nên sử dụng coroutine. Hãy xem phần Chạy các tác vụ trên Android trong luồng nền để biết các tuỳ chọn đề xuất cho ngôn ngữ lập trình Java.
Lifecycle
Phiên bản của các lớp trong lớp dữ liệu vẫn sẽ còn trong bộ nhớ miễn là chúng có thể truy cập được từ thư mục thu thập rác gốc—thường là được tham chiếu từ các đối tượng khác trong ứng dụng của bạn.
Nếu một lớp chứa dữ liệu trong bộ nhớ—ví dụ: bộ nhớ đệm—bạn có thể muốn sử dụng lại cùng một phiên bản của lớp đó trong một khoảng thời gian cụ thể. Đây cũng được gọi là vòng đời của phiên bản lớp.
Nếu trách nhiệm của lớp đó là quan trọng đối với toàn bộ ứng dụng, bạn có thể
đặt phạm vi một phiên bản của lớp đó cho lớp Application
. Điều này làm cho
phiên bản này tuân theo vòng đời của ứng dụng. Ngoài ra, nếu bạn chỉ
cần sử dụng lại cùng một phiên bản trong một quy trình cụ thể trong ứng dụng—ví dụ:
quy trình đăng ký hoặc đăng nhập—thì bạn nên đặt phạm vi của phiên bản đó cho lớp
sở hữu vòng đời của quy trình đó. Ví dụ: bạn có thể đặt phạm vi
RegistrationRepository
chứa dữ liệu trong bộ nhớ thành
RegistrationActivity
hoặc biểu đồ
điều hướng của
quy trình đăng ký ,
Vòng đời của từng phiên bản là một yếu tố quan trọng để quyết định cách cung cấp các phần phụ thuộc trong ứng dụng. Bạn nên làm theo các phương pháp hay nhất về cách chèn phần phụ thuộc vào những phần phụ thuộc mà bạn quản lý và có thể được đặt phạm vi vào các vùng chứa phần phụ thuộc. Để tìm hiểu thêm về xác định phạm vi trong Android, hãy xem phần Xác định phạm vi trong Android và Hilt trên blog.
Đại diện cho các mô hình kinh doanh
Các mô hình dữ liệu mà bạn muốn hiển thị từ lớp dữ liệu có thể là một tập hợp con chứa thông tin mà bạn nhận được từ nhiều nguồn dữ liệu. Lý tưởng nhất là các nguồn dữ liệu khác nhau—cả mạng và cục bộ—chỉ cần trả về thông tin mà ứng dụng cần; nhưng điều này thường không đúng.
Ví dụ: hãy tưởng tượng một máy chủ API Tin tức không chỉ trả về thông tin bài viết mà còn chỉnh sửa lịch sử, nhận xét của người dùng và một số siêu dữ liệu:
data class ArticleApiModel(
val id: Long,
val title: String,
val content: String,
val publicationDate: Date,
val modifications: Array<ArticleApiModel>,
val comments: Array<CommentApiModel>,
val lastModificationDate: Date,
val authorId: Long,
val authorName: String,
val authorDateOfBirth: Date,
val readTimeMin: Int
)
Ứng dụng chỉ hiển thị nội dung của bài viết trên màn hình cùng với thông tin cơ bản về tác giả nên không cần nhiều thông tin đến vậy về bài viết. Bạn nên tách riêng các lớp mô hình và yêu cầu
kho lưu trữ chỉ hiển thị dữ liệu mà các lớp khác trong hệ thống
phân cấp yêu cầu. Ví dụ: dưới đây là cách bạn có thể cắt bớt ArticleApiModel
từ
mạng để hiển thị lớp mô hình Article
cho lớp miền và
giao diện người dùng:
data class Article(
val id: Long,
val title: String,
val content: String,
val publicationDate: Date,
val authorName: String,
val readTimeMin: Int
)
Việc tách các lớp mô hình có lợi như sau:
- Cách này giúp tiết kiệm bộ nhớ ứng dụng bằng cách giảm dữ liệu xuống chỉ còn những dữ liệu cần thiết.
- Ứng dụng này điều chỉnh các loại dữ liệu bên ngoài cho phù hợp với các loại dữ liệu mà ứng dụng sử dụng—ví dụ: ứng dụng của bạn có thể sử dụng một loại dữ liệu khác để thể hiện ngày tháng.
- Trường hợp này giúp tách biệt đáng kể các mối quan ngại–ví dụ: các thành viên trong một nhóm lớn có thể làm việc riêng lẻ trên các mạng và các lớp của giao diện người dùng của một tính năng nếu lớp mô hình được xác định trước.
Bạn cũng có thể mở rộng phương pháp này và xác định các lớp mô hình riêng biệt trong các phần khác của cấu trúc ứng dụng, ví dụ: trong các lớp nguồn dữ liệu và ViewModels. Tuy nhiên, điều này yêu cầu bạn phải xác định thêm các lớp và logic mà bạn nên ghi nhận và kiểm tra đúng cách. Bạn nên tạo ít nhất một mô hình mới trong mọi trường hợp mà nguồn dữ liệu nhận được dữ liệu không khớp với thông tin mà phần còn lại của ứng dụng dự kiến.
Các loại thao tác với dữ liệu
Lớp dữ liệu có thể xử lý các loại thao tác khác nhau tùy theo mức độ quan trọng của chúng: thao tác do giao diện người dùng điều hướng, do ứng dụng điều hướng và do hoạt động kinh doanh điều hướng.
Thao tác do giao diện người dùng điều hướng
Các thao tác do giao diện người dùng điều hướng chỉ có liên quan khi người dùng đang sử dụng một màn hình cụ thể và các thao tác đó sẽ bị huỷ khi người dùng rời khỏi màn hình đó. Ví dụ: hiển thị một số dữ liệu thu thập được từ cơ sở dữ liệu đó.
Thao tác do giao diện người dùng điều hướng thường do lớp giao diện người dùng kích hoạt và tuân theo vòng đời của lệnh gọi, ví dụ: vòng đời của ViewModel. Hãy xem mục Tạo yêu cầu mạng để xem ví dụ về thao tác do giao diện người dùng điều hướng.
Thao tác do ứng dụng điều hướng
Các thao tác do ứng dụng điều hướng phù hợp với điều kiện ứng dụng đang mở. Nếu ứng dụng bị đóng hoặc quy trình bị huỷ, thì các thao tác này sẽ bị huỷ. Một ví dụ là lưu vào bộ nhớ đệm kết quả của một yêu cầu mạng để có thể sử dụng sau này khi cần thiết. Xem phần Triển khai lưu vào bộ nhớ đệm dữ liệu trong bộ nhớ để tìm hiểu thêm.
Các thao tác này thường tuân theo vòng đời của lớp Application
hoặc
lớp dữ liệu. Ví dụ: hãy xem mục Làm cho thao tác hoạt động lâu hơn
màn hình.
Thao tác do hoạt động kinh doanh điều hướng
Bạn không thể huỷ các thao tác do hoạt động kinh doanh điều hướng. Chúng cần vẫn tồn tại sau quá trình xử lý bị gián đoạn. Một ví dụ là việc hoàn tất tải ảnh lên mà người dùng muốn đăng trong hồ sơ của họ.
Bạn nên sử dụng WorkManager cho các thao tác do hoạt động kinh doanh điều hướng. Hãy xem mục Lên lịch tác vụ bằng WorkManager để tìm hiểu thêm.
Lỗi hiển thị
Các tương tác với kho lưu trữ và nguồn dữ liệu có thể thành công hoặc đưa ra
một ngoại lệ khi xảy ra lỗi. Đối với các coroutine và quy trình, bạn nên
sử dụng cơ chế xử lý
lỗi tích hợp sẵn của Kotlin. Để
lỗi có thể do hàm tạm ngưng kích hoạt, hãy sử dụng khối try/catch
khi
phù hợp; còn trong luồng, hãy sử dụng
catch
toán tử. Với cách tiếp cận này, lớp giao diện người dùng dự kiến sẽ xử lý các trường hợp ngoại lệ khi
gọi lớp dữ liệu.
Lớp dữ liệu có thể hiểu và xử lý nhiều loại lỗi, cũng như hiển thị các lỗi đó bằng cách sử dụng các trường hợp ngoại lệ tuỳ chỉnh, ví dụ: UserNotAuthenticatedException
.
Để tìm hiểu thêm về lỗi trong coroutine, hãy xem phần Ngoại lệ trong coroutine trên blog.
Tác vụ chung
Các phần sau đây trình bày ví dụ về cách sử dụng và thiết kế cấu trúc lớp dữ liệu để thực hiện một số tác vụ nhất định trong các ứng dụng Android. Các ví dụ dựa trên ứng dụng Tin tức điển hình được đề cập trước đây trong hướng dẫn.
Gửi yêu cầu kết nối mạng
Yêu cầu mạng là một trong những tác vụ phổ biến nhất mà một ứng dụng Android có thể
thực hiện. Ứng dụng Tin tức cần hiển thị cho người dùng những tin tức mới nhất được
tìm nạp từ mạng. Do đó, ứng dụng này cần có một lớp nguồn dữ liệu để quản lý
các thao tác mạng: NewsRemoteDataSource
. Để hiển thị thông tin cho phần còn lại của ứng dụng, hệ thống sẽ tạo một kho lưu trữ mới xử lý các thao tác đối với dữ liệu tin tức được tạo: NewsRepository
.
Yêu cầu là bạn phải luôn cập nhật tin tức mới nhất khi người dùng mở màn hình. Do đó, đây là một thao tác do giao diện người dùng điều hướng.
Tạo nguồn dữ liệu
Nguồn dữ liệu cần hiển thị một hàm trả về tin tức mới nhất: danh sách
các phiên bản ArticleHeadline
. Nguồn dữ liệu cần cung cấp một cách an toàn cho luồng chính để
nhận tin tức mới nhất từ mạng. Do đó, cần phải
nhận một phần phụ thuộc trên CoroutineDispatcher
hoặc Executor
để chạy tác vụ đó.
Gửi yêu cầu kết nối mạng là một lệnh gọi một lần do phương thức fetchLatestNews()
mới xử lý:
class NewsRemoteDataSource(
private val newsApi: NewsApi,
private val ioDispatcher: CoroutineDispatcher
) {
/**
* Fetches the latest news from the network and returns the result.
* This executes on an IO-optimized thread pool, the function is main-safe.
*/
suspend fun fetchLatestNews(): List<ArticleHeadline> =
// Move the execution to an IO-optimized thread since the ApiService
// doesn't support coroutines and makes synchronous requests.
withContext(ioDispatcher) {
newsApi.fetchLatestNews()
}
}
// Makes news-related network synchronous requests.
interface NewsApi {
fun fetchLatestNews(): List<ArticleHeadline>
}
Giao diện NewsApi
ẩn hoạt động triển khai API mạng máy khách; việc này không
ảnh hưởng đến việc giao diện có được
Retrofit hoặc
HttpURLConnection
hỗ trợ hay không. Việc dựa vào
giao diện giúp các nội dung triển khai API có thể tuỳ chỉnh trong ứng dụng của bạn.
Tạo kho lưu trữ
Vì không cần thêm logic trong lớp kho lưu trữ cho tác vụ này,
NewsRepository
đóng vai trò là proxy cho nguồn dữ liệu mạng. Những lợi ích của
việc thêm lớp trừu tượng bổ sung này sẽ được giải thích trong mục lưu vào bộ nhớ
đệm dữ liệu trong bộ nhớ.
// NewsRepository is consumed from other layers of the hierarchy.
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource
) {
suspend fun fetchLatestNews(): List<ArticleHeadline> =
newsRemoteDataSource.fetchLatestNews()
}
Để tìm hiểu cách sử dụng lớp kho lưu trữ trực tiếp từ lớp giao diện người dùng, hãy xem hướng dẫn về lớp giao diện người dùng.
Triển khai lưu vào bộ nhớ đệm dữ liệu trong bộ nhớ
Giả sử một yêu cầu mới được đưa ra cho ứng dụng News (Tin tức): khi người dùng mở màn hình, tin tức đã lưu vào bộ nhớ đệm phải hiển thị cho người dùng nếu trước đó, người dùng đã thực hiện yêu cầu. Nếu không, ứng dụng sẽ tạo một yêu cầu mạng để tìm nạp tin tức mới nhất.
Dựa trên yêu cầu mới này, ứng dụng phải lưu giữ những tin tức mới nhất trong bộ nhớ trong khi người dùng mở ứng dụng. Do đó, đây là một thao tác do ứng dụng điều hướng.
Bộ nhớ đệm
Bạn có thể lưu giữ dữ liệu trong khi người dùng đang ở trong ứng dụng của bạn bằng cách thêm tính năng lưu vào bộ nhớ đệm dữ liệu trong bộ nhớ. Bộ nhớ đệm được dùng để lưu một số thông tin trong bộ nhớ trong một khoảng thời gian cụ thể—trong trường hợp này, miễn là người dùng vẫn ở trong ứng dụng. Các nội dung triển khai bộ nhớ đệm có thể có nhiều dạng khác nhau. Nó có thể khác nhau từ một biến đơn giản đến một lớp phức tạp hơn giúp bảo vệ khỏi các thao tác đọc/ghi trên nhiều chuỗi. Tuỳ thuộc vào trường hợp sử dụng, bạn có thể triển khai lưu vào bộ nhớ đệm trong kho lưu trữ hoặc các lớp nguồn dữ liệu.
Lưu vào bộ nhớ đệm kết quả của yêu cầu mạng
Để đơn giản, NewsRepository
sử dụng một biến có thể thay đổi để lưu vào bộ nhớ đệm các tin tức
mới nhất. Để bảo vệ lượt đọc và ghi từ nhiều luồng,
Mutex
sẽ được sử dụng. Để tìm hiểu thêm về trạng thái và tính năng đồng thời có thể thay đổi, hãy xem
tài liệu
về Kotlin.
Hoạt động triển khai sau đây sẽ lưu vào bộ nhớ đệm tin tức mới nhất trong một biến ở kho lưu trữ được bảo vệ chống ghi bằng Mutex
. Nếu kết quả của yêu cầu
mạng thành công, thì dữ liệu sẽ được gán cho biến latestNews
.
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource
) {
// Mutex to make writes to cached values thread-safe.
private val latestNewsMutex = Mutex()
// Cache of the latest news got from the network.
private var latestNews: List<ArticleHeadline> = emptyList()
suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> {
if (refresh || latestNews.isEmpty()) {
val networkResult = newsRemoteDataSource.fetchLatestNews()
// Thread-safe write to latestNews
latestNewsMutex.withLock {
this.latestNews = networkResult
}
}
return latestNewsMutex.withLock { this.latestNews }
}
}
Làm cho thao tác hoạt động lâu hơn màn hình
Nếu người dùng rời khỏi màn hình trong khi yêu cầu kết nối mạng đang diễn ra, thì yêu cầu đó sẽ bị huỷ và kết quả sẽ không được lưu vào bộ nhớ đệm. NewsRepository
không được sử dụng CoroutineScope
của lệnh gọi để thực hiện logic này. Thay vào đó,
NewsRepository
nên sử dụng CoroutineScope
gắn với vòng đời của lệnh gọi.
Việc tìm nạp tin tức mới nhất phải là một thao tác do ứng dụng điều hướng.
Để làm theo các phương pháp hay nhất về tính năng chèn phần phụ thuộc, NewsRepository
sẽ nhận được
phạm vi dưới dạng thông số trong hàm tạo thay vì tự tạo
CoroutineScope
riêng. Vì kho lưu trữ sẽ thực hiện hầu hết công việc trong
chuỗi nền, nên bạn phải định cấu hình CoroutineScope
bằng
Dispatchers.Default
hoặc với nhóm chuỗi riêng.
class NewsRepository(
...,
// This could be CoroutineScope(SupervisorJob() + Dispatchers.Default).
private val externalScope: CoroutineScope
) { ... }
Do NewsRepository
đã sẵn sàng thực hiện các thao tác do ứng dụng điều hướng với
CoroutineScope
bên ngoài, nên kho lữu trữ này phải thực hiện lệnh gọi đến nguồn dữ liệu và lưu
kết quả bằng một coroutine mới bắt đầu bằng phạm vi đó:
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource,
private val externalScope: CoroutineScope
) {
/* ... */
suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> {
return if (refresh) {
externalScope.async {
newsRemoteDataSource.fetchLatestNews().also { networkResult ->
// Thread-safe write to latestNews.
latestNewsMutex.withLock {
latestNews = networkResult
}
}
}.await()
} else {
return latestNewsMutex.withLock { this.latestNews }
}
}
}
async
được dùng để bắt đầu coroutine trong phạm vi bên ngoài. await
được gọi
trên coroutine mới để tạm ngưng cho đến khi yêu cầu mạng trở lại và kết quả
được lưu vào bộ nhớ đệm. Nếu vào thời điểm đó người dùng vẫn ở trên màn hình,
thì họ sẽ thấy tin tức mới nhất; nếu người dùng di chuyển khỏi màn hình,
await
sẽ bị huỷ nhưng logic bên trong async
tiếp tục thực thi.
Xem blog này
bài đăng
để tìm hiểu thêm về các mẫu cho CoroutineScope
.
Lưu và truy xuất dữ liệu từ ổ đĩa
Giả sử bạn muốn lưu dữ liệu như tin tức đã đánh dấu và tuỳ chọn của người dùng. Kiểu dữ liệu này cần tiếp tục có hiệu lực sau khi ứng dụng bị buộc tắt và có thể truy cập được ngay cả khi người dùng không kết nối mạng.
Nếu dữ liệu bạn đang xử lý cần tiếp tục có hiệu lực sau khi ứng dụng bị buộc tắt, thì bạn phải lưu trữ dữ liệu đó trên ổ đĩa theo một trong các cách sau:
- Đối với các tập dữ liệu lớn cần được truy vấn, cần tính toàn vẹn tham chiếu hoặc cần cập nhật một phần, hãy lưu dữ liệu trong Cơ sở dữ liệu phòng. Trong ví dụ về ứng dụng Tin tức, các tin bài hoặc tác giả có thể được lưu trong cơ sở dữ liệu.
- Đối với các tập dữ liệu nhỏ chỉ cần được truy xuất và đặt (không phải truy vấn hoặc cập nhật một phần), hãy sử dụng DataStore. Trong ví dụ về ứng dụng Tin tức, định dạng ngày mà người dùng ưu tiên hoặc các tuỳ chọn hiển thị khác có thể được lưu trong DataStore.
- Đối với các khối dữ liệu như đối tượng JSON, hãy sử dụng tệp.
Như đã đề cập trong mục Nguồn đáng tin cậy, mỗi nguồn dữ liệu
chỉ hoạt động với một nguồn và phản hồi với một loại dữ liệu cụ thể (ví dụ:
News
, Authors
, NewsAndAuthors
hoặc UserPreferences
). Các lớp
sử dụng nguồn dữ liệu đó không nên biết cách lưu dữ liệu – ví dụ: trong
cơ sở dữ liệu hoặc trong tệp.
Phòng đóng vai trò nguồn dữ liệu
Vì mỗi nguồn dữ liệu sẽ có trách nhiệm chỉ làm việc với một nguồn duy nhất cho một
loại dữ liệu cụ thể, nên nguồn dữ liệu Phòng sẽ nhận được một
đối tượng truy cập dữ liệu (DAO) hoặc
chính cơ sở dữ liệu dưới dạng thông số. Ví dụ: NewsLocalDataSource
có thể lấy
một phiên bản của NewsDao
làm thông số và AuthorsLocalDataSource
có thể lấy
một phiên bản của AuthorsDao
.
Trong một số trường hợp, nếu không cần thêm logic, bạn có thể đưa trực tiếp DAO vào kho lưu trữ, vì DAO là một giao diện mà bạn có thể dễ dàng thay thế trong các thử nghiệm.
Để tìm hiểu thêm về cách làm việc với API phòng, hãy xem Hướng dẫn về phòng.
DataStore đóng vai trò nguồn dữ liệu
DataStore rất phù hợp để lưu trữ các cặp khóa-giá trị như các chế độ cài đặt người dùng. Ví dụ có thể bao gồm định dạng thời gian, tuỳ chọn thông báo và liệu hiển thị hay ẩn các mục tin tức sau khi người dùng đã đọc các định dạng đó. DataStore cũng có thể lưu trữ các đối tượng đã nhập bằng các vùng đệm giao thức.
Giống như với bất kỳ đối tượng nào khác, một nguồn dữ liệu do DataStore hỗ trợ phải chứa dữ liệu tương ứng với một loại nhất định hoặc với một phần nhất định của ứng dụng. Điều này thậm chí còn đúng hơn với DataStore, vì các lượt đọc DataStore hiển thị dưới dạng một luồng được gửi đi mỗi khi giá trị được cập nhật. Vì lý do này, bạn nên lưu trữ các tuỳ chọn liên quan trong cùng một DataStore.
Ví dụ: bạn có thể có một NotificationsDataStore
chỉ xử lý
các tuỳ chọn liên quan đến thông báo và một NewsPreferencesDataStore
chỉ
xử lý các tuỳ chọn liên quan đến màn hình tin tức. Bằng cách đó, bạn có thể đặt phạm vi
cập nhật tốt hơn, vì luồng newsScreenPreferencesDataStore.data
chỉ
phát ra khi một tuỳ chọn ưu tiên liên quan đến màn hình đó thay đổi. Điều này cũng có nghĩa là
vòng đời của đối tượng có thể ngắn hơn vì đối tượng chỉ có thể tồn tại
khi màn hình tin tức hiển thị.
Để tìm hiểu thêm về cách làm việc với API DataStore, hãy xem hướng dẫn về DataStore.
Tệp đóng vai trò là nguồn dữ liệu
Khi làm việc với các đối tượng lớn như đối tượng JSON hoặc bitmap, bạn cần làm việc với đối tượng File
và xử lý các luồng chuyển đổi.
Để tìm hiểu thêm về cách làm việc với dung lượng lưu trữ của tệp, hãy xem trang Tổng quan về bộ nhớ.
Lên lịch tác vụ bằng WorkManager
Giả sử một yêu cầu mới khác được đưa ra cho ứng dụng News (Tin tức): ứng dụng phải cung cấp cho người dùng lựa chọn tìm nạp tin tức mới nhất thường xuyên và tự động miễn là thiết bị đang sạc và kết nối với một mạng không đo lượng dữ liệu. Yêu cầu đó khiến thao tác này trở thành một thao tác do hoạt động kinh doanh điều hướng. Yêu cầu này được đưa ra để ngay cả khi thiết bị không có kết nối khi người dùng mở ứng dụng, thì người dùng vẫn có thể xem tin tức gần đây.
WorkManager giúp bạn dễ dàng
lên lịch trình công việc không đồng bộ và đáng tin cậy, đồng thời có thể đảm nhận việc quản lý
giới hạn. Đây là thư viện đề xuất cho tính năng làm việc liên tục. Để thực hiện tác vụ được xác định ở trên, một lớp Worker
sẽ được tạo: RefreshLatestNewsWorker
. Lớp này sẽ lấy NewsRepository
làm một phần phụ thuộc để tìm nạp tin tức mới nhất rồi lưu vào bộ nhớ đệm.
class RefreshLatestNewsWorker(
private val newsRepository: NewsRepository,
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result = try {
newsRepository.refreshLatestNews()
Result.success()
} catch (error: Throwable) {
Result.failure()
}
}
Logic nghiệp vụ cho loại tác vụ này phải được đóng gói trong lớp riêng và được coi là một nguồn dữ liệu riêng biệt. Sau đó, WorkManager sẽ chỉ chịu trách nhiệm về việc đảm bảo công việc được thực thi trên một luồng ở chế độ nền khi đáp ứng được tất cả các quy tắc giới hạn. Bằng cách tuân thủ mẫu này, bạn có thể nhanh chóng hoán đổi các nội dung triển khai trên các môi trường khác nhau nếu cần.
Trong ví dụ này, tác vụ liên quan đến tin tức này phải được gọi từ NewsRepository
. Thao tác này sẽ lấy một nguồn dữ liệu mới làm phần phụ thuộc: NewsTasksDataSource
,
được triển khai như sau:
private const val REFRESH_RATE_HOURS = 4L
private const val FETCH_LATEST_NEWS_TASK = "FetchLatestNewsTask"
private const val TAG_FETCH_LATEST_NEWS = "FetchLatestNewsTaskTag"
class NewsTasksDataSource(
private val workManager: WorkManager
) {
fun fetchNewsPeriodically() {
val fetchNewsRequest = PeriodicWorkRequestBuilder<RefreshLatestNewsWorker>(
REFRESH_RATE_HOURS, TimeUnit.HOURS
).setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.TEMPORARILY_UNMETERED)
.setRequiresCharging(true)
.build()
)
.addTag(TAG_FETCH_LATEST_NEWS)
workManager.enqueueUniquePeriodicWork(
FETCH_LATEST_NEWS_TASK,
ExistingPeriodicWorkPolicy.KEEP,
fetchNewsRequest.build()
)
}
fun cancelFetchingNewsPeriodically() {
workManager.cancelAllWorkByTag(TAG_FETCH_LATEST_NEWS)
}
}
Các loại lớp này được đặt tên theo dữ liệu mà các lớp này chịu trách nhiệm,
ví dụ: NewsTasksDataSource
hoặc PaymentsTasksDataSource
. Tất cả các tác vụ liên quan đến
một loại dữ liệu cụ thể phải được gói gọn trong cùng một lớp.
Nếu tác vụ cần được kích hoạt khi khởi động ứng dụng, bạn nên kích hoạt
yêu cầu WorkManager bằng cách sử dụng thư viện Khởi động ứng dụng.
Thư viện sẽ gọi kho lưu trữ từ
Initializer
.
Để tìm hiểu thêm về cách làm việc với các API WorkManager, hãy xem hướng dẫn về WorkManager.
Thử nghiệm
Các phương pháp hay nhất về chèn phần phụ thuộc giúp ích khi thử nghiệm ứng dụng của bạn. Việc dựa vào giao diện của các lớp giao tiếp với các tài nguyên bên ngoài cũng rất hữu ích. Khi thử nghiệm một đơn vị, bạn có thể chèn các phiên bản phần phụ thuộc giả mạo để làm cho thử nghiệm trở nên tất định và đáng tin cậy.
Thử nghiệm đơn vị
Hướng dẫn thử nghiệm chung áp dụng khi thử nghiệm lớp dữ liệu. Đối với thử nghiệm đơn vị, hãy sử dụng các đối tượng thực khi cần thiết và giả mạo bất kỳ phần phụ thuộc nào với các nguồn bên ngoài như đọc từ một tệp hoặc đọc từ mạng.
Thử nghiệm tích hợp
Các thử nghiệm tích hợp truy cập vào các nguồn bên ngoài có xu hướng ít mang tính quyết định hơn vì các thử nghiệm này cần chạy trên một thiết bị thực. Bạn nên thực hiện các thử nghiệm đó trong môi trường được kiểm soát để làm cho các thử nghiệm tích hợp trở nên đáng tin cậy hơn.
Đối với cơ sở dữ liệu, Room cho phép tạo một cơ sở dữ liệu trong bộ nhớ mà bạn có thể toàn quyền kiểm soát trong các bài kiểm thử của mình. Để tìm hiểu thêm, hãy xem trang Kiểm tra và gỡ lỗi cho cơ sở dữ liệu của bạn.
Đối với hoạt động kết nối mạng, có các thư viện phổ biến như WireMock hoặc MockWebServer cho phép bạn thực hiện các lệnh gọi HTTP và HTTPS giả mạo, cũng như xác minh rằng các yêu cầu được thực hiện dưới dạng như dự kiến.
Mẫu
Các mẫu sau đây của Google minh hoạ cách sử dụng lớp dữ liệu. Hãy khám phá những mẫu đó để xem hướng dẫn này trong thực tế:
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Lớp miền
- Xây dựng ứng dụng có chế độ ngoại tuyến
- Tạo trạng thái giao diện người dùng