Lớp dữ liệu

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ụnglogic 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.
Hình 1. Vai trò của lớp dữ liệu trong cấu trúc ứng dụng.

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 phần tử giữ 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 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 khởi 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:

  • Đối với các thao tác một lần, hãy cung cấp các hàm tạm ngưng.
  • Để nhận thông báo về các thay đổi của dữ liệu theo thời gian, hãy hiển thị các luồng.
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 sang 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ư LoginRepositoryRegistrationRepository để đá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.
Hình 2. Biểu đồ phần phụ thuộc của một kho lưu trữ phụ thuộc vào những kho lưu trữ khác.

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.

Vòng đời

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 thành một ngăn xếp lui bằng cách sử dụng NavEntryDecorator.

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ề tính năng đặt phạm vi trong Android, hãy xem bài đăng trên blog Đặt phạm vi trong Android và Hilt.

Đạ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