Duy trì dữ liệu thông qua Room

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

Hầu hết ứng dụng đủ chất lượng để phát hành công khai đều có dữ liệu mà ứng dụng cần duy trì. Ví dụ: có thể ứng dụng đó lưu trữ danh sách phát bài hát, các mục trong danh sách việc cần làm, bản ghi chi phí và thu nhập, danh mục chòm sao hoặc bản ghi dữ liệu cá nhân. Trong những trường hợp sử dụng này, bạn sẽ sử dụng cơ sở dữ liệu để lưu trữ những dữ liệu cố định (persistent data).

Room là một thư viện dữ liệu cố định, thuộc Jetpack của Android. Room là một tầng trừu tượng ở đầu cơ sở dữ liệu SQLite. SQLite sử dụng một ngôn ngữ chuyên biệt (SQL) để thực hiện các thao tác liên quan đến cơ sở dữ liệu. Thay vì trực tiếp sử dụng SQLite, Room đơn giản hoá các công việc thiết lập cơ sở dữ liệu, định cấu hình và tương tác với ứng dụng. Room cũng cho phép kiểm tra thời gian biên dịch của các câu lệnh SQLite.

Tầng trừu tượng (abstraction layer) là một tập hợp gồm nhiều hàm giúp ẩn phương thức triển khai/độ phức tạp cơ bản. Tầng trừu tượng cung cấp giao diện cho một nhóm chức năng hiện có, chẳng hạn như SQLite trong trường hợp này.

Hình ảnh dưới đây cho thấy cách Room (trong vai trò nguồn dữ liệu) hoạt động với cấu trúc tổng thể được đề xuất trong khoá học này. Room là một Nguồn dữ liệu.

tầng dữ liệu (data layer) chứa kho lưu trữ (repository) và nguồn dữ liệu

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

  • Có thể xây dựng giao diện người dùng (UI) cơ bản cho một ứng dụng Android bằng Jetpack Compose.
  • Có thể sử dụng các thành phần kết hợp (composable) như Text, Icon, IconButtonLazyColumn.
  • Có thể sử dụng thành phần kết hợp NavHost để xác định các tuyến và màn hình trong ứng dụng.
  • Có thể điều hướng giữa các màn hình bằng NavHostController.
  • Quen thuộc với thành phần cấu trúc Android ViewModel. Có thể sử dụng ViewModelProvider.Factory để tạo ViewModel.
  • Quen thuộc với các nguyên tắc cơ bản về cơ chế xử lý đồng thời.
  • Có thể sử dụng coroutine cho các tác vụ chạy trong thời gian dài.
  • Có kiến thức cơ bản về cơ sở dữ liệu SQLite và ngôn ngữ SQL.

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

  • Cách tạo và tương tác với cơ sở dữ liệu SQLite bằng thư viện Room.
  • Cách tạo thực thể, đối tượng truy cập dữ liệu (DAO) và các lớp cơ sở dữ liệu.
  • Cách sử dụng DAO để ánh xạ hàm Kotlin đến truy vấn SQL.

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

  • Bạn sẽ tạo một ứng dụng Inventory (Kiểm kho) có chức năng lưu các mặt hàng tồn kho vào cơ sở dữ liệu SQLite.

Bạn cần có

  • Đoạn mã khởi đầu cho ứng dụng Inventory
  • Máy tính đã cài đặt Android Studio
  • Thiết bị hoặc trình mô phỏng có API cấp 26 trở lên

2. Tổng quan về ứng dụng

Trong lớp học lập trình này, bạn sẽ xử lý một đoạn mã khởi đầu của ứng dụng Inventory và dùng thư viện Room để thêm tầng cơ sở dữ liệu vào ứng dụng này. Phiên bản hoàn thiện của ứng dụng cho thấy một danh sách các mặt hàng qua cơ sở dữ liệu kho hàng. Người dùng có thể chọn thêm mặt hàng mới, cập nhật mặt hàng hiện có và xoá mặt hàng trong cơ sở dữ liệu kho hàng. Đối với lớp học lập trình này, bạn sẽ lưu dữ liệu mặt hàng vào cơ sở dữ liệu Room. Bạn sẽ hoàn tất chức năng còn lại của ứng dụng trong lớp học lập trình tiếp theo.

Màn hình điện thoại hiện các mặt hàng tồn kho

Màn hình Add item (Thêm mặt hàng) hiện trên màn hình điện thoại.

Màn hình điện thoại kèm theo chi tiết mặt hàng đã được điền.

3. Tổng quan về ứng dụng khởi đầu

Tải đoạn mã khởi đầu cho lớp học lập trình này

Để bắt đầu, hãy tải đoạn mã khởi đầu xuống:

Ngoài ra, bạn có thể sao chép kho lưu trữ GitHub cho mã:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-inventory-app.git
$ cd basic-android-kotlin-compose-training-inventory-app
$ git checkout starter

Bạn có thể duyệt xem đoạn mã này trong kho lưu trữ GitHub Inventory app.

Tổng quan về đoạn mã khởi đầu

  1. Mở dự án có đoạn mã khởi đầu trong Android Studio.
  2. Chạy ứng dụng trên thiết bị Android hoặc trên trình mô phỏng. Đảm bảo trình mô phỏng hoặc thiết bị được kết nối chạy API cấp 26 trở lên. Trình kiểm tra cơ sở dữ liệu hoạt động trên trình mô phỏng/thiết bị chạy API cấp 26 trở lên.
  1. Lưu ý rằng ứng dụng không cho thấy dữ liệu kho hàng.
  2. Hãy nhấn vào nút hành động nổi (FAB). Nút này cho phép bạn thêm các mặt hàng mới vào cơ sở dữ liệu.

Ứng dụng sẽ chuyển đến màn hình mới để bạn có thể nhập thông tin chi tiết về mặt hàng mới.

Màn hình điện thoại cho thấy kho hàng trống

Màn hình Add item (Thêm mặt hàng) hiện trên màn hình điện thoại.

Vấn đề trong đoạn mã khởi đầu

  1. Trên màn hình Add Item (Thêm mặt hàng), hãy nhập thông tin chi tiết của một mặt hàng như tên, giá và số lượng của mặt hàng đó.
  2. Nhấn vào Save (Lưu). Màn hình Add Item (Thêm mặt hàng) không đóng, nhưng bạn có thể quay lại bằng phím Quay lại. Chức năng lưu chưa được triển khai, vì vậy, thông tin chi tiết về mặt hàng không được lưu.

Hãy để ý rằng ứng dụng chưa hoàn thiện và chức năng của nút Save (Lưu) chưa được triển khai.

Màn hình điện thoại kèm theo chi tiết mặt hàng đã được điền.

Trong lớp học lập trình này, bạn sẽ thêm đoạn mã sử dụng Room để lưu thông tin kho hàng trong cơ sở dữ liệu SQLite. Bạn sẽ dùng thư viện Room về dữ liệu cố định để tương tác với cơ sở dữ liệu SQLite.

Hướng dẫn về đoạn mã

Đoạn mã khởi đầu bạn tải xuống có bố cục màn hình được thiết kế sẵn cho bạn. Trong lộ trình này, bạn sẽ tập trung vào việc triển khai logic cơ sở dữ liệu. Phần sau đây là hướng dẫn ngắn gọn về một số tệp để bạn bắt đầu.

ui/home/HomeScreen.kt

Tệp này là màn hình chính hoặc màn hình đầu tiên của ứng dụng, chứa thành phần kết hợp để cho bạn thấy danh sách mặt hàng tồn kho. Tệp này có FAB + để thêm mặt hàng mới vào danh sách. Theo lộ trình học tập thì bạn sẽ cho thấy các mặt hàng trong danh sách ở phần sau.

Màn hình điện thoại hiện các mặt hàng tồn kho

ui/item/ItemEntryScreen.kt

Màn hình này tương tự như ItemEditScreen.kt. Cả hai đều có trường văn bản để cho thấy thông tin chi tiết về mặt hàng. Màn hình này xuất hiện khi người dùng nhấn vào FAB trên màn hình chính. ItemEntryViewModel.ktViewModel tương ứng cho màn hình này.

Màn hình điện thoại kèm theo chi tiết mặt hàng đã được điền.

ui/navigation/InventoryNavGraph.kt

Tệp này là biểu đồ điều hướng cho toàn bộ ứng dụng.

4. Các thành phần chính của Room

Kotlin giúp bạn xử lý dữ liệu dễ dàng thông qua các lớp dữ liệu (data class). Tuy bạn có thể dễ dàng xử lý dữ liệu trong bộ nhớ bằng cách sử dụng lớp dữ liệu, nhưng khi nói đến việc duy trì dữ liệu, bạn cần chuyển đổi dữ liệu này thành định dạng tương thích với dung lượng lưu trữ cơ sở dữ liệu. Để làm như vậy, bạn cần các bảng để lưu trữ dữ liệu và truy vấn để truy cập và sửa đổi dữ liệu.

Ba thành phần sau của Room giúp cho các quy trình này diễn ra liền mạch.

  • Thực thể Room đại diện cho bảng trong cơ sở dữ liệu của ứng dụng. Bạn dùng các thực thể này để cập nhật dữ liệu được lưu trữ trong các hàng trong bảng và nhằm tạo hàng mới để chèn.
  • DAO (đối tượng truy cập dữ liệu) của Room cung cấp các phương thức mà ứng dụng của bạn sử dụng để truy xuất, cập nhật, chèn và xoá dữ liệu trong cơ sở dữ liệu.
  • Lớp cơ sở dữ liệu Room là lớp cơ sở dữ liệu cung cấp cho ứng dụng của bạn các phiên bản DAO được liên kết với cơ sở dữ liệu đó.

Bạn sẽ triển khai và tìm hiểu thêm về các thành phần này trong phần sau của lớp học lập trình này. Sơ đồ dưới đây minh hoạ cách các thành phần của Room hoạt động cùng nhau để tương tác với cơ sở dữ liệu.

b95ee603605526c1.png

Thêm phần phụ thuộc Room

Trong nhiệm vụ này, bạn sẽ thêm các thư viện thành phần Room cần có vào tệp Gradle.

  1. Mở tệp gradle cấp mô-đun build.gradle.kts (Module: InventoryApp.app).
  2. Trong khối dependencies, hãy thêm các phần phụ thuộc cho thư viện Room như minh hoạ trong mã sau.
//Room
implementation("androidx.room:room-runtime:${rootProject.extra["room_version"]}")
ksp("androidx.room:room-compiler:${rootProject.extra["room_version"]}")
implementation("androidx.room:room-ktx:${rootProject.extra["room_version"]}")

KSP là một API mạnh mẽ và đơn giản để phân tích cú pháp các chú giải Kotlin.

5. Tạo Thực thể (Entity) cho mặt hàng

Lớp Thực thể (Entity) định nghĩa một bảng và mỗi phiên bản thể hiện của lớp này đại diện cho một hàng trong bảng cơ sở dữ liệu. Lớp thực thể có các mục ánh xạ để cho Room biết cách lớp này định trình bày và tương tác với thông tin trong cơ sở dữ liệu. Trong ứng dụng của bạn, thực thể này giữ thông tin về các mặt hàng tồn kho, chẳng hạn như tên mặt hàng, giá mặt hàng và số lượng mặt hàng hiện có.

ee0ef2847ddcbe91.png

Thẻ chú giải @Entity đánh dấu một lớp là lớp Thực thể (Entity) trong cơ sở dữ liệu. Đối với mỗi lớp Thực thể, ứng dụng sẽ tạo một bảng cơ sở dữ liệu để lưu các mặt hàng. Mỗi trường của Thực thể được biểu thị dưới dạng một cột trong cơ sở dữ liệu, trừ phi trường đó được thể hiện dưới dạng khác (hãy xem tài liệu về Thực thể để biết thông tin chi tiết). Mọi phiên bản của thực thể được lưu trữ trong cơ sở dữ liệu phải có một khoá chính. Khoá chính (primary key) được dùng để định nghĩa riêng biệt bản ghi/mục nhập trong bảng cơ sở dữ liệu của bạn. Khoá chính đại diện cho đối tượng thực thể, miễn là đối tượng đó tồn tại trong cơ sở dữ liệu. Sau khi ứng dụng chỉ định một khoá chính, bạn sẽ không thể sửa đổi khoá này.

Trong nhiệm vụ này, bạn sẽ tạo một lớp Thực thể và định nghĩa các trường để lưu trữ thông tin kho hàng sau đây đối với từng mặt hàng: Int để lưu trữ khoá chính, String để lưu trữ tên mặt hàng, double để lưu trữ giá của mặt hàng và một Int để lưu trữ số lượng trong kho.

  1. Mở đoạn mã khởi đầu trong Android Studio.
  2. Mở gói data trong gói cơ sở com.example.inventory.
  3. Bên trong gói data, hãy mở lớp Kotlin Item, đại diện cho thực thể cơ sở dữ liệu trong ứng dụng.
// No need to copy over, this is part of the starter code
class Item(
    val id: Int,
    val name: String,
    val price: Double,
    val quantity: Int
)

Lớp dữ liệu

Các lớp dữ liệu chủ yếu được dùng để lưu giữ dữ liệu trong Kotlin. Các lớp này được định nghĩa bằng từ khoá data. Các đối tượng lớp dữ liệu trong Kotlin cung cấp thêm cho bạn một số lợi ích. Ví dụ: trình biên dịch tự động tạo ra các tiện ích để so sánh, in và sao chép, chẳng hạn như toString(), copy()equals().

Ví dụ:

// Example data class with 2 properties.
data class User(val firstName: String, val lastName: String){
}

Để đảm bảo tính nhất quán và hành vi có ý nghĩa của mã được tạo, các lớp dữ liệu phải đáp ứng những yêu cầu sau:

  • Hàm khởi tạo chính phải có ít nhất một tham số.
  • Tất cả tham số của hàm khởi tạo chính phải là val hoặc var.
  • Các lớp dữ liệu không được có trạng thái abstract, open, hoặc sealed.

Để tìm hiểu thêm về các lớp Dữ liệu (Data), hãy xem tài liệu về Lớp dữ liệu.

  1. Thêm tiền tố cho phần định nghĩa lớp Item vào từ khoá data để chuyển đổi lớp đó thành một lớp dữ liệu.
data class Item(
    val id: Int,
    val name: String,
    val price: Double,
    val quantity: Int
)
  1. Phía trên phần khai báo cho lớp Item, hãy chú giải lớp dữ liệu bằng @Entity. Sử dụng đối số tableName để thiết lập items làm tên bảng SQLite.
import androidx.room.Entity

@Entity(tableName = "items")
data class Item(
   ...
)
  1. Chú giải thuộc tính id bằng @PrimaryKey để thiết lập id làm khoá chính. Khoá chính là giá trị nhận dạng duy nhất cho mỗi bản ghi/mục nhập trong bảng Item
import androidx.room.PrimaryKey

@Entity(tableName = "items")
data class Item(
    @PrimaryKey
    val id: Int,
    ...
)
  1. Gán giá trị mặc định 0 cho id. Đây là giá trị cần thiết để id tự động tạo ra các giá trị id.
  2. Hãy thiết lập tham số autoGenerate thành true để Room tạo giá trị nhận dạng tăng dần cho mỗi thực thể. Phương pháp này đảm bảo rằng giá trị nhận dạng của mỗi mặt hàng là giá trị duy nhất.
data class Item(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    // ...
)

Trong nhiệm vụ tiếp theo, bạn sẽ xác định giao diện truy cập cơ sở dữ liệu.

6. Tạo đối tượng truy cập dữ liệu cho mặt hàng

Đối tượng truy cập dữ liệu (DAO) là một mẫu bạn có thể dùng để tách lớp cố định khỏi phần còn lại của ứng dụng bằng cách cung cấp một giao diện trừu tượng. Cách phân tách này tuân theo nguyên tắc trách nhiệm duy nhất mà bạn đã thấy trong các lớp học lập trình trước đây.

Chức năng của DAO là ẩn mọi chi tiết phức tạp liên quan đến việc thực hiện các thao tác cơ sở dữ liệu trong tầng cố định cơ sở khỏi phần còn lại của ứng dụng. Điều này cho phép bạn thay đổi lớp dữ liệu một cách độc lập với mã sử dụng dữ liệu đó.

6d40734740c0cffc.png

Trong nhiệm vụ này, bạn sẽ khai báo DAO cho Room. DAO là các thành phần chính của Room, chịu trách nhiệm định nghĩa giao diện truy cập vào cơ sở dữ liệu.

DAO mà bạn tạo là một giao diện tuỳ chỉnh cung cấp các phương thức tiện lợi để truy vấn/truy xuất, chèn, xoá và cập nhật cơ sở dữ liệu. Room sẽ tạo mã phương thức triển khai của lớp này tại thời điểm biên dịch.

Thư viện Room cung cấp cho bạn các chú giải tiện lợi như @Insert, @Delete@Update, để xác định các phương thức thực hiện thao tác chèn, xoá và cập nhật đơn giản mà không cần viết câu lệnh SQL.

Nếu bạn cần xác định các thao tác phức tạp hơn để chèn, xoá, cập nhật hoặc cần truy vấn dữ liệu trong cơ sở dữ liệu, hãy dùng chú giải @Query.

Bên cạnh đó, khi bạn viết truy vấn trong Android Studio, trình biên dịch sẽ kiểm tra các truy vấn SQL của bạn để tìm lỗi cú pháp.

Đối với ứng dụng Inventory (Kiểm kho), bạn cần tính năng giúp thực hiện những việc sau:

  • Chèn hoặc thêm mặt hàng mới.
  • Cập nhật mặt hàng hiện có để thay đổi tên, giá và số lượng.
  • Tìm một mặt hàng cụ thể dựa trên khoá chính của mặt hàng đó (id).
  • Tìm tất cả mặt hàng để bạn có thể cho hiện các mặt hàng đó.
  • Xoá một mục trong cơ sở dữ liệu.

286d6a1799c173c9.png

Hãy hoàn tất các bước sau để triển khai DAO của mặt hàng trong ứng dụng:

  1. Trong gói data, hãy tạo giao diện Kotlin ItemDao.kt.

trường tên được điền là "ItemDao"

  1. Chú giải giao diện ItemDao bằng @Dao.
import androidx.room.Dao

@Dao
interface ItemDao {
}
  1. Bên trong phần nội dung của giao diện, hãy thêm chú giải @Insert.
  2. Bên dưới @Insert, hãy thêm hàm insert(), hàm này lấy phiên bản thể hiện của lớp Entity của item làm đối số.
  3. Đánh dấu hàm này bằng từ khoá suspend để chạy trên một luồng riêng.

Có thể mất nhiều thời gian để thực thi các thao tác với cơ sở dữ liệu, nên sẽ cần phải chạy trên một luồng riêng. Room không cho phép truy cập cơ sở dữ liệu trên luồng chính.

import androidx.room.Insert

@Insert
suspend fun insert(item: Item)

Khi chèn các mặt hàng vào cơ sở dữ liệu, xung đột có thể xảy ra. Ví dụ: nhiều vị trí trong mã cố gắng cập nhật thực thể bằng các giá trị khác biệt và xung đột với nhau, chẳng hạn như cùng một khoá chính. Thực thể là một hàng trong DB. Trong ứng dụng Inventory (Kiểm kho), chúng ta chỉ chèn thực thể từ một nơi là màn hình Add Item (Thêm mặt hàng) nên dự kiến sẽ không gặp bất kỳ xung đột nào và có thể đặt chiến lược xung đột thành Ignore (Bỏ qua).

  1. Thêm một đối số onConflict và gán giá trị OnConflictStrategy.IGNORE cho đối số đó.

Đối số onConflict cho Room biết việc cần làm trong trường hợp có xung đột. Chiến lược OnConflictStrategy.IGNORE sẽ bỏ qua một mục mới.

Để tìm hiểu thêm về các chiến lược có thể gây xung đột, hãy xem tài liệu OnConflictStrategy này.

import androidx.room.OnConflictStrategy

@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)

Giờ đây, Room sẽ tạo tất cả mã cần thiết để chèn item vào cơ sở dữ liệu. Khi bạn gọi bất cứ hàm DAO nào được đánh dấu bằng chú thích Room, Room sẽ thực thi truy vấn SQL tương ứng trên cơ sở dữ liệu. Ví dụ: khi bạn gọi phương thức trên, insert() từ mã Kotlin, Room sẽ thực thi một truy vấn SQL để chèn thực thể vào cơ sở dữ liệu.

  1. Thêm một hàm mới có chú thích @Update. Hàm này sẽ lấy Item làm tham số.

Thực thể được cập nhật có cùng khoá với thực thể được truyền vào. Bạn có thể cập nhật một số hoặc tất cả thuộc tính khác của thực thể.

  1. Tương tự như phương thức insert(), hãy đánh dấu hàm này bằng từ khoá suspend.
import androidx.room.Update

@Update
suspend fun update(item: Item)

Thêm một hàm khác có chú thích @Delete để xoá (các) mục và thiết lập hàm đó thành hàm tạm ngưng.

import androidx.room.Delete

@Delete
suspend fun delete(item: Item)

Không có chú giải tiện lợi cho chức năng còn lại. Vì vậy, bạn phải sử dụng chú giải @Query và cung cấp các truy vấn SQLite.

  1. Viết một truy vấn SQLite để truy xuất một mặt hàng cụ thể qua bảng mặt hàng dựa trên id đã cho. Mã sau đây cung cấp cho bạn một truy vấn mẫu chọn tất cả các cột từ items, trong đó id khớp với một giá trị cụ thể và id là giá trị nhận dạng duy nhất.

Ví dụ:

// Example, no need to copy over
SELECT * from items WHERE id = 1
  1. Thêm chú giải @Query.
  2. Sử dụng truy vấn SQLite ở bước trước dưới dạng tham số chuỗi cho chú giải @Query.
  3. Thêm tham số String vào @Query. Đây là truy vấn SQLite để truy xuất một mặt hàng qua bảng danh sách mặt hàng.

Truy vấn hiện cho biết rằng sẽ chọn tất cả các cột từ items, trong đó id khớp với đối số :id. Hãy lưu ý rằng :id sử dụng ký hiệu dấu hai chấm trong truy vấn để tham chiếu các đối số trong hàm.

@Query("SELECT * from items WHERE id = :id")
  1. Sau chú thích @Query, hãy thêm hàm getItem() (nhận đối số Int và trả về Flow<Item>).
import androidx.room.Query
import kotlinx.coroutines.flow.Flow

@Query("SELECT * from items WHERE id = :id")
fun getItem(id: Int): Flow<Item>

Bạn nên sử dụng Flow trong tầng cố định. Với loại dữ liệu trả về là Flow, bạn sẽ nhận được thông báo mỗi khi dữ liệu trong cơ sở dữ liệu thay đổi. Room giúp Flow này luôn cập nhật, nghĩa là bạn chỉ cần định nghĩa rõ ràng dữ liệu một lần. Chế độ thiết lập này rất hữu ích khi cập nhật danh sách hàng tồn kho mà bạn sẽ triển khai trong lớp học lập trình tiếp theo. Do loại dữ liệu trả về Flow nên Room cũng chạy truy vấn trên luồng trong nền. Bạn không cần phải chỉ định rõ ràng thành hàm suspend và gọi trong phạm vi coroutine.

  1. Thêm @Query có hàm getAllItems():
  2. Yêu cầu truy vấn SQLite trả về tất cả cột trong bảng item, theo thứ tự tăng dần.
  3. Yêu cầu getAllItems() trả về danh sách các thực thể Item dưới dạng Flow. Room giúp Flow này luôn cập nhật, nghĩa là bạn chỉ cần xác định rõ ràng dữ liệu một lần.
@Query("SELECT * from items ORDER BY name ASC")
fun getAllItems(): Flow<List<Item>>

Đã hoàn tất ItemDao

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow

@Dao
interface ItemDao {
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(item: Item)

    @Update
    suspend fun update(item: Item)

    @Delete
    suspend fun delete(item: Item)

    @Query("SELECT * from items WHERE id = :id")
    fun getItem(id: Int): Flow<Item>

    @Query("SELECT * from items ORDER BY name ASC")
    fun getAllItems(): Flow<List<Item>>
}
  1. Dù không nhận thấy thay đổi nào, nhưng hãy tạo bản dựng ứng dụng của bạn để đảm bảo ứng dụng không có lỗi.

7. Tạo phiên bản thể hiện Database

Trong nhiệm vụ này, bạn tạo một RoomDatabase sử dụng Entity và DAO từ các nhiệm vụ trước đó. Lớp cơ sở dữ liệu định nghĩa danh sách các thực thể và DAO.

Lớp Database cung cấp cho ứng dụng các phiên bản thể hiện (instance) của các DAO mà bạn định nghĩa. Đổi lại, ứng dụng có thể dùng các DAO đó để truy xuất dữ liệu từ cơ sở dữ liệu dưới dạng phiên bản thể hiện của đối tượng thực thể dữ liệu được liên kết. Ứng dụng cũng có thể dùng các thực thể dữ liệu đã định nghĩa để cập nhật các hàng trong bảng tương ứng hoặc tạo hàng mới để chèn dữ liệu.

Bạn cần tạo một lớp RoomDatabase trừu tượng và chú thích lớp đó bằng @Database. Lớp này có một phương thức trả về phiên bản hiện có của RoomDatabase nếu cơ sở dữ liệu không tồn tại.

Dưới đây là quy trình chung để tải phiên bản thể hiện RoomDatabase:

  • Tạo lớp public abstract, lớp này mở rộng RoomDatabase. Lớp trừu tượng mới mà bạn đã định nghĩa hoạt động như một lớp lưu giữ cơ sở dữ liệu. Lớp mà bạn đã định nghĩa là lớp trừu tượng vì Room tạo phương thức triển khai cho bạn.
  • Chú giải lớp này bằng @Database. Trong các đối số, hãy liệt kê các thực thể cho cơ sở dữ liệu và đặt số hiệu phiên bản.
  • Định nghĩa phương thức hoặc thuộc tính trừu tượng sẽ trả về phiên bản thể hiện ItemDao, và Room sẽ tạo phương thức triển khai cho bạn.
  • Bạn chỉ cần một phiên bản thể hiện của RoomDatabase cho toàn bộ ứng dụng, vì vậy hãy thiết lập RoomDatabase thành một singleton.
  • Chỉ sử dụng Room.databaseBuilder của Room để tạo cơ sở dữ liệu (item_database) khi cơ sở dữ liệu này chưa tồn tại. Nếu không, hãy trả về cơ sở dữ liệu hiện có.

Tạo Cơ sở dữ liệu

  1. Trong gói data, hãy tạo một lớp Kotlin là InventoryDatabase.kt.
  2. Trong tệp InventoryDatabase.kt, hãy thiết lập lớp InventoryDatabase làm lớp abstract, lớp này mở rộng RoomDatabase.
  3. Chú giải lớp này bằng @Database. Bỏ qua lỗi thiếu tham số, bạn sẽ khắc phục lỗi này trong bước tiếp theo.
import androidx.room.Database
import androidx.room.RoomDatabase

@Database
abstract class InventoryDatabase : RoomDatabase() {}

Chú giải @Database yêu cầu một số đối số để Room có thể xây dựng cơ sở dữ liệu.

  1. Hãy chỉ định Item làm lớp duy nhất có danh sách entities.
  2. Thiết lập version thành 1. Bạn phải tăng số hiệu phiên bản mỗi lần thay đổi giản đồ của bảng cơ sở dữ liệu.
  3. Thiết lập exportSchema thành false để không giữ lại các bản sao lưu nhật ký phiên bản giản đồ.
@Database(entities = [Item::class], version = 1, exportSchema = false)
  1. Bên trong phần nội dung của lớp, hãy khai báo hàm trừu tượng trả về ItemDao để cơ sở dữ liệu biết về DAO.
abstract fun itemDao(): ItemDao
  1. Bên dưới hàm trừu tượng này, hãy định nghĩa companion object, cho phép truy cập vào các phương thức để tạo hoặc lấy cơ sở dữ liệu và sử dụng tên lớp làm bộ hạn định.
 companion object {}
  1. Bên trong đối tượng companion, hãy khai báo Instance (biến riêng tư có thể mang giá trị rỗng) cho cơ sở dữ liệu và khởi tạo biến đó đến null.

Biến Instance lưu giữ một tham chiếu đến cơ sở dữ liệu này khi cơ sở dữ liệu được tạo. Điều này giúp đảm bảo chỉ một phiên bản thể hiện của cơ sở dữ liệu được mở tại một thời điểm nhất định, vì cơ sở dữ liệu là một tài nguyên khó tạo và duy trì.

  1. Chú giải Instance bằng @Volatile.

Giá trị của biến volatile không bao giờ được lưu vào bộ nhớ đệm và tất cả lượt đọc và ghi đều đến và đi từ bộ nhớ chính. Các tính năng này giúp đảm bảo giá trị của Instance luôn cập nhật và giống nhau đối với tất cả các luồng thực thi. Tức là khi một luồng thực hiện thay đổi đối với Instance, tất cả luồng khác sẽ thấy thay đổi đó ngay lập tức.

@Volatile
private var Instance: InventoryDatabase? = null
  1. Bên dưới Instance, vẫn bên trong đối tượng companion, hãy khai báo một phương thức getDatabase() có tham số Context mà hàm tạo cơ sở dữ liệu sẽ cần đến.
  2. Trả về một kiểu InventoryDatabase. Sẽ xuất hiện một thông báo lỗi vì getDatabase() chưa trả về giá trị nào.
import android.content.Context

fun getDatabase(context: Context): InventoryDatabase {}

Có thể nhiều luồng sẽ cùng lúc yêu cầu một phiên bản thể hiện của cơ sở dữ liệu, dẫn đến việc có hai cơ sở dữ liệu thay vì chỉ một. Vấn đề này được gọi là tình huống tương tranh (race condition). Việc bao bọc mã để đưa cơ sở dữ liệu vào bên trong khối synchronized đồng nghĩa rằng tại mỗi thời điểm, chỉ có một luồng thực thi có thể nhập khối mã này, qua đó đảm bảo cơ sở dữ liệu chỉ được khởi tạo một lần.

  1. Bên trong getDatabase(), hãy trả về biến Instance, hoặc nếu Instance rỗng, hãy khởi tạo biến đó bên trong khối synchronized{}. Hãy dùng toán tử elvis (?:) để thực hiện thao tác này.
  2. Truyền vào đối tượng đồng hành this. Bạn sẽ sửa lỗi trong các bước sau.
return Instance ?: synchronized(this) { }
  1. Bên trong khối đã đồng bộ hoá, hãy sử dụng trình tạo cơ sở dữ liệu để tải cơ sở dữ liệu. Hãy tiếp tục bỏ qua các lỗi, bạn sẽ khắc phục trong các bước tiếp theo.
import androidx.room.Room

Room.databaseBuilder()
  1. Bên trong khối synchronized, hãy sử dụng trình tạo cơ sở dữ liệu để tải cơ sở dữ liệu. Truyền ngữ cảnh ứng dụng, lớp cơ sở dữ liệu và tên cho cơ sở dữ liệu (item_database) vào Room.databaseBuilder().
Room.databaseBuilder(context, InventoryDatabase::class.java, "item_database")

Android Studio sẽ tạo thông báo lỗi Kiểu không khớp (Type Mismatch). Để xoá lỗi này, bạn phải thêm build() trong các bước sau.

  1. Thêm chiến lược di chuyển cần có vào hàm tạo. Sử dụng .fallbackToDestructiveMigration().
.fallbackToDestructiveMigration()
  1. Để tạo phiên bản thể hiện của cơ sở dữ liệu, hãy gọi .build(). Lệnh gọi này sẽ xoá các thông báo lỗi của Android Studio.
.build()
  1. Sau build(), hãy thêm một khối also và gán Instance = it để giữ lại thông tin tham chiếu đến phiên bản thể hiện db mới tạo gần đây.
.also { Instance = it }
  1. Ở cuối khối synchronized, hãy trả về instance. Mã hoàn chỉnh của bạn sẽ có dạng như sau:
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

/**
* Database class with a singleton Instance object.
*/
@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class InventoryDatabase : RoomDatabase() {

    abstract fun itemDao(): ItemDao

    companion object {
        @Volatile
        private var Instance: InventoryDatabase? = null

        fun getDatabase(context: Context): InventoryDatabase {
            // if the Instance is not null, return it, otherwise create a new database instance.
            return Instance ?: synchronized(this) {
                Room.databaseBuilder(context, InventoryDatabase::class.java, "item_database")
                    .build()
                    .also { Instance = it }
            }
        }
    }
}
  1. Tạo bản dựng mã của bạn để đảm bảo không có lỗi.

8. Triển khai Repository (Kho lưu trữ)

Trong nhiệm vụ này, bạn sẽ triển khai giao diện ItemsRepository và lớp OfflineItemsRepository để cung cấp các thực thể get, insert, deleteupdate từ cơ sở dữ liệu.

  1. Mở tệp ItemsRepository.kt trong gói data.
  2. Thêm các hàm sau vào giao diện, (được liên kết với phương thức triển khai DAO).
import kotlinx.coroutines.flow.Flow

/**
* Repository that provides insert, update, delete, and retrieve of [Item] from a given data source.
*/
interface ItemsRepository {
    /**
     * Retrieve all the items from the given data source.
     */
    fun getAllItemsStream(): Flow<List<Item>>

    /**
     * Retrieve an item from the given data source that matches with the [id].
     */
    fun getItemStream(id: Int): Flow<Item?>

    /**
     * Insert item in the data source
     */
    suspend fun insertItem(item: Item)

    /**
     * Delete item from the data source
     */
    suspend fun deleteItem(item: Item)

    /**
     * Update item in the data source
     */
    suspend fun updateItem(item: Item)
}
  1. Mở tệp OfflineItemsRepository.kt trong gói data.
  2. Truyền vào một tham số hàm khởi tạo thuộc kiểu ItemDao.
class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository
  1. Trong lớp OfflineItemsRepository, hãy ghi đè các hàm được định nghĩa trong giao diện ItemsRepository và gọi các hàm tương ứng từ ItemDao.
import kotlinx.coroutines.flow.Flow

class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository {
    override fun getAllItemsStream(): Flow<List<Item>> = itemDao.getAllItems()

    override fun getItemStream(id: Int): Flow<Item?> = itemDao.getItem(id)

    override suspend fun insertItem(item: Item) = itemDao.insert(item)

    override suspend fun deleteItem(item: Item) = itemDao.delete(item)

    override suspend fun updateItem(item: Item) = itemDao.update(item)
}

Triển khai lớp AppContainer

Trong nhiệm vụ này, bạn sẽ tạo thực thể cơ sở dữ liệu và truyền phiên bản thể hiện DAO đến lớp OfflineItemsRepository.

  1. Mở tệp AppContainer.kt trong gói data.
  2. Truyền thực thể ItemDao() vào hàm khởi tạo OfflineItemsRepository.
  3. Tạo phiên bản thể hiện cơ sở dữ liệu bằng cách gọi getDatabase() trên lớp InventoryDatabase, truyền ngữ cảnh và gọi .itemDao() để tạo phiên bản thể hiện Dao.
override val itemsRepository: ItemsRepository by lazy {
    OfflineItemsRepository(InventoryDatabase.getDatabase(context).itemDao())
}

Hiện bạn đã có tất cả yếu tố nền móng để làm việc với Room. Mã này sẽ biên dịch và chạy được, nhưng bạn không có cách nào để biết mã có thực sự hoạt động hay không. Vì vậy, đây là thời điểm thích hợp để kiểm tra cơ sở dữ liệu của bạn. Để hoàn tất quy trình kiểm thử, bạn cần có ViewModel để trao đổi với cơ sở dữ liệu.

9. Thêm chức năng lưu

Đến nay, bạn đã tạo một cơ sở dữ liệu và các lớp trên giao diện người dùng là một phần của mã khởi động. Để lưu dữ liệu tạm thời của ứng dụng và truy cập cơ sở dữ liệu, bạn cần cập nhật các ViewModel. ViewModel của bạn tương tác với cơ sở dữ liệu thông qua DAO và cung cấp dữ liệu cho giao diện người dùng. Mọi hoạt động liên quan đến cơ sở dữ liệu đều cần chạy khỏi luồng giao diện người dùng chính. Bạn sẽ thực hiện việc này bằng cách sử dụng coroutine và viewModelScope.

Hướng dẫn từng bước về lớp trạng thái giao diện người dùng

Mở tệp ui/item/ItemEntryViewModel.kt. Lớp dữ liệu ItemUiState thể hiện trạng thái giao diện người dùng của một Mục. Lớp dữ liệu ItemDetails biểu thị một mục.

Mã khởi đầu cung cấp cho bạn 3 hàm mở rộng:

  • Hàm mở rộng ItemDetails.toItem() chuyển đổi đối tượng trạng thái giao diện người dùng ItemUiState thành kiểu thực thể Item.
  • Hàm mở rộng Item.toItemUiState() chuyển đổi đối tượng thực thể Room Item thành kiểu trạng thái giao diện người dùng ItemUiState.
  • Hàm mở rộng Item.toItemDetails() chuyển đổi đối tượng thực thể Room Item thành ItemDetails.
// No need to copy, this is part of starter code
/**
* Represents Ui State for an Item.
*/
data class ItemUiState(
    val itemDetails: ItemDetails = ItemDetails(),
    val isEntryValid: Boolean = false
)

data class ItemDetails(
    val id: Int = 0,
    val name: String = "",
    val price: String = "",
    val quantity: String = "",
)

/**
* Extension function to convert [ItemDetails] to [Item]. If the value of [ItemDetails.price] is
* not a valid [Double], then the price will be set to 0.0. Similarly if the value of
* [ItemDetails.quantity] is not a valid [Int], then the quantity will be set to 0
*/
fun ItemDetails.toItem(): Item = Item(
    id = id,
    name = name,
    price = price.toDoubleOrNull() ?: 0.0,
    quantity = quantity.toIntOrNull() ?: 0
)

fun Item.formatedPrice(): String {
    return NumberFormat.getCurrencyInstance().format(price)
}

/**
* Extension function to convert [Item] to [ItemUiState]
*/
fun Item.toItemUiState(isEntryValid: Boolean = false): ItemUiState = ItemUiState(
    itemDetails = this.toItemDetails(),
    isEntryValid = isEntryValid
)

/**
* Extension function to convert [Item] to [ItemDetails]
*/
fun Item.toItemDetails(): ItemDetails = ItemDetails(
    id = id,
    name = name,
    price = price.toString(),
    quantity = quantity.toString()
)

Bạn sẽ sử dụng lớp trên trong các mô hình hiển thị để đọc và cập nhật giao diện người dùng.

Cập nhật ViewModel của ItemEntry

Trong nhiệm vụ này, bạn sẽ truyền từ kho lưu trữ vào tệp ItemEntryViewModel.kt. Bạn cũng sẽ lưu thông tin chi tiết về mặt hàng đã nhập trong màn hình Add Item (Thêm mặt hàng) vào cơ sở dữ liệu.

  1. Hãy chú ý đến hàm riêng tư validateInput() trong lớp ItemEntryViewModel.
// No need to copy over, this is part of starter code
private fun validateInput(uiState: ItemDetails = itemUiState.itemDetails): Boolean {
    return with(uiState) {
        name.isNotBlank() && price.isNotBlank() && quantity.isNotBlank()
    }
}

Hàm trên kiểm tra xem name, pricequantity có trống hay không. Bạn sẽ dùng hàm này để xác minh hoạt động đầu vào của người dùng, trước khi thêm hoặc cập nhật thực thể trong cơ sở dữ liệu.

  1. Mở lớp ItemEntryViewModel và thêm tham số hàm khởi tạo mặc định private thuộc kiểu ItemsRepository.
import com.example.inventory.data.ItemsRepository

class ItemEntryViewModel(private val itemsRepository: ItemsRepository) : ViewModel() {
}
  1. Cập nhật initializer cho mô hình thành phần hiển thị mục nhập trong ui/AppViewModelProvider.kt và truyền vào thực thể lưu trữ dưới dạng tham số.
object AppViewModelProvider {
    val Factory = viewModelFactory {
        // Other Initializers
        // Initializer for ItemEntryViewModel
        initializer {
            ItemEntryViewModel(inventoryApplication().container.itemsRepository)
        }
        //...
    }
}
  1. Chuyển đến tệp ItemEntryViewModel.kt và ở cuối lớp ItemEntryViewModel, sau đó thêm hàm tạm ngưng có tên saveItem() để chèn một mục vào cơ sở dữ liệu Room. Hàm này thêm dữ liệu vào cơ sở dữ liệu theo cách không chặn luồng thực thi.
suspend fun saveItem() {
}
  1. Bên trong hàm này, hãy kiểm tra tính hợp lệ của itemUiState, và chuyển đổi thành Item để Room có thể hiểu được dữ liệu.
  2. Gọi insertItem() trên itemsRepository rồi truyền dữ liệu vào. Giao diện người dùng gọi hàm này để thêm thông tin chi tiết về Item (mặt hàng) vào cơ sở dữ liệu.
suspend fun saveItem() {
    if (validateInput()) {
        itemsRepository.insertItem(itemUiState.itemDetails.toItem())
    }
}

Hiện bạn đã thêm tất cả hàm cần thiết để thêm đối tượng vào cơ sở dữ liệu. Trong nhiệm vụ tiếp theo, bạn sẽ cập nhật giao diện người dùng để sử dụng các hàm trên.

Hướng dẫn từng bước về thành phần kết hợp ItemEntryBody()

  1. Trong tệp ui/item/ItemEntryScreen.kt, thành phần kết hợp ItemEntryBody() được triển khai một phần cho bạn dưới dạng một phần của mã khởi đầu. Hãy xem thành phần kết hợp ItemEntryBody() trong lệnh gọi hàm ItemEntryScreen().
// No need to copy over, part of the starter code
ItemEntryBody(
    itemUiState = viewModel.itemUiState,
    onItemValueChange = viewModel::updateUiState,
    onSaveClick = { },
    modifier = Modifier
        .padding(innerPadding)
        .verticalScroll(rememberScrollState())
        .fillMaxWidth()
)
  1. Lưu ý trạng thái giao diện người dùng và hàm lambda updateUiState đang được truyền dưới dạng tham số hàm. Xem xét phần định nghĩa hàm để thấy trạng thái của giao diện người dùng đang được cập nhật.
// No need to copy over, part of the starter code
@Composable
fun ItemEntryBody(
    itemUiState: ItemUiState,
    onItemValueChange: (ItemUiState) -> Unit,
    onSaveClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        // ...
    ) {
        ItemInputForm(
             itemDetails = itemUiState.itemDetails,
             onValueChange = onItemValueChange,
             modifier = Modifier.fillMaxWidth()
         )
        Button(
             onClick = onSaveClick,
             enabled = itemUiState.isEntryValid,
             shape = MaterialTheme.shapes.small,
             modifier = Modifier.fillMaxWidth()
         ) {
             Text(text = stringResource(R.string.save_action))
         }
    }
}

Bạn đang hiển thị ItemInputForm và nút Save (Lưu) trong thành phần kết hợp này. Trong thành phần kết hợp ItemInputForm(), bạn sẽ hiển thị 3 trường văn bản. Tính năng Lưu chỉ được bật nếu bạn nhập văn bản vào các trường văn bản. Giá trị isEntryValid là true nếu văn bản trong tất cả các trường văn bản là hợp lệ (không trống).

Màn hình điện thoại hiện thông tin mặt hàng đã điền một phần và nút Save (Lưu) đang tắt

Màn hình điện thoại hiện thông tin mặt hàng đã điền và nút Save (Lưu) đang bật

  1. Hãy xem phương thức triển khai hàm có khả năng kết hợp ItemInputForm() và chú ý đến tham số hàm onValueChange. Bạn đang cập nhật giá trị itemDetails bằng giá trị do người dùng nhập vào các trường văn bản. Vào thời điểm bật nút Save (Lưu), itemUiState.itemDetails sẽ có các giá trị cần lưu.
// No need to copy over, part of the starter code
@Composable
fun ItemEntryBody(
    //...
) {
    Column(
        // ...
    ) {
        ItemInputForm(
             itemDetails = itemUiState.itemDetails,
             //...
         )
        //...
    }
}
// No need to copy over, part of the starter code
@Composable
fun ItemInputForm(
    itemDetails: ItemDetails,
    modifier: Modifier = Modifier,
    onValueChange: (ItemUiState) -> Unit = {},
    enabled: Boolean = true
) {
    Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
        OutlinedTextField(
            value = itemUiState.name,
            onValueChange = { onValueChange(itemDetails.copy(name = it)) },
            //...
        )
        OutlinedTextField(
            value = itemUiState.price,
            onValueChange = { onValueChange(itemDetails.copy(price = it)) },
            //...
        )
        OutlinedTextField(
            value = itemUiState.quantity,
            onValueChange = { onValueChange(itemDetails.copy(quantity = it)) },
            //...
        )
    }
}

Thêm trình nghe lượt nhấp vào nút Save (Lưu)

Để liên kết mọi thứ với nhau, hãy thêm một trình xử lý lượt nhấp vào nút Save (Lưu). Bên trong trình xử lý lượt nhấp, hãy khởi chạy một coroutine rồi gọi saveItem() để lưu dữ liệu trong cơ sở dữ liệu Room.

  1. Trong ItemEntryScreen.kt, bên trong hàm hàm có khả năng kết hợp ItemEntryScreen, hãy tạo một val có tên là coroutineScope bằng hàm có khả năng kết hợp rememberCoroutineScope().
import androidx.compose.runtime.rememberCoroutineScope

val coroutineScope = rememberCoroutineScope()
  1. Cập nhật lệnh gọi hàm ItemEntryBody() và chạy một coroutine bên trong hàm lambda onSaveClick.
ItemEntryBody(
   // ...
    onSaveClick = {
        coroutineScope.launch {
        }
    },
    modifier = modifier.padding(innerPadding)
)
  1. Xem xét phương thức triển khai hàm saveItem() trong tệp ItemEntryViewModel.kt để kiểm tra xem itemUiState có hợp lệ hay không, chuyển đổi itemUiState thành loại Item và chèn tệp đó vào cơ sở dữ liệu bằng itemsRepository.insertItem().
// No need to copy over, you have already implemented this as part of the Room implementation

suspend fun saveItem() {
    if (validateInput()) {
        itemsRepository.insertItem(itemUiState.itemDetails.toItem())
    }
}
  1. Trong ItemEntryScreen.kt, bên trong hàm có khả năng kết hợp ItemEntryScreen, bên trong coroutine, hãy gọi viewModel.saveItem() để lưu mục trong cơ sở dữ liệu.
ItemEntryBody(
    // ...
    onSaveClick = {
        coroutineScope.launch {
            viewModel.saveItem()
        }
    },
    //...
)

Xin lưu ý rằng bạn không sử dụng viewModelScope.launch() cho saveItem() trong tệp ItemEntryViewModel.kt, nhưng điều này cần thiết cho ItemEntryBody() khi gọi phương thức kho lưu trữ. Bạn chỉ có thể gọi các hàm tạm ngưng từ một coroutine hoặc một hàm tạm ngưng khác. Hàm viewModel.saveItem() là hàm tạm ngưng.

  1. Tạo bản dựng và chạy ứng dụng của bạn.
  2. Nhấn vào nút hành động nổi +.
  3. Trong màn hình Add Item (Thêm mặt hàng), hãy thêm thông tin chi tiết về mặt hàng rồi nhấn vào Save (Lưu). Lưu ý rằng thao tác nhấn vào nút Save (Lưu) không đóng màn hình Add Item (Thêm mặt hàng).

Màn hình điện thoại hiện thông tin mặt hàng đã điền và nút Save (Lưu) đang bật

  1. Trong hàm lambda onSaveClick, hãy thêm một lệnh gọi tới navigateBack() sau lệnh gọi đến viewModel.saveItem() để quay lại màn hình trước đó. Hàm ItemEntryBody() của bạn sẽ có dạng như mã sau:
ItemEntryBody(
    itemUiState = viewModel.itemUiState,
    onItemValueChange = viewModel::updateUiState,
    onSaveClick = {
        coroutineScope.launch {
            viewModel.saveItem()
            navigateBack()
        }
    },
    modifier = modifier.padding(innerPadding)
)
  1. Chạy lại ứng dụng và thực hiện các bước tương tự để nhập và lưu dữ liệu. Lần này, hãy lưu ý rằng ứng dụng sẽ quay lại màn hình Inventory (Kho hàng).

Thao tác này sẽ lưu dữ liệu, nhưng bạn không thể xem dữ liệu kho hàng trong ứng dụng. Trong nhiệm vụ tiếp theo, bạn sẽ sử dụng Trình kiểm tra cơ sở dữ liệu để xem dữ liệu đã lưu.

Màn hình ứng dụng hiện danh sách kho hàng trống

10. Dùng Trình kiểm tra cơ sở dữ liệu để xem nội dung của cơ sở dữ liệu

Trình kiểm tra cơ sở dữ liệu cho phép bạn kiểm tra, truy vấn và sửa đổi cơ sở dữ liệu của ứng dụng trong khi ứng dụng chạy. Điều này đặc biệt hữu ích khi gỡ lỗi cơ sở dữ liệu. Trình kiểm tra cơ sở dữ liệu hoạt động với SQLite thuần tuý và với các thư viện được xây dựng dựa trên SQLite, chẳng hạn như Room. Trình kiểm tra cơ sở dữ liệu hoạt động hiệu quả nhất trên trình mô phỏng/thiết bị chạy API cấp 26.

  1. Chạy ứng dụng của bạn trên trình mô phỏng hoặc một thiết bị được kết nối chạy API cấp 26 trở lên (nếu bạn chưa thực hiện việc này).
  2. Trong Android Studio, trên thanh trình đơn, hãy chọn View (Xem) > Tool Windows (Cửa sổ công cụ) > App Inspection (Kiểm tra ứng dụng).
  3. Chọn thẻ Database Inspector (Trình kiểm tra cơ sở dữ liệu).
  4. Trong ngăn Database Inspector (Trình kiểm tra cơ sở dữ liệu), hãy chọn com.example.inventory trên trình đơn thả xuống (nếu chưa chọn). item_database trong ứng dụng Kiểm kho xuất hiện trong ngăn Databases (Cơ sở dữ liệu).

6876a506d634ca2a.png

  1. Mở rộng nút (node) cho item_database trong ngăn Databases (Cơ sở dữ liệu) rồi chọn Item (Mục) để kiểm tra. Nếu ngăn Databases (Cơ sở dữ liệu) trống, hãy sử dụng trình mô phỏng để thêm một số mặt hàng vào cơ sở dữ liệu bằng màn hình Add Item (Thêm mặt hàng).
  2. Chọn hộp đánh dấu Live updates (Cập nhật trực tiếp) trong Trình kiểm tra cơ sở dữ liệu để tự động cập nhật những dữ liệu xuất hiện khi bạn tương tác với ứng dụng đang chạy trong trình mô phỏng hoặc thiết bị.

ffd820637ed70b89.png

Xin chúc mừng! Bạn đã sử dụng Room để tạo một ứng dụng có thể duy trì dữ liệu. Trong lớp học lập trình tiếp theo, bạn sẽ thêm lazyColumn vào ứng dụng của mình để cho thấy các mặt hàng trên cơ sở dữ liệu và thêm tính năng mới vào ứng dụng, như khả năng xoá và cập nhật các thực thể. Hẹn gặp bạn ở lớp học này nhé!

11. Lấy đoạn mã giải pháp

Đoạn mã giải pháp cho lớp học lập trình này nằm trong kho lưu trữ GitHub. Để tải đoạn mã cho lớp học lập trình đã hoàn thành này, hãy sử dụng lệnh git sau:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-inventory-app.git
$ cd basic-android-kotlin-compose-training-inventory-app
$ git checkout room

Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp zip rồi giải nén và mở trong Android Studio.

Nếu bạn muốn tham khảo đoạn mã giải pháp cho lớp học lập trình này, hãy xem trên GitHub.

12. Tóm tắt

  • Khai báo các bảng dưới dạng lớp dữ liệu được chú giải bằng @Entity. Khai báo các thuộc tính được chú giải bằng @ColumnInfo dưới dạng cột trong bảng.
  • Khai báo một đối tượng truy cập dữ liệu (DAO) làm giao diện, kèm theo chú giải bằng @Dao. DAO ánh xạ các hàm Kotlin đến các truy vấn cơ sở dữ liệu.
  • Sử dụng chú giải để định nghĩa các hàm @Insert, @Delete@Update.
  • Sử dụng chú giải @Query có chuỗi truy vấn SQLite làm tham số cho mọi truy vấn khác.
  • Sử dụng Trình kiểm tra cơ sở dữ liệu để xem dữ liệu được lưu trong cơ sở dữ liệu Android SQLite.

13. Tìm hiểu thêm

Tài liệu dành cho nhà phát triển Android

Bài đăng trên blog

Video

Các tài liệu và bài viết khác