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.
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
- Đây là lớp học lập trình trung cấp và bạn đã nắm được kiến thức cơ bản về cách phát triển ứng dụng Android (xem tài liệu học tập cho người mới bắt đầu ở phía dưới).
- Kinh nghiệm về Kotlin, bao gồm cả Lambda, Coroutine và Dòng dữ liệu (Flow). Để tìm hiểu cách viết mã Kotlin trong các ứng dụng Android, hãy xem Bài 1 của khoá học Kiến thức cơ bản về Kotlin cho Android.
- Hiểu biết cơ bản về thư viện Hilt (chèn phần phụ thuộc) và Room (lưu trữ cơ sở dữ liệu).
- Một số kinh nghiệm với Jetpack Compose. Học phần 1 đến 3 của Khoá học Khái niệm cơ bản về Compose cho Android là nơi tuyệt vời để bạn tìm hiểu về Compose.
- Không bắt buộc: Đọc hướng dẫn tổng quan về kiến trúc và lớp dữ liệu.
- Không bắt buộc: Hoàn tất Lớp học lập trình về Room.
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.
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
- Tải mã nguồn:
https://github.com/android/architecture-samples/archive/refs/heads/data-codelab-start.zip
- 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
- 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.
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.
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.
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ữ.
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 |
| 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 |
| Nội bộ | Một công việc được lưu trữ trong cơ sở dữ liệu cục bộ |
|
| Nội bộ | Một công việc đã được truy xuất từ máy chủ mạng |
|
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
:
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ị.
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 trongdata/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, LocalTask
là nội bộ đối với lớp dữ liệu và Task
là bê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
.
- Mở
ToDoDatabase.kt
và thay đổiBlankEntity
thànhLocalTask
. - Xoá
BlankEntity
và mọi câu lệnhimport
thừa. - 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àoDatabaseModule
:
@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:
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 |
- 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)
}
- 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).
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.
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.
- Xoá câu lệnh
assertEquals
hiện tại. - 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])
- 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ử.
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ó trongdata/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 LocalTask
và NetworkTask
:
- 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 enumstatus
, có thể có hai giá trị:ACTIVE
vàCOMPLETE
. - 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ênTaskNetworkDataSource
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.
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 (LocalTask
và NetworkTask
).
Đã đế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
- Mở
DefaultTaskRepository.kt
trong góidata
, sau đó tạo một lớp có tênDefaultTaskRepository
. Lớp này sẽ lấyTaskDao
vàTaskNetworkDataSource
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.
- Thêm phương thức có tên
observeAll
. Phương thức này sẽ trả về luồng mô hìnhTask
bằng cách sử dụngFlow
.
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
.
- 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
.
- Sử dụng hàm
toExternal
mới tạo bên trongobserveAll
:
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
.
- Thêm một phương thức có tên
create
. Phương thức này sẽ lấytitle
vàdescription
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.
- 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()
}
- 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.
- Trước tiên, hãy thêm
CoroutineDispatcher
dưới dạng phần phụ thuộc vàoDefaultTaskRepository
. Sử dụng bộ hạn định@DefaultDispatcher
đã tạo (được xác định trongdi/CoroutinesModule.kt
) để yêu cầu Hilt chèn phần phụ thuộc này vớiDispatchers.Default
. Trình điều phốiDefault
đượ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,
)
- Bây giờ, hãy thực hiện lệnh gọi đến
UUID.randomUUID().toString()
bên trong khốiwithContext
.
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
- 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
.
- Thêm hàm mở rộng sau vào cuối
LocalTask
. Đây là hàm ánh xạ ngược đếnLocalTask.toExternal
mà bạn đã tạo trước đó.
fun Task.toLocal() = LocalTask(
id = id,
title = title,
description = description,
isCompleted = isCompleted,
)
- 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ấuTask
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 |
| Tải dữ liệu từ cơ sở dữ liệu cục bộ | 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 |
| 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 | 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 |
| 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 | 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.
- Trước tiên, hãy tạo các hàm ánh xạ từ
LocalTask
đếnNetworkTask
và ngược lại bên trongNetworkTask.kt
. Việc đặt các hàm bên trongLocalTask.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.
- Thêm phương thức
refresh
ở cuốiDefaultTaskRepository
.
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.
- Thêm phương thức
saveTasksToNetwork
vào cuốiDefaultTaskRepository
.
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ộ.
- 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
create
vàcomplete
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 create
và complete
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ả.
- 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.
- Gói mã bên trong
saveTasksToNetwork
bằngscope.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 đó.
- 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ụcsource.local
và mởFakeTaskDao.kt.
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.
- 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.
- 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ử: |
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 |
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 |
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 |
- 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 |
Kết quả | Dữ liệu cục bộ và dữ liệu mạng cũng sẽ được cập nhật |
- 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 |
|
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.
- 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 () { /* ... */ }
- 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ã!
- 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 {
...
}
}
}
- 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
- Mở
AddEditTaskViewModel
và thêmDefaultTaskRepository
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,
)
- 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
- Đó 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!.
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.
- 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.
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.
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ị.
- 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ả.
Hình 22. Ảnh chụp màn hình thêm công việc của ứng dụng.
- Nhấn vào nút đánh dấu ở góc dưới bên phải để lưu công việc.
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.
- 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.
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ùng và lớ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.