Xây dựng lớp dữ liệu

1. Trước khi bắt đầu

Lớp học lập trình này hướng dẫn bạn về lớp dữ liệu và cách lớp này phù hợp với kiến trúc tổng thể của ứng dụng của bạn.

Lớp dữ liệu là lớp dưới cùng bên dưới lớp miền và các lớp giao diện người dùng.

Hình 1. Sơ đồ cho thấy lớp dữ liệu là lớp mà các lớp miền và lớp giao diện người dùng phụ thuộc vào.

Bạn sẽ xây dựng lớp dữ liệu cho một ứng dụng quản lý công việc. Bạn sẽ tạo các nguồn dữ liệu cho cơ sở dữ liệu cục bộ và dịch vụ mạng, cũng như kho lưu trữ có chức năng hiển thị, cập nhật và đồng bộ hoá dữ liệu.

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

Kiến thức bạn sẽ học được

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách:

  • Tạo kho lưu trữ, nguồn dữ liệu và mô hình dữ liệu để quản lý dữ liệu một cách hiệu quả cũng như có thể mở rộng quy mô dữ liệu.
  • Hiển thị dữ liệu cho các lớp kiến trúc khác.
  • Xử lý các bản cập nhật dữ liệu không đồng bộ và các tác vụ phức tạp hoặc chạy trong thời gian dài.
  • Đồng bộ hoá dữ liệu giữa nhiều nguồn dữ liệu.
  • Tạo chương trình kiểm thử để xác minh hành vi của kho lưu trữ và nguồn dữ liệu.

Sản phẩm bạn sẽ tạo ra

Bạn sẽ tạo một ứng dụng quản lý công việc cho phép thêm công việc cũng như đánh dấu công việc là đã hoàn thành.

Bạn sẽ không viết ứng dụng từ đầu. Mà sẽ làm việc trên một ứng dụng đã có lớp giao diện người dùng. Lớp giao diện người dùng trong ứng dụng này chứa màn hình và phần tử giữ trạng thái cấp màn hình được triển khai bằng ViewModel.

Trong lớp học lập trình này, bạn sẽ thêm lớp dữ liệu vào, sau đó kết nối lớp này với lớp giao diện người dùng hiện tại để cho phép ứng dụng này hoạt động với đầy đủ chức năng.

Màn hình danh sách công việc.

Màn hình thông tin về công việc.

Hình 2. Ảnh chụp màn hình danh sách công việc.

Hình 3. Ảnh chụp màn hình thông tin về công việc.

2. Bắt đầu thiết lập

  1. Tải mã nguồn:

https://github.com/android/architecture-samples/archive/refs/heads/data-codelab-start.zip

  1. Ngoài ra, bạn có thể sao chép kho lưu trữ GitHub cho mã:
git clone https://github.com/android/architecture-samples.git
git checkout data-codelab-start
  1. Mở Android Studio rồi tải dự án architecture-samples.

Cấu trúc thư mục

  • Mở Project Explorer (Trình khám phá dự án) trong chế độ xem Android.

Trong thư mục java/com.example.android.architecture.blueprints.todoapp, có một số thư mục.

Cửa sổ Project Explorer (Trình khám phá dự án) của Android Studio trong chế độ xem Android.

Hình 4. Ảnh chụp màn hình cho thấy cửa sổ Project Explorer (Trình khám phá dự án) trong Android Studio ở chế độ xem Android.

  • <root> chứa các lớp cấp ứng dụng, chẳng hạn như lớp thao tác, lớp hoạt động chính và lớp ứng dụng.
  • addedittask chứa tính năng giao diện người dùng cho phép người dùng thêm và chỉnh sửa công việc.
  • data chứa lớp dữ liệu. Bạn sẽ làm việc chủ yếu trong thư mục này.
  • di chứa các mô-đun Hilt để chèn phần phụ thuộc.
  • tasks chứa tính năng giao diện người dùng cho phép người dùng xem và cập nhật danh sách công việc.
  • util chứa các lớp tiện ích.

Ngoài ra, còn có 2 thư mục kiểm thử được biểu thị bằng văn bản trong ngoặc đơn ở cuối tên thư mục.

  • androidTest có cấu trúc tương tự như <root> nhưng có các quy trình kiểm thử đo lường.
  • test có cấu trúc tương tự như <root> nhưng có các quy trình kiểm thử cục bộ.

Chạy dự án

  • Nhấp vào biểu tượng Chạy màu xanh lục trên thanh công cụ trên cùng.

Cấu hình chạy Android Studio, thiết bị mục tiêu và nút Chạy.

Hình 5. Ảnh chụp màn hình cho thấy cấu hình chạy Android Studio, thiết bị mục tiêu và nút chạy.

Bạn sẽ thấy màn hình Danh sách công việc với một vòng quay tải không bao giờ biến mất.

Ứng dụng ở trạng thái khởi động với vòng quay tải vô hạn.

Hình 6. Ảnh chụp màn hình ứng dụng ở trạng thái ban đầu với vòng quay tải vô hạn.

Khi kết thúc lớp học lập trình, danh sách các công việc sẽ xuất hiện trên màn hình này.

Bạn có thể xem mã nguồn hoàn chỉnh trong lớp học lập trình bằng cách xem nhánh data-codelab-final.

git checkout data-codelab-final

Nhưng hãy nhớ lưu trữ các thay đổi của bạn trước tiên!

3. Tìm hiểu về lớp dữ liệu

Trong lớp học lập trình này, bạn sẽ tạo lớp dữ liệu (data layer) cho ứng dụng.

Lớp dữ liệu, đúng như tên gọi, là một lớp kiến trúc quản lý dữ liệu ứng dụng của bạn. Lớp này cũng chứa logic kinh doanh — các quy tắc kinh doanh trong thế giới thực xác định cách tạo, lưu trữ và sửa đổi dữ liệu ứng dụng. Việc tách biệt các vấn đề cần quan tâm này giúp lớp dữ liệu có thể sử dụng lại, cho phép lớp này có thể xuất hiện trên nhiều màn hình, chia sẻ thông tin giữa các phần của ứng dụng và tái tạo logic kinh doanh bên ngoài giao diện người dùng đối với kiểm thử đơn vị.

Các loại thành phần chính tạo nên lớp dữ liệu là mô hình dữ liệu, nguồn dữ liệu và kho lưu trữ.

Các loại thành phần trong lớp dữ liệu, kể cả các phần phụ thuộc giữa mô hình dữ liệu, nguồn dữ liệu và kho lưu trữ.

Hình 7. Sơ đồ cho thấy các loại thành phần trong lớp dữ liệu, kể cả các phần phụ thuộc giữa mô hình dữ liệu, nguồn dữ liệu và kho lưu trữ.

Mô hình dữ liệu

Dữ liệu ứng dụng thường được trình bày dưới dạng mô hình dữ liệu. Đây là những đại diện của dữ liệu trong bộ nhớ.

Vì ứng dụng này là ứng dụng quản lý công việc nên bạn cần một mô hình dữ liệu cho một công việc. Sau đây là lớp Task:

data class Task(
    val id: String
    val title: String = "",
    val description: String = "",
    val isCompleted: Boolean = false,
) { ... }

Điểm chính của mô hình này là không thể thay đổi. Các lớp khác không thể thay đổi thuộc tính của công việc; chúng phải sử dụng lớp dữ liệu nếu muốn thay đổi một công việc.

Mô hình dữ liệu nội bộ và bên ngoài

Task là một ví dụ về mô hình dữ liệu bên ngoài. Lớp này hiển thị với lớp bên ngoài và có thể được truy cập qua các lớp khác. Sau này, bạn sẽ xác định các mô hình dữ liệu nội bộ chỉ dùng trong lớp dữ liệu.

Bạn nên xác định mô hình dữ liệu cho từng đại diện của một mô hình kinh doanh. Trong ứng dụng này, có 3 mô hình dữ liệu.

Tên mẫu thiết bị

Nội bộ hay bên ngoài lớp dữ liệu?

Đại diện cho

Nguồn dữ liệu được liên kết

Task

Bên ngoài

Một công việc có thể được dùng ở mọi nơi trong ứng dụng, chỉ được lưu trữ trong bộ nhớ hoặc khi lưu trạng thái của ứng dụng

Không áp dụng

LocalTask

Nội bộ

Một công việc được lưu trữ trong cơ sở dữ liệu cục bộ

TaskDao

NetworkTask

Nội bộ

Một công việc đã được truy xuất từ máy chủ mạng

NetworkTaskDataSource

Nguồn dữ liệu

Nguồn dữ liệu là lớp chịu trách nhiệm đọc và ghi dữ liệu vào một nguồn như cơ sở dữ liệu hoặc dịch vụ mạng.

Trong ứng dụng này, có 2 nguồn dữ liệu:

  • TaskDao là nguồn dữ liệu cục bộ có chức năng đọc và ghi vào cơ sở dữ liệu.
  • NetworkTaskDataSource là nguồn dữ liệu mạng có chức năng đọc và ghi vào máy chủ mạng.

Kho lưu trữ

Kho lưu trữ nên quản lý một mô hình dữ liệu duy nhất. Trong ứng dụng này, bạn sẽ tạo một kho lưu trữ quản lý các mô hình Task. Kho lưu trữ:

  • Hiển thị danh sách mô hình Task.
  • Cung cấp các phương thức để tạo và cập nhật mô hình Task.
  • Thực thi logic kinh doanh, chẳng hạn như tạo mã nhận dạng duy nhất cho mỗi công việc.
  • Kết hợp hoặc liên kết các mô hình dữ liệu nội bộ từ nguồn dữ liệu thành các mô hình Task.
  • Đồng bộ hoá nguồn dữ liệu.

Sẵn sàng viết mã!

  • Chuyển sang chế độ xem Android và mở rộng gói com.example.android.architecture.blueprints.todoapp.data:

Cửa sổ Project Explorer (Trình khám phá dự án) hiển thị các thư mục và tệp.

Hình 8. Cửa sổ Project Explorer (Trình khám phá dự án) hiển thị các thư mục và tệp.

Lớp Task đã được tạo để phần còn lại của ứng dụng biên dịch. Từ giờ trở đi, bạn có thể tạo hầu hết lớp dữ liệu từ đầu bằng cách thêm phương thức triển khai vào các tệp .kt trống được cung cấp.

4. Lưu trữ dữ liệu cục bộ

Ở bước này, bạn sẽ tạo một nguồn dữ liệu và một mô hình dữ liệu cho cơ sở dữ liệu Room lưu trữ các công việc trên thiết bị.

Mối quan hệ giữa kho lưu trữ công việc, mô hình, nguồn dữ liệu và cơ sở dữ liệu.

Hình 9. Sơ đồ cho thấy mối quan hệ giữa kho lưu trữ công việc, mô hình, nguồn dữ liệu và cơ sở dữ liệu.

Tạo mô hình dữ liệu

Để lưu trữ dữ liệu trong cơ sở dữ liệu Room, bạn cần tạo một thực thể cơ sở dữ liệu.

  • Mở tệp LocalTask.kt bên trong data/source/local, sau đó thêm đoạn mã sau vào tệp đó:
@Entity(
    tableName = "task"
)
data class LocalTask(
    @PrimaryKey val id: String,
    var title: String,
    var description: String,
    var isCompleted: Boolean,
)

Lớp LocalTask đại diện cho dữ liệu được lưu trữ trong bảng có tên là task trong cơ sở dữ liệu Room. Lớp này được liên kết chặt chẽ với Room và không được dùng cho các nguồn dữ liệu khác như DataStore.

Tiền tố Local trong tên lớp được dùng để cho biết rằng dữ liệu này được lưu trữ cục bộ. Tiền tố này cũng được dùng để phân biệt lớp này với mô hình dữ liệu Task (hiển thị với các lớp khác trong ứng dụng). Nói cách khác, LocalTasknội bộ đối với lớp dữ liệu và Taskbên ngoài đối với lớp dữ liệu.

Tạo một nguồn dữ liệu

Bây giờ, bạn đã có mô hình dữ liệu, hãy tạo nguồn dữ liệu để tạo, đọc, cập nhật và xoá (CRUD) mô hình LocalTask. Vì đang dùng Room, nên bạn có thể dùng Đối tượng truy cập dữ liệu (chú thích @Dao) làm nguồn dữ liệu cục bộ.

  • Tạo giao diện Kotlin mới trong tệp có tên TaskDao.kt.
@Dao
interface TaskDao {

    @Query("SELECT * FROM task")
    fun observeAll(): Flow<List<LocalTask>>

    @Upsert
    suspend fun upsert(task: LocalTask)

    @Upsert
    suspend fun upsertAll(tasks: List<LocalTask>)

    @Query("UPDATE task SET isCompleted = :completed WHERE id = :taskId")
    suspend fun updateCompleted(taskId: String, completed: Boolean)

    @Query("DELETE FROM task")
    suspend fun deleteAll()
}

Các phương thức đọc dữ liệu có tiền tố observe. Đây là các hàm không tạm ngưng trả về Flow. Mỗi lần dữ liệu cơ bản thay đổi, một mục mới sẽ được phát vào luồng. Tính năng hữu ích này của thư viện Room (và nhiều thư viện lưu trữ dữ liệu khác) có nghĩa là bạn có thể theo dõi các thay đổi đối với dữ liệu thay vì thăm dò cơ sở dữ liệu để lấy dữ liệu mới.

Các phương thức để ghi dữ liệu là tạm ngưng các hàm vì chúng đang thực hiện các hoạt động I/O.

Cập nhật giản đồ cơ sở dữ liệu

Việc tiếp theo bạn cần làm là cập nhật cơ sở dữ liệu để lưu trữ các mô hình LocalTask.

  1. Mở ToDoDatabase.kt và thay đổi BlankEntity thành LocalTask.
  2. Xoá BlankEntity và mọi câu lệnh import thừa.
  3. Thêm một phương thức để trả về DAO có tên taskDao.

Lớp được cập nhật sẽ có dạng như sau:

@Database(entities = [LocalTask::class], version = 1, exportSchema = false)
abstract class ToDoDatabase : RoomDatabase() {

    abstract fun taskDao(): TaskDao
}

Cập nhật cấu hình Hilt

Dự án này sử dụng Hilt để chèn phần phụ thuộc. Hilt cần biết cách tạo TaskDao để có thể đưa vào các lớp sử dụng đối tượng này.

  • Hãy mở di/DataModules.kt rồi thêm phương thức sau vào DatabaseModule:
    @Provides
    fun provideTaskDao(database: ToDoDatabase) : TaskDao = database.taskDao()

Giờ đây, bạn đã có tất cả yếu tố cần thiết để đọc và ghi công việc vào cơ sở dữ liệu cục bộ.

5. Kiểm tra nguồn dữ liệu cục bộ

Trong bước cuối cùng, bạn đã viết khá nhiều mã, nhưng làm cách nào để biết là cách viết mã này hoạt động chính xác? Bạn có thể mắc lỗi với khi viết tất cả truy vấn SQL đó trong TaskDao. Hãy tạo chương trình kiểm thử để xác minh rằng TaskDao hoạt động bình thường.

Các chương trình kiểm thử không phải là một phần của ứng dụng, vì vậy bạn nên kiểm thử ở một thư mục khác. Có hai thư mục kiểm thử được biểu thị bằng văn bản trong ngoặc đơn ở cuối tên gói:

Thư mục test (kiểm thử) và androidTest trong Project Explorer (Trình khám phá dự án).

Hình 10. Ảnh chụp màn hình cho thấy các thư mục test (kiểm thử) và androidTest trong Project Explorer (Trình khám phá dự án).

  • androidTest chứa các chương trình kiểm thử chạy trên trình mô phỏng hoặc thiết bị Android. Các chương trình này được gọi là kiểm thử đo lường.
  • test chứa các chương trình kiểm thử chạy trên máy chủ của bạn (còn được gọi là chương trình kiểm thử cục bộ).

TaskDao yêu cầu cơ sở dữ liệu Room (chỉ tạo được trên thiết bị Android). Vì vậy, để kiểm thử cơ sở dữ liệu này, bạn cần tạo một chương trình kiểm thử đo lường.

Tạo lớp kiểm thử

  • Mở rộng thư mục androidTest rồi mở TaskDaoTest.kt. Bên trong lớp này, hãy tạo một lớp trống có tên là TaskDaoTest.
class TaskDaoTest {

}

Thêm cơ sở dữ liệu kiểm thử

  • Thêm ToDoDatabase và khởi tạo trước mỗi lần kiểm thử.
    private lateinit var database: ToDoDatabase

    @Before
    fun initDb() {
        database = Room.inMemoryDatabaseBuilder(
            getApplicationContext(),
            ToDoDatabase::class.java
        ).allowMainThreadQueries().build()
    }

Thao tác này sẽ tạo một cơ sở dữ liệu trong bộ nhớ trước mỗi lần kiểm thử. Cơ sở dữ liệu trong bộ nhớ nhanh hơn nhiều so với cơ sở dữ liệu dựa trên ổ đĩa. Đây là một lựa chọn phù hợp cho các chương trình kiểm thử tự động, trong đó dữ liệu không cần tồn tại lâu hơn chương trình kiểm thử.

Thêm chương trình kiểm thử

Thêm chương trình kiểm thử xác minh rằng có thể chèn một LocalTask và có thể đọc cùng một LocalTask đó bằng TaskDao.

Các chương trình kiểm thử trong lớp học lập trình này đều tuân theo cấu trúc given, when, then (điều kiện, thời điểm, kết quả):

Điều kiện

Cơ sở dữ liệu trống

Thời điểm

Một công việc sẽ được chèn vào và bạn sẽ bắt đầu quan sát luồng công việc đó

Kết quả

Mục đầu tiên trong luồng công việc khớp với công việc đã chèn

  1. Bắt đầu bằng cách tạo một chương trình kiểm thử không thành công. Điều này sẽ xác minh rằng chương trình kiểm thử thực sự đang chạy và có kiểm thử đúng đối tượng và phần phụ thuộc hay không.
@Test
fun insertTaskAndGetTasks() = runTest {

    val task = LocalTask(
        title = "title",
        description = "description",
        id = "id",
        isCompleted = false,
    )
    database.taskDao().upsert(task)

    val tasks = database.taskDao().observeAll().first()

    assertEquals(0, tasks.size)
}
  1. Chạy chương trình kiểm thử bằng cách nhấp vào Play (Chạy) bên cạnh chương trình kiểm thử trong phần lề (gutter).

Nút Chạy chương trình kiểm thử trong lề của trình soạn thảo mã.

Hình 11. Ảnh chụp màn hình cho thấy nút Play (Chạy) của chương trình kiểm thử trong phần lề trên trình soạn thảo mã.

Bên trong cửa sổ kết quả kiểm thử, bạn sẽ thấy chương trình kiểm thử không thành công cùng thông báo expected:<0> but was:<1>. Điều này là bình thường vì số lượng công việc trong cơ sở dữ liệu là 1, chứ không phải là 0.

Chương trình kiểm thử không thành công.

Hình 12. Ảnh chụp màn hình cho thấy chương trình kiểm thử không thành công.

  1. Xoá câu lệnh assertEquals hiện tại.
  2. Thêm mã để kiểm thử nhằm đảm bảo rằng một và chỉ một công việc do nguồn dữ liệu cung cấp và đó cũng chính là công việc đã được chèn.

Thứ tự của các tham số đối với assertEquals phải luôn là giá trị dự kiến rồi đến giá trị thực tế**.**

assertEquals(1, tasks.size)
assertEquals(task, tasks[0])
  1. Chạy lại chương trình kiểm thử. Bạn sẽ thấy chương trình kiểm thử thành công trong cửa sổ kết quả kiểm thử.

Chương trình kiểm thử thành công.

Hình 13. Ảnh chụp màn hình cho thấy một chương trình kiểm thử thành công.

6. Tạo nguồn dữ liệu mạng

Thật tuyệt vời khi công việc có thể được lưu cục bộ trên thiết bị, nhưng nếu bạn cũng muốn lưu và tải những công việc đó vào dịch vụ mạng thì sao? Có lẽ ứng dụng Android của bạn chỉ là một cách để người dùng thêm công việc vào danh sách TODO (Cần thực hiện) của họ. Bạn cũng có thể quản lý công việc thông qua trang web hoặc ứng dụng dành cho máy tính. Hoặc có thể bạn chỉ muốn cung cấp bản sao lưu dữ liệu trực tuyến để người dùng có thể khôi phục dữ liệu ứng dụng ngay cả khi thay đổi thiết bị.

Trong các trường hợp này, thường thì bạn sẽ có một dịch vụ dựa trên mạng mà tất cả ứng dụng khách (kể cả ứng dụng Android của bạn) có thể sử dụng để tải và lưu dữ liệu.

Trong bước tiếp theo này, bạn sẽ tạo một nguồn dữ liệu để giao tiếp với dịch vụ mạng này. Nhằm phục vụ mục đích của lớp học lập trình này, đây là một dịch vụ mô phỏng không kết nối với dịch vụ mạng đang hoạt động, nhưng sẽ giúp bạn biết được cách triển khai trong ứng dụng thực tế.

Giới thiệu về dịch vụ mạng

Trong ví dụ, API mạng rất đơn giản. API này chỉ thực hiện hai tác vụ:

  • Lưu tất cả công việc, ghi đè mọi dữ liệu đã ghi trước đó.
  • Tải tất cả các công việc, danh sách này cung cấp danh sách tất cả các công việc hiện được lưu trên dịch vụ mạng.

Lập mô hình dữ liệu mạng

Khi có được dữ liệu từ API mạng, dữ liệu đó thường được thể hiện khác với cách cục bộ. Đại diện trên mạng của một công việc có thể có các trường bổ sung, hoặc có thể sử dụng các kiểu hoặc tên trường khác nhau để thể hiện các giá trị giống nhau.

Để tính đến những khác biệt này, hãy tạo một mô hình dữ liệu dành riêng cho mạng.

  • Mở tệp NetworkTask.kt có trong data/source/network rồi thêm mã sau để thể hiện các trường:
data class NetworkTask(
    val id: String,
    val title: String,
    val shortDescription: String,
    val priority: Int? = null,
    val status: TaskStatus = TaskStatus.ACTIVE
) {
    enum class TaskStatus {
        ACTIVE,
        COMPLETE
    }
}

Dưới đây là sự khác biệt giữa LocalTaskNetworkTask:

  • Nội dung mô tả công việc có tên là shortDescription thay vì description.
  • Trường isCompleted được thể hiện dưới dạng enum status, có thể có hai giá trị: ACTIVECOMPLETE.
  • Tệp này chứa trường priority bổ sung, là một số nguyên.

Tạo nguồn dữ liệu mạng

  • Mở TaskNetworkDataSource.kt, sau đó tạo một lớp có tên TaskNetworkDataSource như sau:
class TaskNetworkDataSource @Inject constructor() {

    // A mutex is used to ensure that reads and writes are thread-safe.
    private val accessMutex = Mutex()
    private var tasks = listOf(
        NetworkTask(
            id = "PISA",
            title = "Build tower in Pisa",
            shortDescription = "Ground looks good, no foundation work required."
        ),
        NetworkTask(
            id = "TACOMA",
            title = "Finish bridge in Tacoma",
            shortDescription = "Found awesome girders at half the cost!"
        )
    )

    suspend fun loadTasks(): List<NetworkTask> = accessMutex.withLock {
        delay(SERVICE_LATENCY_IN_MILLIS)
        return tasks
    }

    suspend fun saveTasks(newTasks: List<NetworkTask>) = accessMutex.withLock {
        delay(SERVICE_LATENCY_IN_MILLIS)
        tasks = newTasks
    }
}

private const val SERVICE_LATENCY_IN_MILLIS = 2000L

Đối tượng này mô phỏng hoạt động tương tác với máy chủ, kể cả độ trễ được mô phỏng là 2 giây mỗi khi loadTasks hoặc saveTasks được gọi. Thông tin này có thể biểu thị độ trễ phản hồi của mạng hoặc máy chủ.

Trang này cũng chứa một số dữ liệu kiểm thử mà bạn sử dụng sau này để xác minh rằng có thể tải các công việc từ mạng thành công.

Nếu API máy chủ thực của bạn sử dụng HTTP, hãy cân nhắc sử dụng một thư viện ( chẳng hạn như Ktor hoặc Retrofit) để xây dựng nguồn dữ liệu mạng.

7. Tạo kho lưu trữ nhiệm vụ

Chúng ta đang ghép các phần lại với nhau.

Các phần phụ thuộc của DefaultTaskRepository.

Hình 14. Sơ đồ cho thấy các phần phụ thuộc của DefaultTaskRepository.

Chúng ta có hai nguồn dữ liệu — một cho dữ liệu cục bộ (TaskDao) và một cho dữ liệu mạng (TaskNetworkDataSource). Mỗi nguồn dữ liệu cho phép đọc và ghi, đồng thời đại diện riêng cho công việc (LocalTaskNetworkTask).

Đã đến lúc tạo kho lưu trữ sử dụng các nguồn dữ liệu này và cung cấp API để các lớp kiến trúc khác có thể truy cập dữ liệu công việc này.

Hiển thị dữ liệu

  1. Mở DefaultTaskRepository.kt trong gói data, sau đó tạo một lớp có tên DefaultTaskRepository. Lớp này sẽ lấy TaskDaoTaskNetworkDataSource làm phần phụ thuộc.
class DefaultTaskRepository @Inject constructor(
    private val localDataSource: TaskDao,
    private val networkDataSource: TaskNetworkDataSource,
) {

}

Dữ liệu phải được hiển thị bằng dòng dữ liệu. Điều này cho phép phương thức gọi nhận thông báo về các thay đổi đối với dữ liệu đó theo thời gian.

  1. Thêm phương thức có tên observeAll. Phương thức này sẽ trả về luồng mô hình Task bằng cách sử dụng Flow.
fun observeAll() : Flow<List<Task>> {
    // TODO add code to retrieve Tasks
}

Các kho lưu trữ phải hiển thị dữ liệu từ một nguồn đáng tin cậy. Nghĩa là, dữ liệu chỉ nên đến từ một nguồn dữ liệu. Đây có thể là bộ nhớ đệm trong bộ nhớ, máy chủ từ xa hoặc trong trường hợp này là cơ sở dữ liệu cục bộ.

Bạn có thể truy cập các công việc trong cơ sở dữ liệu cục bộ bằng cách sử dụng TaskDao.observeAll để thuận lợi trả về một dòng dữ liệu. Tuy nhiên, đây là dòng dữ liệu gồm các mô hình LocalTask, trong đó LocalTask là một mô hình nội bộ không được hiển thị với các lớp kiến trúc khác.

Bạn cần chuyển đổi LocalTask thành Task. Đây là mô hình bên ngoài tạo thành một phần của API lớp dữ liệu.

Ánh xạ mô hình nội bộ tới mô hình bên ngoài

Để thực hiện việc chuyển đổi này, bạn cần ánh xạ các trường từ LocalTask đến các trường trong Task.

  1. Tạo các hàm mở rộng để thực hiện điều này trong LocalTask.
// Convert a LocalTask to a Task
fun LocalTask.toExternal() = Task(
    id = id,
    title = title,
    description = description,
    isCompleted = isCompleted,
)

// Convenience function which converts a list of LocalTasks to a list of Tasks
fun List<LocalTask>.toExternal() = map(LocalTask::toExternal) // Equivalent to map { it.toExternal() }

Giờ đây, mỗi khi cần chuyển đổi LocalTask thành Task, bạn chỉ cần gọi toExternal.

  1. Sử dụng hàm toExternal mới tạo bên trong observeAll:
fun observeAll(): Flow<List<Task>> {
    return localDataSource.observeAll().map { tasks ->
        tasks.toExternal()
    }
}

Mỗi lần dữ liệu công việc thay đổi trong cơ sở dữ liệu cục bộ, một danh sách mô hình LocalTask mới sẽ được phát vào dòng dữ liệu. Sau đó, mỗi LocalTask sẽ được liên kết với một Task.

Tuyệt vời! Giờ đây, các lớp khác có thể sử dụng observeAll để lấy tất cả mô hình Task qua cơ sở dữ liệu cục bộ và nhận thông báo mỗi khi các mô hình Task đó thay đổi.

Cập nhật dữ liệu

Ứng dụng TODO (Việc cần làm) sẽ không tốt lắm nếu bạn không thể tạo và cập nhật công việc. Giờ đây, bạn có thể thêm các phương thức để thực hiện việc đó.

Các phương thức tạo, cập nhật hoặc xoá dữ liệu là thao tác một lần và cần được triển khai bằng các hàm suspend.

  1. Thêm một phương thức có tên create. Phương thức này sẽ lấy titledescription làm tham số và trả về mã của công việc mới tạo.
suspend fun create(title: String, description: String): String {
}

Lưu ý rằng API lớp dữ liệu cấm Task được tạo bởi các lớp khác bằng cách chỉ cung cấp phương thức create chấp nhận các tham số riêng lẻ, không chấp nhận Task. Phương pháp này bao gồm:

  • Logic kinh doanh để tạo mã công việc duy nhất.
  • Nơi công việc được lưu trữ sau lần tạo đầu tiên.
  1. Thêm phương thức để tạo mã công việc
// This method might be computationally expensive
private fun createTaskId() : String {
    return UUID.randomUUID().toString()
}
  1. Tạo mã công việc bằng phương thức createTaskId mới được thêm vào
suspend fun create(title: String, description: String): String {
    val taskId = createTaskId()
}

Không chặn luồng chính

Nhưng chờ đã! Điều gì sẽ xảy ra nếu việc tạo mã công việc sẽ tốn kém nhiều năng lực tính toán? Có thể khoá này sẽ sử dụng mật mã học để tạo khoá băm cho mã nhận dạng. Quá trình này mất vài giây. Điều này có thể khiến giao diện người dùng bị giật nếu được gọi trên luồng chính.

Lớp dữ liệu có trách nhiệm đảm bảo rằng các tác vụ chạy trong thời gian dài hoặc phức tạp sẽ không chặn luồng chính.

Để khắc phục vấn đề này, hãy chỉ định trình điều phối coroutine sẽ được dùng để thực thi các lệnh này.

  1. Trước tiên, hãy thêm CoroutineDispatcher dưới dạng phần phụ thuộc vào DefaultTaskRepository. Sử dụng bộ hạn định @DefaultDispatcher đã tạo (được xác định trong di/CoroutinesModule.kt) để yêu cầu Hilt chèn phần phụ thuộc này với Dispatchers.Default. Trình điều phối Default được chỉ định vì trình điều phối này được tối ưu hoá cho các công việc đòi hỏi nhiều CPU. Đọc thêm về trình điều phối coroutine tại đây.
class DefaultTaskRepository @Inject constructor(
   private val localDataSource: TaskDao,
   private val networkDataSource: TaskNetworkDataSource,
   @DefaultDispatcher private val dispatcher: CoroutineDispatcher,
)
  1. Bây giờ, hãy thực hiện lệnh gọi đến UUID.randomUUID().toString() bên trong khối withContext.
val taskId = withContext(dispatcher) {
    createTaskId()
}

Đọc thêm về việc phân luồng trong lớp dữ liệu.

Tạo và lưu trữ công việc

  1. Bây giờ, bạn đã có mã công việc, hãy sử dụng mã này cùng với các tham số đã cung cấp để tạo Task mới.
suspend fun create(title: String, description: String): String {
    val taskId = withContext(dispatcher) {
        createTaskId()
    }
    val task = Task(
        title = title,
        description = description,
        id = taskId,
    )
}

Trước khi chèn công việc vào nguồn dữ liệu cục bộ, bạn cần liên kết công việc đó với LocalTask.

  1. Thêm hàm mở rộng sau vào cuối LocalTask. Đây là hàm ánh xạ ngược đến LocalTask.toExternal mà bạn đã tạo trước đó.
fun Task.toLocal() = LocalTask(
    id = id,
    title = title,
    description = description,
    isCompleted = isCompleted,
)
  1. Sử dụng thuộc tính này bên trong create để chèn công việc vào nguồn dữ liệu cục bộ rồi trả về taskId.
suspend fun create(title: String, description: String): Task {
    ...
    localDataSource.upsert(task.toLocal())
    return taskId
}

Hoàn thành công việc

  • Tạo thêm một phương thức là complete để đánh dấu Task là hoàn tất.
suspend fun complete(taskId: String) {
    localDataSource.updateCompleted(taskId, true)
}

Giờ đây, bạn đã có một số phương thức hữu ích để tạo và hoàn thành công việc.

Đồng bộ hoá dữ liệu

Trong ứng dụng này, nguồn dữ liệu mạng được dùng làm dữ liệu sao lưu trực tuyến và được cập nhật mỗi khi dữ liệu được ghi cục bộ. Dữ liệu sẽ được tải từ mạng mỗi khi người dùng yêu cầu làm mới.

Các sơ đồ dưới đây tóm tắt hành vi của từng loại tác vụ.

Loại tác vụ

Phương thức kho lưu trữ

Các bước

Di chuyển dữ liệu

Tải

observeAll

Tải dữ liệu từ cơ sở dữ liệu cục bộ

Dòng dữ liệu từ nguồn dữ liệu cục bộ đến kho lưu trữ công việc.Hình 15. Biểu đồ thể hiện dòng dữ liệu từ nguồn dữ liệu cục bộ đến kho lưu trữ công việc.

Lưu

createcomplete

1. Ghi dữ liệu vào database2 cục bộ. Sao chép tất cả dữ liệu vào mạng, ghi đè mọi dữ liệu

Dòng dữ liệu từ kho lưu trữ công việc đến nguồn dữ liệu cục bộ, sau đó đến nguồn dữ liệu mạng.Hình 16. Biểu đồ thể hiện dòng dữ liệu từ kho lưu trữ công việc đến nguồn dữ liệu cục bộ, sau đó đến nguồn dữ liệu mạng.

Làm mới

refresh

1. Tải dữ liệu từ network2. Sao chép dữ liệu vào cơ sở dữ liệu cục bộ, ghi đè mọi dữ liệu

Dòng dữ liệu từ nguồn dữ liệu mạng đến nguồn dữ liệu cục bộ, sau đó chuyển đến kho lưu trữ công việc.Hình 17. Biểu đồ thể hiện dòng dữ liệu từ nguồn dữ liệu mạng đến nguồn dữ liệu cục bộ, sau đó đến kho lưu trữ công việc.

Lưu và làm mới dữ liệu mạng

Kho lưu trữ của bạn đã tải các công việc từ nguồn dữ liệu cục bộ. Để hoàn tất thuật toán đồng bộ hoá, bạn cần tạo các phương thức để lưu và làm mới dữ liệu từ nguồn dữ liệu mạng.

  1. Trước tiên, hãy tạo các hàm ánh xạ từ LocalTask đến NetworkTask và ngược lại bên trong NetworkTask.kt. Việc đặt các hàm bên trong LocalTask.kt cũng có giá trị như nhau.
fun NetworkTask.toLocal() = LocalTask(
    id = id,
    title = title,
    description = shortDescription,
    isCompleted = (status == NetworkTask.TaskStatus.COMPLETE),
)

fun List<NetworkTask>.toLocal() = map(NetworkTask::toLocal)

fun LocalTask.toNetwork() = NetworkTask(
    id = id,
    title = title,
    shortDescription = description,
    status = if (isCompleted) { NetworkTask.TaskStatus.COMPLETE } else { NetworkTask.TaskStatus.ACTIVE }
)

fun List<LocalTask>.toNetwork() = map(LocalTask::toNetwork)

Ở đây, bạn có thể thấy được lợi thế của việc có các mô hình riêng biệt cho mỗi nguồn dữ liệu—việc ánh xạ một loại dữ liệu với một loại dữ liệu khác được đóng gói thành các hàm riêng biệt.

  1. Thêm phương thức refresh ở cuối DefaultTaskRepository.
suspend fun refresh() {
    val networkTasks = networkDataSource.loadTasks()
    localDataSource.deleteAll()
    val localTasks = withContext(dispatcher) {
        networkTasks.toLocal()
    }
    localDataSource.upsertAll(networkTasks.toLocal())
}

Thao tác này sẽ thay thế tất cả công việc cục bộ thành các công việc từ mạng. withContext được dùng cho tác vụ toLocal hàng loạt vì số lượng công việc là không xác định và mỗi tác vụ ánh xạ có thể gây hao tốn năng lực tính toán.

  1. Thêm phương thức saveTasksToNetwork vào cuối DefaultTaskRepository.
private suspend fun saveTasksToNetwork() {
    val localTasks = localDataSource.observeAll().first()
    val networkTasks = withContext(dispatcher) {
        localTasks.toNetwork()
    }
    networkDataSource.saveTasks(networkTasks)
}

Thao tác này sẽ thay thế tất cả công việc mạng bằng các công việc từ nguồn dữ liệu cục bộ.

  1. Bây giờ, hãy cập nhật các phương thức hiện có để cập nhật công việc createcomplete sao cho dữ liệu cục bộ được lưu vào mạng khi thay đổi.
    suspend fun create(title: String, description: String): String {
        ...
        saveTasksToNetwork()
        return taskId
    }

     suspend fun complete(taskId: String) {
        localDataSource.updateCompleted(taskId, true)
        saveTasksToNetwork()
    }

Không bắt phương thức gọi phải chờ

Nếu chạy mã này, bạn sẽ thấy rằng saveTasksToNetwork bị chặn. Điều này có nghĩa là phương thức gọi của createcomplete buộc phải đợi cho đến khi dữ liệu được lưu vào mạng rồi mới có thể đảm bảo rằng tác vụ đó đã hoàn tất. Trong nguồn dữ liệu mạng mô phỏng, quá trình này chỉ diễn ra trong 2 giây nhưng trong một ứng dụng thực tế thì có thể mất nhiều thời gian hơn – hoặc không bao giờ hoàn tất nếu không có kết nối mạng.

Điều này là không cần thiết và có thể sẽ khiến người dùng có trải nghiệm không tốt. Không ai muốn chờ đợi để tạo một công việc, đặc biệt là khi đang bận rộn!

Một giải pháp hay hơn là sử dụng phạm vi coroutine khác để lưu dữ liệu vào mạng. Điều này cho phép tác vụ hoàn tất trong nền mà không làm cho phương thức gọi phải chờ kết quả.

  1. Thêm một phạm vi coroutine dưới dạng tham số vào DefaultTaskRepository.
class DefaultTaskRepository @Inject constructor(
    // ...other parameters...
    @ApplicationScope private val scope: CoroutineScope,
)

Bộ hạn định Hilt @ApplicationScope (được định nghĩa trong di/CoroutinesModule.kt) được dùng để chèn một phạm vi tuân theo vòng đời của ứng dụng.

  1. Gói mã bên trong saveTasksToNetwork bằng scope.launch.
    private fun saveTasksToNetwork() {
        scope.launch {
            val localTasks = localDataSource.observeAll().first()
            val networkTasks = withContext(dispatcher) {
                localTasks.toNetwork()
            }
            networkDataSource.saveTasks(networkTasks)
        }
    }

Bây giờ, saveTasksToNetwork sẽ trả về ngay lập tức và các công việc sẽ được lưu vào mạng trong nền.

8. Kiểm thử kho lưu trữ công việc

Ồ, đã có rất nhiều chức năng được thêm vào lớp dữ liệu của bạn. Đã đến lúc xác minh rằng mọi thứ đều hoạt động tốt bằng cách tạo các chương trình kiểm thử đơn vị cho DefaultTaskRepository.

Bạn cần tạo thực thể cho đối tượng sẽ kiểm thử (DefaultTaskRepository) bằng các phần phụ thuộc kiểm thử đối với nguồn dữ liệu cục bộ và nguồn dữ liệu mạng. Trước tiên, bạn cần tạo các phần phụ thuộc đó.

  1. Trong cửa sổ Project Explorer (Trình khám phá dự án), hãy mở rộng thư mục (test), sau đó mở rộng thư mục source.local và mở FakeTaskDao.kt.

Tệp FakeTaskDao.kt trong cấu trúc thư mục Dự án.

Hình 18. Ảnh chụp màn hình cho thấy FakeTaskDao.kt trong cấu trúc thư mục Dự án.

  1. Thêm các nội dung sau:
class FakeTaskDao(initialTasks: List<LocalTask>) : TaskDao {

    private val _tasks = initialTasks.toMutableList()
    private val tasksStream = MutableStateFlow(_tasks.toList())

    override fun observeAll(): Flow<List<LocalTask>> = tasksStream

    override suspend fun upsert(task: LocalTask) {
        _tasks.removeIf { it.id == task.id }
        _tasks.add(task)
        tasksStream.emit(_tasks)
    }

    override suspend fun upsertAll(tasks: List<LocalTask>) {
        val newTaskIds = tasks.map { it.id }
        _tasks.removeIf { newTaskIds.contains(it.id) }
        _tasks.addAll(tasks)
    }

    override suspend fun updateCompleted(taskId: String, completed: Boolean) {
        _tasks.firstOrNull { it.id == taskId }?.let { it.isCompleted = completed }
        tasksStream.emit(_tasks)
    }

    override suspend fun deleteAll() {
        _tasks.clear()
        tasksStream.emit(_tasks)
    }
}

Trong một ứng dụng thực tế, bạn cũng sẽ tạo một phần phụ thuộc giả để thay thế TaskNetworkDataSource (bằng cách cho đối tượng giả và đối tượng thực triển khai một giao diện chung), nhưng bạn sẽ sử dụng trực tiếp lớp này để phục vụ mục đích của lớp học lập trình này.

  1. Bên trong DefaultTaskRepositoryTest, hãy thêm nội dung sau.

Quy tắc được thiết lập để sử dụng trình điều phối chính trong tất cả chương trình kiểm thử.

Một số dữ liệu kiểm thử.

Các phần phụ thuộc kiểm thử đối với nguồn dữ liệu cục bộ và nguồn dữ liệu mạng.

Đối tượng kiểm thử: DefaultTaskRepository.

class DefaultTaskRepositoryTest {

    private var testDispatcher = UnconfinedTestDispatcher()
    private var testScope = TestScope(testDispatcher)

    private val localTasks = listOf(
        LocalTask(id = "1", title = "title1", description = "description1", isCompleted = false),
        LocalTask(id = "2", title = "title2", description = "description2", isCompleted = true),
    )

    private val localDataSource = FakeTaskDao(localTasks)
    private val networkDataSource = TaskNetworkDataSource()
    private val taskRepository = DefaultTaskRepository(
        localDataSource = localDataSource,
        networkDataSource = networkDataSource,
        dispatcher = testDispatcher,
        scope = testScope
    )
}

Tuyệt vời! Bây giờ, bạn có thể bắt đầu viết chương trình kiểm thử đơn vị. Có 3 khía cạnh chính mà bạn nên kiểm thử: đọc, ghi và đồng bộ hoá dữ liệu.

Kiểm thử dữ liệu hiển thị

Dưới đây là cách bạn có thể kiểm tra xem kho lưu trữ có cho thấy dữ liệu chính xác hay không. Chương trình kiểm thử được tạo theo cấu trúc given, when, then. Ví dụ:

Điều kiện

Nguồn dữ liệu cục bộ hiện có một số công việc

Thời điểm

Luồng công việc lấy từ kho lưu trữ bằng cách sử dụng observeAll

Kết quả

Mục đầu tiên trong luồng công việc khớp với phần trình bày bên ngoài của các công việc trong nguồn dữ liệu cục bộ

  • Tạo chương trình kiểm thử có tên observeAll_exposesLocalData với nội dung sau:
@Test
fun observeAll_exposesLocalData() = runTest {
    val tasks = taskRepository.observeAll().first()
    assertEquals(localTasks.toExternal(), tasks)
}

Dùng hàm first để lấy mục đầu tiên từ luồng công việc.

Kiểm thử việc cập nhật dữ liệu

Tiếp theo, hãy viết chương trình kiểm thử xác minh rằng một công việc được tạo và lưu vào nguồn dữ liệu mạng.

Điều kiện

Cơ sở dữ liệu trống

Thời điểm

Một công việc sẽ được tạo bằng cách gọi create

Kết quả

Công việc sẽ được tạo trong cả nguồn dữ liệu cục bộ và nguồn dữ liệu mạng

  1. Tạo chương trình kiểm thử có tên onTaskCreation_localAndNetworkAreUpdated.
@Test
    fun onTaskCreation_localAndNetworkAreUpdated() = testScope.runTest {
        val newTaskId = taskRepository.create(
            localTasks[0].title,
            localTasks[0].description
        )

        val localTasks = localDataSource.observeAll().first()
        assertEquals(true, localTasks.map { it.id }.contains(newTaskId))

        val networkTasks = networkDataSource.loadTasks()
        assertEquals(true, networkTasks.map { it.id }.contains(newTaskId))
    }

Tiếp theo, hãy xác minh rằng khi hoàn thành một công việc, công việc được ghi chính xác vào nguồn dữ liệu cục bộ và được lưu vào nguồn dữ liệu mạng.

Điều kiện

Nguồn dữ liệu cục bộ chứa một công việc

Thời điểm

Công việc này có thể được hoàn tất bằng cách gọi complete

Kết quả

Dữ liệu cục bộ và dữ liệu mạng cũng sẽ được cập nhật

  1. Tạo chương trình kiểm thử có tên onTaskCompletion_localAndNetworkAreUpdated.
    @Test
    fun onTaskCompletion_localAndNetworkAreUpdated() = testScope.runTest {
        taskRepository.complete("1")

        val localTasks = localDataSource.observeAll().first()
        val isLocalTaskComplete = localTasks.firstOrNull { it.id == "1" } ?.isCompleted
        assertEquals(true, isLocalTaskComplete)

        val networkTasks = networkDataSource.loadTasks()
        val isNetworkTaskComplete =
            networkTasks.firstOrNull { it.id == "1"} ?.status == NetworkTask.TaskStatus.COMPLETE
        assertEquals(true, isNetworkTaskComplete)
    }

Làm mới dữ liệu kiểm thử

Cuối cùng, hãy kiểm thử để đảm bảo rằng tác vụ làm mới thành công.

Điều kiện

Nguồn dữ liệu mạng chứa dữ liệu

Thời điểm

refresh sẽ được gọi

Kết quả

dữ liệu cục bộ giống với dữ liệu mạng

  • Tạo chương trình kiểm thử có tên onRefresh_localIsEqualToNetwork
@Test
    fun onRefresh_localIsEqualToNetwork() = runTest {
        val networkTasks = listOf(
            NetworkTask(id = "3", title = "title3", shortDescription = "desc3"),
            NetworkTask(id = "4", title = "title4", shortDescription = "desc4"),
        )
        networkDataSource.saveTasks(networkTasks)

        taskRepository.refresh()

        assertEquals(networkTasks.toLocal(), localDataSource.observeAll().first())
    }

Vậy là xong! Hãy chạy chương trình kiểm thử và tất cả đều thành công.

9. Cập nhật lớp giao diện người dùng

Bây giờ, bạn đã biết lớp dữ liệu đã hoạt động, đã đến lúc kết nối lớp này với lớp giao diện người dùng.

Cập nhật mô hình chế độ xem cho màn hình danh sách công việc

Bắt đầu với TasksViewModel Đây là mô hình chế độ xem để hiện màn hình đầu tiên trong ứng dụng – danh sách tất cả thao tác đang hoạt động.

  1. Mở lớp này và thêm DefaultTaskRepository làm tham số hàm khởi tạo.
@HiltViewModel
class TasksViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
    private val taskRepository: DefaultTaskRepository,
) : ViewModel () { /* ... */ }
  1. Khởi tạo biến tasksStream bằng kho lưu trữ.
private val tasksStream = taskRepository.observeAll()

Mô hình chế độ xem của bạn hiện có quyền truy cập vào tất cả các công việc do kho lưu trữ cung cấp và sẽ nhận được một danh sách công việc mới mỗi khi dữ liệu thay đổi – chỉ với một dòng mã!

  1. Tất cả việc còn lại là kết nối các hành động của người dùng với các phương thức tương ứng trong kho lưu trữ. Tìm phương thức complete và cập nhật thành:
fun complete(task: Task, completed: Boolean) {
    viewModelScope.launch {
        if (completed) {
            taskRepository.complete(task.id)
            showSnackbarMessage(R.string.task_marked_complete)
        } else {
            ...
       }
    }
}
  1. Làm tương tự với refresh.
    fun refresh() {
        _isLoading.value = true
        viewModelScope.launch {
            taskRepository.refresh()
            _isLoading.value = false
        }
    }

Cập nhật mô hình chế độ xem cho màn hình thêm công việc

  1. Mở AddEditTaskViewModel và thêm DefaultTaskRepository làm tham số hàm khởi tạo, giống như cách đã làm trong bước trước.
class AddEditTaskViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val taskRepository: DefaultTaskRepository,
)
  1. Cập nhật phương thức create như sau:
    private fun createNewTask() = viewModelScope.launch {
        taskRepository.create(uiState.value.title, uiState.value.description)
        _uiState.update {
            it.copy(isTaskSaved = true)
        }
    }

Chạy ứng dụng

  1. Đó là khoảnh khắc bạn đã chờ đợi—đã đến lúc chạy ứng dụng. Bạn sẽ thấy màn hình hiển thị Bạn không có công việc nào!.

Màn hình công việc của ứng dụng khi không có công việc nào.

Hình 19. Ảnh chụp màn hình công việc của ứng dụng khi không có công việc nào.

  1. Nhấn vào ba dấu chấm ở góc trên cùng bên phải rồi nhấn vào Làm mới.

Màn hình các công việc của ứng dụng hiển thị trình đơn thao tác.

Hình 20. Ảnh chụp màn hình công việc của ứng dụng đang hiển thị trình đơn thao tác.

Bạn sẽ thấy một vòng quay đang tải xuất hiện trong hai giây, sau đó những công việc kiểm thử mà bạn đã thêm trước đó sẽ xuất hiện.

Màn hình công việc của ứng dụng với hai công việc được hiển thị.

Hình 21. Ảnh chụp màn hình công việc của ứng dụng, trong đó có hai công việc được hiển thị.

  1. Giờ hãy nhấn vào dấu cộng ở góc dưới cùng bên phải để thêm công việc mới. Điền vào các trường tiêu đề và mô tả.

Màn hình thêm công việc của ứng dụng.

Hình 22. Ảnh chụp màn hình thêm công việc của ứng dụng.

  1. Nhấn vào nút đánh dấu ở góc dưới bên phải để lưu công việc.

Màn hình các công việc của ứng dụng sau khi một công việc được thêm vào.

Hình 23. Ảnh chụp màn hình các công việc của ứng dụng sau khi bạn thêm một công việc.

  1. Chọn hộp kiểm bên cạnh công việc để đánh dấu công việc đó là hoàn thành.

Màn hình công việc của ứng dụng cho thấy một công việc đã hoàn thành.

Hình 24. Ảnh chụp màn hình các công việc của ứng dụng, cho thấy một công việc đã hoàn thành.

10. Xin chúc mừng!

Bạn đã tạo thành công lớp dữ liệu cho một ứng dụng.

Lớp dữ liệu đóng vai trò quan trọng trong cấu trúc ứng dụng. Đây là nền tảng mà bạn có thể dựa vào để xây dựng các lớp khác, vì vậy việc xây dựng nền tảng đó sao cho phù hợp sẽ giúp ứng dụng của bạn mở rộng quy mô phù hợp với nhu cầu của người dùng và doanh nghiệp của bạn.

Kiến thức bạn học được

  • Vai trò của lớp dữ liệu trong cấu trúc ứng dụng Android.
  • Cách tạo mô hình dữ liệu và nguồn dữ liệu.
  • Vai trò của kho lưu trữ, cũng như cách kho lưu trữ hiển thị dữ liệu và cung cấp phương thức dùng một lần để cập nhật dữ liệu.
  • Thời điểm thay đổi trình điều phối coroutine và lý do cần thực hiện việc này.
  • Đồng bộ hoá dữ liệu bằng nhiều nguồn dữ liệu.
  • Cách tạo chương trình kiểm thử đơn vị và kiểm thử đo lường cho các lớp lớp dữ liệu phổ biến.

Thử thách nâng cao

Nếu bạn muốn thử thách mình thêm, hãy triển khai các tính năng sau:

  • Kích hoạt lại công việc sau khi đánh dấu công việc đó là hoàn thành.
  • Chỉnh sửa tiêu đề và nội dung mô tả của công việc bằng cách nhấn vào công việc.

Sẽ không có hướng dẫn nào — tất cả hoàn toàn tuỳ thuộc vào bạn! Nếu bạn gặp khó khăn, hãy xem ứng dụng với đầy đủ chức năng trên nhánh main.

git checkout main

Các bước tiếp theo

Để tìm hiểu thêm về lớp dữ liệu, hãy tham khảo tài liệu chính thức và hướng dẫn về ứng dụng ưu tiên dùng chế độ ngoại tuyến. Bạn cũng có thể tìm hiểu về các lớp kiến trúc khác – lớp giao diện người dùnglớp miền.

Để xem mẫu phức tạp hơn và thực tế hơn, hãy tham khảo ứng dụng Now in Android.