Mục đích của Thành phần kiến trúc là cung cấp hướng dẫn về cấu trúc ứng dụng, với các thư viện cho những tác vụ phổ biến như quản lý vòng đời và duy trì dữ liệu. Các thành phần cấu trúc giúp bạn cấu trúc ứng dụng theo cách mạnh mẽ, có thể kiểm thử và duy trì với ít mã nguyên mẫu hơn. Các thư viện Thành phần cấu trúc là một phần của Android Jetpack.
Đây là phiên bản Kotlin của lớp học lập trình. Bạn có thể xem phiên bản bằng ngôn ngữ lập trình Java tại đây.
Nếu bạn gặp vấn đề (lỗi trong đoạn mã, lỗi ngữ pháp, từ ngữ không rõ ràng, v.v.) khi thực hành theo lớp học lập trình này, vui lòng báo cáo vấn đề thông qua đường liên kết Báo cáo lỗi ở góc dưới bên trái lớp học lập trình.
Điều kiện tiên quyết
Bạn cần nắm vững Kotlin, các khái niệm về thiết kế hướng đối tượng và kiến thức cơ bản về phát triển Android, cụ thể là:
RecyclerView
và bộ chuyển đổi- Cơ sở dữ liệu SQLite và ngôn ngữ truy vấn SQLite
- Coroutine cơ bản (Nếu chưa quen với coroutine, bạn có thể xem phần Sử dụng coroutine của Kotlin trong ứng dụng Android.)
Bạn cũng nên làm quen với các mẫu kiến trúc phần mềm tách dữ liệu khỏi giao diện người dùng, chẳng hạn như MVP hoặc MVC. Lớp học lập trình này triển khai kiến trúc được xác định trong Hướng dẫn về cấu trúc ứng dụng.
Lớp học lập trình này tập trung vào Các thành phần kiến trúc Android. Các khái niệm và mã không liên quan được cung cấp để bạn chỉ cần sao chép và dán.
Nếu bạn chưa quen với Kotlin, thì một phiên bản của lớp học lập trình này được cung cấp bằng ngôn ngữ lập trình Java tại đây.
Bạn sẽ thực hiện
Trong lớp học lập trình này, bạn sẽ tìm hiểu cách thiết kế và xây dựng một ứng dụng bằng các Thành phần cấu trúc Room, ViewModel và LiveData, đồng thời xây dựng một ứng dụng có thể làm những việc sau:
- Triển khai cấu trúc được đề xuất bằng cách sử dụng Bộ thành phần cấu trúc Android.
- Hoạt động với cơ sở dữ liệu để lấy và lưu dữ liệu, đồng thời điền sẵn một số từ vào cơ sở dữ liệu.
- Hiển thị tất cả các từ trong một
RecyclerView
trongMainActivity
. - Mở một hoạt động thứ hai khi người dùng nhấn vào nút +. Khi người dùng nhập một từ, hãy thêm từ đó vào cơ sở dữ liệu và danh sách.
Ứng dụng này không có nhiều tính năng, nhưng đủ phức tạp để bạn có thể dùng làm mẫu để phát triển. Dưới đây là bản xem trước:
Bạn cần có
- Android Studio 3.0 trở lên và có kiến thức về cách sử dụng. Đảm bảo bạn đã cập nhật Android Studio, cũng như SDK và Gradle.
- Trình mô phỏng hoặc thiết bị Android.
Lớp học lập trình này cung cấp tất cả mã cần thiết để bạn tạo ứng dụng hoàn chỉnh.
Có rất nhiều bước để sử dụng Thành phần cấu trúc và triển khai cấu trúc được đề xuất. Điều quan trọng nhất là bạn phải tạo ra một mô hình tinh thần về những gì đang diễn ra, hiểu rõ cách các phần kết hợp với nhau và cách dữ liệu lưu chuyển. Khi làm việc trong lớp học lập trình này, đừng chỉ sao chép và dán mã mà hãy cố gắng bắt đầu xây dựng kiến thức nội tại.
Các Thành phần cấu trúc được đề xuất là gì?
Để giới thiệu thuật ngữ, sau đây là phần giới thiệu ngắn gọn về các Thành phần trong cấu trúc và cách chúng phối hợp với nhau. Xin lưu ý rằng lớp học lập trình này tập trung vào một nhóm nhỏ các thành phần, cụ thể là LiveData, ViewModel và Room. Mỗi thành phần sẽ được giải thích thêm khi bạn sử dụng.
Sơ đồ này cho thấy một dạng cơ bản của cấu trúc:
Thực thể: Lớp được chú giải mô tả một bảng cơ sở dữ liệu khi làm việc với Room.
Cơ sở dữ liệu SQLite: Bộ nhớ trên thiết bị. Thư viện lưu trữ Room sẽ tạo và duy trì cơ sở dữ liệu này cho bạn.
DAO: Đối tượng truy cập dữ liệu. Một mối liên kết giữa các truy vấn SQL và hàm. Khi bạn sử dụng DAO, bạn sẽ gọi các phương thức và Room sẽ đảm nhận phần việc còn lại.
Cơ sở dữ liệu Room: Đơn giản hoá công việc liên quan đến cơ sở dữ liệu và đóng vai trò là điểm truy cập vào cơ sở dữ liệu SQLite cơ bản (ẩn SQLiteOpenHelper)
. Cơ sở dữ liệu Room sử dụng DAO để đưa ra các truy vấn cho cơ sở dữ liệu SQLite.
Kho lưu trữ: Một lớp mà bạn tạo, chủ yếu dùng để quản lý nhiều nguồn dữ liệu.
ViewModel: Đóng vai trò là trung tâm giao tiếp giữa Kho lưu trữ (dữ liệu) và giao diện người dùng. Giao diện người dùng không còn cần lo lắng về nguồn gốc của dữ liệu. Các thực thể ViewModel vẫn tồn tại sau khi Hoạt động/Mảnh được tạo lại.
LiveData: Một lớp lưu giữ dữ liệu có thể quan sát được. Luôn giữ/lưu vào bộ nhớ đệm phiên bản dữ liệu mới nhất và thông báo cho các đối tượng tiếp nhận dữ liệu khi dữ liệu thay đổi. LiveData
nhận biết được vòng đời. Các thành phần giao diện người dùng chỉ quan sát các dữ liệu liên quan và không ngừng hoặc tiếp tục việc quan sát. LiveData tự động quản lý tất cả những việc này nhờ khả năng nhận biết những thay đổi trạng thái liên quan đến vòng đời trong khi quan sát.
Tổng quan về cấu trúc RoomWordSample
Sơ đồ sau đây cho thấy tất cả các thành phần của ứng dụng. Mỗi hộp bao quanh (ngoại trừ cơ sở dữ liệu SQLite) đại diện cho một lớp mà bạn sẽ tạo.
- Mở Android Studio rồi nhấp vào Bắt đầu một dự án Android Studio mới.
- Trong cửa sổ Create New Project (Tạo dự án mới), hãy chọn Empty Activity (Hoạt động trống) rồi nhấp vào Next (Tiếp theo).
- Trên màn hình tiếp theo, hãy đặt tên cho ứng dụng là RoomWordSample rồi nhấp vào Finish (Hoàn tất).
Tiếp theo, bạn sẽ phải thêm các thư viện thành phần vào tệp Gradle.
- Trong Android Studio, hãy nhấp vào thẻ Projects (Dự án) rồi mở rộng thư mục Gradle Scripts (Tập lệnh Gradle).
Mở build.gradle
(Module: app).
- Áp dụng trình bổ trợ Kotlin annotation processor
kapt
bằng cách thêm trình bổ trợ này sau các trình bổ trợ khác được xác định ở đầu tệpbuild.gradle
(Module: app).
apply plugin: 'kotlin-kapt'
- Thêm khối
packagingOptions
vào bên trong khốiandroid
để loại trừ mô-đun hàm nguyên tử khỏi gói và ngăn chặn cảnh báo.
android {
// other configuration (buildTypes, defaultConfig, etc.)
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
}
- Thêm mã sau vào cuối khối
dependencies
.
// Room components
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion"
// Kotlin components
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"
// Material design
implementation "com.google.android.material:material:$rootProject.materialVersion"
// Testing
testImplementation 'junit:junit:4.12'
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"
- Trong tệp
build.gradle
(Project: RoomWordsSample), hãy thêm số phiên bản vào cuối tệp, như trong mã bên dưới.
ext {
roomVersion = '2.2.5'
archLifecycleVersion = '2.2.0'
coreTestingVersion = '2.1.0'
materialVersion = '1.1.0'
coroutines = '1.3.4'
}
Dữ liệu cho ứng dụng này là các từ và bạn sẽ cần một bảng đơn giản để lưu giữ các giá trị đó:
Room cho phép bạn tạo các bảng thông qua một Thực thể. Hãy bắt đầu ngay bây giờ.
- Tạo một tệp lớp Kotlin mới tên là
Word
chứa lớp dữ liệuWord
.
Lớp này sẽ mô tả Thực thể (đại diện cho bảng SQLite) cho các từ của bạn. Mỗi thuộc tính trong lớp đại diện cho một cột trong bảng. Cuối cùng, Room sẽ dùng các thuộc tính này để vừa tạo bảng vừa khởi tạo các đối tượng từ các hàng trong cơ sở dữ liệu.
Sau đây là mã:
data class Word(val word: String)
Để lớp Word
có ý nghĩa đối với cơ sở dữ liệu Room, bạn cần chú thích lớp đó. Các chú thích xác định mối quan hệ giữa từng phần của lớp này với một mục trong cơ sở dữ liệu. Room sử dụng thông tin này để tạo mã.
Nếu bạn tự nhập chú giải (thay vì dán), Android Studio sẽ tự động nhập các lớp chú giải.
- Cập nhật lớp
Word
bằng chú thích như trong mã này:
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
Hãy xem những chú thích này làm được gì:
@Entity(tableName =
"word_table"
)
Mỗi lớp@Entity
đại diện cho một bảng SQLite. Chú thích khai báo lớp để cho biết đó là một thực thể. Bạn có thể chỉ định tên của bảng nếu muốn tên này khác với tên của lớp. Thao tác này sẽ đặt tên cho bảng là "word_table".@PrimaryKey
Mọi thực thể đều cần một khoá chính. Để đơn giản, mỗi từ sẽ đóng vai trò là khoá chính riêng.@ColumnInfo(name =
"word"
)
Chỉ định tên của cột trong bảng nếu bạn muốn tên này khác với tên của biến thành phần. Thao tác này đặt tên cho cột là "word".- Mọi thuộc tính được lưu trữ trong cơ sở dữ liệu đều cần có chế độ hiển thị công khai (đây là chế độ mặc định của Kotlin).
Bạn có thể tìm thấy danh sách đầy đủ các chú thích trong Tài liệu tham khảo tóm tắt về gói Phòng.
DAO là gì?
Trong DAO (đối tượng truy cập dữ liệu), bạn chỉ định các truy vấn SQL và liên kết chúng với các lệnh gọi phương thức. Trình biên dịch kiểm tra SQL và tạo các truy vấn từ chú giải tiện lợi cho các truy vấn phổ biến, chẳng hạn như @Insert
. Room sử dụng DAO để tạo một API rõ ràng cho mã của bạn.
DAO phải là một giao diện hoặc lớp trừu tượng.
Theo mặc định, tất cả các truy vấn phải được thực thi trên một luồng riêng biệt.
Room hỗ trợ coroutine, cho phép chú thích các truy vấn bằng đối tượng sửa đổi suspend
rồi gọi từ một coroutine hoặc từ một hàm tạm ngưng khác.
Triển khai DAO
Hãy viết một DAO cung cấp các truy vấn cho:
- Sắp xếp tất cả các từ theo thứ tự bảng chữ cái
- Chèn một từ
- Đang xoá tất cả các từ
- Tạo tệp lớp Kotlin mới tên là
WordDao
. - Sao chép và dán mã sau đây vào
WordDao
, đồng thời sửa các nội dung nhập nếu cần để biên dịch mã.
@Dao
interface WordDao {
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAlphabetizedWords(): List<Word>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(word: Word)
@Query("DELETE FROM word_table")
suspend fun deleteAll()
}
Hãy cùng tìm hiểu:
WordDao
là một giao diện; DAO phải là giao diện hoặc lớp trừu tượng.- Chú giải
@Dao
xác định đây là một lớp DAO cho Room. suspend fun insert(word: Word)
: Khai báo một hàm tạm ngưng để chèn một từ.- Chú giải
@Insert
là một chú giải phương thức DAO đặc biệt mà bạn không cần cung cấp bất kỳ SQL nào! (Ngoài ra còn có chú thích@Delete
và@Update
để xoá và cập nhật các hàng, nhưng bạn không dùng chúng trong ứng dụng này.) onConflict = OnConflictStrategy.IGNORE
: Chiến lược onConflict đã chọn sẽ bỏ qua một từ mới nếu từ đó hoàn toàn giống với một từ đã có trong danh sách. Để tìm hiểu thêm về các chiến lược gây xung đột hiện có, hãy xem tài liệu này.suspend fun deleteAll()
: Khai báo một hàm tạm ngưng để xoá tất cả các từ.- Không có chú giải tiện lợi để xoá nhiều thực thể, vì vậy, bạn có thể chú giải bằng
@Query
chung. @Query
("DELETE FROM word_table")
:@Query
yêu cầu bạn cung cấp một truy vấn SQL dưới dạng tham số chuỗi cho chú giải, cho phép thực hiện các truy vấn đọc phức tạp và các thao tác khác.fun getAlphabetizedWords(): List<Word>
: Một phương thức để lấy tất cả các từ và trả về mộtList
củaWords
.@Query(
"SELECT * from word_table ORDER BY word ASC"
)
: Truy vấn trả về danh sách các từ được sắp xếp theo thứ tự tăng dần.
Khi dữ liệu thay đổi, bạn thường muốn thực hiện một số thao tác, chẳng hạn như hiển thị dữ liệu đã cập nhật trong giao diện người dùng. Điều này có nghĩa là bạn phải quan sát dữ liệu để có thể phản ứng khi dữ liệu thay đổi.
Tuỳ thuộc vào cách dữ liệu được lưu trữ, việc này có thể khó khăn. Việc quan sát các thay đổi đối với dữ liệu trên nhiều thành phần của ứng dụng có thể tạo ra các đường dẫn phụ thuộc rõ ràng và cứng nhắc giữa các thành phần. Điều này khiến việc kiểm thử và gỡ lỗi trở nên khó khăn, trong số những việc khác.
LiveData
, một lớp thư viện vòng đời để theo dõi dữ liệu, sẽ giải quyết vấn đề này. Sử dụng giá trị trả về thuộc loại LiveData
trong nội dung mô tả phương thức và Room sẽ tạo tất cả mã cần thiết để cập nhật LiveData
khi cơ sở dữ liệu được cập nhật.
Trong WordDao
, hãy thay đổi chữ ký phương thức getAlphabetizedWords()
để List<Word>
được trả về được bao bọc bằng LiveData
.
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAlphabetizedWords(): LiveData<List<Word>>
Trong phần sau của lớp học lập trình này, bạn sẽ theo dõi các thay đổi về dữ liệu thông qua một Observer
trong MainActivity
.
Cơ sở dữ liệu Room là gì?
- Room là một lớp cơ sở dữ liệu ở đầu cơ sở dữ liệu SQLite.
- Room sẽ đảm nhận những việc nhàm chán mà trước đây bạn phải xử lý bằng
SQLiteOpenHelper
. - Room sử dụng DAO để đưa ra các truy vấn cho cơ sở dữ liệu của mình.
- Theo mặc định, để tránh hiệu suất giao diện người dùng kém, Room không cho phép bạn đưa ra các truy vấn trên luồng chính. Khi các truy vấn Room trả về
LiveData
, các truy vấn sẽ tự động chạy không đồng bộ trên một luồng trong nền. - Room cung cấp các chế độ kiểm tra thời gian biên dịch của các câu lệnh SQLite.
Triển khai cơ sở dữ liệu Room
Lớp cơ sở dữ liệu Room phải là lớp trừu tượng và mở rộng RoomDatabase
. Thông thường, bạn chỉ cần một phiên bản của cơ sở dữ liệu Room cho toàn bộ ứng dụng.
Hãy tạo một thẻ ngay bây giờ.
- Tạo một tệp lớp Kotlin tên là
WordRoomDatabase
rồi thêm đoạn mã này vào tệp đó:
// Annotates class to be a Room Database with a table (entity) of the Word class
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(context: Context): WordRoomDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
).build()
INSTANCE = instance
return instance
}
}
}
}
Hãy cùng xem qua mã này:
- Lớp cơ sở dữ liệu cho Room phải là
abstract
và mở rộngRoomDatabase
- Bạn chú thích lớp để trở thành cơ sở dữ liệu của Room bằng
@Database
và sử dụng các tham số chú thích để khai báo các thực thể thuộc cơ sở dữ liệu và đặt số hiệu phiên bản. Mỗi thực thể tương ứng với một bảng sẽ được tạo trong cơ sở dữ liệu. Quy trình di chuyển cơ sở dữ liệu nằm ngoài phạm vi của lớp học lập trình này, vì vậy, chúng ta sẽ đặtexportSchema
thành false ở đây để tránh cảnh báo bản dựng. Trong một ứng dụng thực tế, bạn nên cân nhắc việc thiết lập một thư mục để Room sử dụng nhằm xuất giản đồ để bạn có thể kiểm tra giản đồ hiện tại trong hệ thống kiểm soát phiên bản. - Cơ sở dữ liệu hiển thị các DAO thông qua một phương thức "getter" trừu tượng cho từng @Dao.
- Chúng ta đã xác định một singleton,
WordRoomDatabase,
để ngăn việc mở nhiều phiên bản của cơ sở dữ liệu cùng một lúc. getDatabase
trả về singleton. Thao tác này sẽ tạo cơ sở dữ liệu vào lần đầu tiên truy cập, sử dụng trình tạo cơ sở dữ liệu của Room để tạo một đối tượngRoomDatabase
trong bối cảnh ứng dụng từ lớpWordRoomDatabase
và đặt tên là"word_database"
.
Kho lưu trữ là gì?
Một lớp kho lưu trữ sẽ trừu tượng hoá quyền truy cập vào nhiều nguồn dữ liệu. Kho lưu trữ không thuộc thư viện Thành phần cấu trúc, nhưng là một phương pháp hay nhất được đề xuất để phân tách và cấu trúc mã. Lớp Kho lưu trữ cung cấp một API rõ ràng để truy cập vào dữ liệu trong phần còn lại của ứng dụng.
Tại sao nên sử dụng Kho lưu trữ?
Kho lưu trữ quản lý các truy vấn và cho phép bạn dùng nhiều phần phụ trợ. Trong ví dụ phổ biến nhất, Kho lưu trữ triển khai logic để quyết định xem có nên tìm nạp dữ liệu từ mạng hay sử dụng kết quả được lưu vào bộ nhớ đệm trong cơ sở dữ liệu cục bộ.
Triển khai Kho lưu trữ
Tạo một tệp lớp Kotlin tên là WordRepository
rồi dán đoạn mã sau vào tệp đó:
// Declares the DAO as a private property in the constructor. Pass in the DAO
// instead of the whole database, because you only need access to the DAO
class WordRepository(private val wordDao: WordDao) {
// Room executes all queries on a separate thread.
// Observed LiveData will notify the observer when the data has changed.
val allWords: LiveData<List<Word>> = wordDao.getAlphabetizedWords()
suspend fun insert(word: Word) {
wordDao.insert(word)
}
}
Những điểm chính cần ghi nhớ:
- DAO được truyền vào hàm tạo kho lưu trữ thay vì toàn bộ cơ sở dữ liệu. Điều này là do nó chỉ cần truy cập vào DAO, vì DAO chứa tất cả các phương thức đọc/ghi cho cơ sở dữ liệu. Bạn không cần phải hiển thị toàn bộ cơ sở dữ liệu cho kho lưu trữ.
- Danh sách từ là một tài sản công khai. Thao tác này được khởi chạy bằng cách lấy danh sách
LiveData
gồm các từ trong Room; chúng ta có thể làm việc này vì cách chúng ta xác định phương thứcgetAlphabetizedWords
để trả vềLiveData
trong bước "Lớp LiveData". Room thực thi tất cả các truy vấn trên một luồng riêng biệt. Sau đó,LiveData
được quan sát sẽ thông báo cho đối tượng tiếp nhận dữ liệu trên luồng chính khi dữ liệu thay đổi. - Đối tượng sửa đổi
suspend
cho trình biên dịch biết rằng hàm này cần được gọi từ một coroutine hoặc một hàm tạm ngưng khác.
ViewModel là gì?
Vai trò của ViewModel
là cung cấp dữ liệu cho giao diện người dùng và duy trì dữ liệu đó khi có các thay đổi về cấu hình. ViewModel
đóng vai trò là trung tâm giao tiếp giữa Kho lưu trữ và giao diện người dùng. Bạn cũng có thể dùng ViewModel
để chia sẻ dữ liệu giữa các mảnh. ViewModel là một phần của thư viện vòng đời.
Để xem hướng dẫn giới thiệu về chủ đề này, hãy xem ViewModel Overview
hoặc bài đăng trên blog ViewModel: Một ví dụ đơn giản.
Tại sao nên dùng ViewModel?
ViewModel
lưu giữ dữ liệu giao diện người dùng của ứng dụng theo cách nhận biết vòng đời và vẫn tồn tại sau khi thay đổi cấu hình. Việc tách dữ liệu giao diện người dùng của ứng dụng khỏi các lớp Activity
và Fragment
giúp bạn tuân thủ tốt hơn nguyên tắc trách nhiệm duy nhất: Các hoạt động và mảnh chịu trách nhiệm vẽ dữ liệu ra màn hình, trong khi ViewModel
có thể đảm nhận việc giữ và xử lý tất cả dữ liệu cần thiết cho giao diện người dùng.
Trong ViewModel
, hãy dùng LiveData
cho dữ liệu có thể thay đổi mà giao diện người dùng sẽ sử dụng hoặc hiển thị. Việc sử dụng LiveData
mang lại một số lợi ích:
- Bạn có thể đặt một trình quan sát trên dữ liệu (thay vì thăm dò các thay đổi) và chỉ cập nhật
giao diện người dùng khi dữ liệu thực sự thay đổi. - Kho lưu trữ và giao diện người dùng hoàn toàn tách biệt với nhau bằng
ViewModel
. - Không có lệnh gọi cơ sở dữ liệu nào từ
ViewModel
(tất cả đều được xử lý trong Kho lưu trữ), giúp mã dễ kiểm thử hơn.
viewModelScope
Trong Kotlin, tất cả coroutine đều chạy bên trong một CoroutineScope
. Phạm vi kiểm soát toàn bộ thời gian của coroutine thông qua tác vụ. Khi huỷ công việc trong một phạm vi, thao tác đó sẽ huỷ tất cả các coroutine bắt đầu trong phạm vi đó.
Thư viện AndroidX lifecycle-viewmodel-ktx
thêm viewModelScope
làm hàm tiện ích của lớp ViewModel
, cho phép bạn làm việc với các phạm vi.
Để tìm hiểu thêm về cách sử dụng coroutine trong ViewModel, hãy xem Bước 5 của lớp học lập trình Sử dụng coroutine Kotlin trong ứng dụng Android hoặc bài đăng trên blog về Coroutine dễ dàng trong Android: viewModelScope.
Triển khai ViewModel
Tạo một tệp lớp Kotlin cho WordViewModel
rồi thêm mã sau vào tệp đó:
class WordViewModel(application: Application) : AndroidViewModel(application) {
private val repository: WordRepository
// Using LiveData and caching what getAlphabetizedWords returns has several benefits:
// - We can put an observer on the data (instead of polling for changes) and only update the
// the UI when the data actually changes.
// - Repository is completely separated from the UI through the ViewModel.
val allWords: LiveData<List<Word>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
}
/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
fun insert(word: Word) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(word)
}
}
Sau đây là những gì chúng tôi đã làm:
- Tạo một lớp có tên là
WordViewModel
, lớp này nhậnApplication
làm tham số và mở rộngAndroidViewModel
. - Thêm một biến thành viên riêng tư để lưu giữ thông tin tham chiếu đến kho lưu trữ.
- Đã thêm một biến thành viên
LiveData
công khai để lưu vào bộ nhớ đệm danh sách từ. - Tạo một khối
init
lấy một tham chiếu đếnWordDao
từWordRoomDatabase
. - Trong khối
init
, hãy tạoWordRepository
dựa trênWordRoomDatabase
. - Trong khối
init
, hãy khởi tạoallWords
LiveData bằng kho lưu trữ. - Tạo một phương thức bao bọc
insert()
gọi phương thứcinsert()
của Kho lưu trữ. Bằng cách này, việc triển khaiinsert()
sẽ được đóng gói từ giao diện người dùng. Chúng ta không muốn thao tác chèn chặn luồng chính, vì vậy, chúng ta sẽ chạy một coroutine mới và gọi thao tác chèn của kho lưu trữ (đây là một hàm tạm ngưng). Như đã đề cập, ViewModel có một phạm vi coroutine dựa trên vòng đời của chúng, được gọi làviewModelScope
và chúng ta sẽ sử dụng phạm vi này ở đây.
Tiếp theo, bạn cần thêm bố cục XML cho danh sách và các mục.
Lớp học lập trình này giả định rằng bạn đã quen với việc tạo bố cục trong XML, vì vậy, chúng tôi chỉ cung cấp cho bạn mã.
Tạo giao diện Material cho ứng dụng bằng cách đặt thành phần mẹ AppTheme
thành Theme.MaterialComponents.Light.DarkActionBar
. Thêm kiểu cho các mục trong danh sách trong values/styles.xml
:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<!-- The default font for RecyclerView items is too small.
The margin is a simple delimiter between the words. -->
<style name="word_title">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_marginBottom">8dp</item>
<item name="android:paddingLeft">8dp</item>
<item name="android:background">@android:color/holo_orange_light</item>
<item name="android:textAppearance">@android:style/TextAppearance.Large</item>
</style>
</resources>
Thêm bố cục layout/recyclerview_item.xml
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView"
style="@style/word_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_orange_light" />
</LinearLayout>
Trong layout/activity_main.xml
, hãy thay thế TextView
bằng RecyclerView
và thêm một nút hành động nổi (FAB). Bây giờ, bố cục của bạn sẽ có dạng như sau:
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="0dp"
android:layout_height="0dp"
tools:listitem="@layout/recyclerview_item"
android:padding="@dimen/big_padding"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_word"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Giao diện của FAB phải tương ứng với thao tác có sẵn, vì vậy, chúng ta sẽ muốn thay thế biểu tượng bằng biểu tượng "+".
Trước tiên, chúng ta cần thêm một Vector Asset mới:
- Chọn File > New > Vector Asset (Tệp > Mới > Vector Asset).
- Nhấp vào biểu tượng robot Android trong trường Hình mẫu: .
- Tìm "add" (thêm) rồi chọn thành phần "+". Nhấp vào OK
- Sau đó, hãy nhấp vào Tiếp theo.
- Xác nhận đường dẫn biểu tượng là
main > drawable
rồi nhấp vào Finish (Hoàn tất) để thêm thành phần. - Vẫn trong
layout/activity_main.xml
, hãy cập nhật FAB để thêm drawable mới:
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_word"
android:src="@drawable/ic_add_black_24dp"/>
Bạn sẽ hiển thị dữ liệu trong một RecyclerView
, đẹp hơn một chút so với việc chỉ đưa dữ liệu vào một TextView
. Lớp học lập trình này giả định rằng bạn biết cách hoạt động của RecyclerView
, RecyclerView.LayoutManager
, RecyclerView.ViewHolder
và RecyclerView.Adapter
.
Xin lưu ý rằng biến words
trong bộ nhớ đệm của trình chuyển đổi sẽ lưu dữ liệu vào bộ nhớ đệm. Trong nhiệm vụ tiếp theo, bạn sẽ thêm mã tự động cập nhật dữ liệu.
Tạo một tệp lớp Kotlin cho WordListAdapter
mở rộng RecyclerView.Adapter
. Sau đây là mã:
class WordListAdapter internal constructor(
context: Context
) : RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var words = emptyList<Word>() // Cached copy of words
inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val wordItemView: TextView = itemView.findViewById(R.id.textView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
val itemView = inflater.inflate(R.layout.recyclerview_item, parent, false)
return WordViewHolder(itemView)
}
override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
val current = words[position]
holder.wordItemView.text = current.word
}
internal fun setWords(words: List<Word>) {
this.words = words
notifyDataSetChanged()
}
override fun getItemCount() = words.size
}
Thêm RecyclerView
vào phương thức onCreate()
của MainActivity
.
Trong phương thức onCreate()
sau setContentView
:
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
Chạy ứng dụng để đảm bảo mọi thứ đều hoạt động. Không có mục nào vì bạn chưa kết nối dữ liệu.
Không có dữ liệu trong cơ sở dữ liệu. Bạn sẽ thêm dữ liệu theo hai cách: Thêm một số dữ liệu khi cơ sở dữ liệu được mở và thêm một Activity
để thêm các từ.
Để xoá tất cả nội dung và điền lại cơ sở dữ liệu mỗi khi ứng dụng khởi động, bạn hãy tạo một RoomDatabase.Callback
và ghi đè onOpen()
. Vì bạn không thể thực hiện các thao tác trên cơ sở dữ liệu Room trên luồng giao diện người dùng, nên onOpen()
sẽ chạy một coroutine trên IO Dispatcher.
Để chạy một coroutine, chúng ta cần có CoroutineScope
. Cập nhật phương thức getDatabase
của lớp WordRoomDatabase
để lấy phạm vi coroutine làm tham số:
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
...
}
Cập nhật trình khởi tạo truy xuất cơ sở dữ liệu trong khối init
của WordViewModel
để truyền cả phạm vi:
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
Trong WordRoomDatabase
, chúng ta tạo một phương thức triển khai tuỳ chỉnh của RoomDatabase.Callback()
, phương thức này cũng nhận được CoroutineScope
làm tham số hàm khởi tạo. Sau đó, chúng ta ghi đè phương thức onOpen
để điền sẵn dữ liệu vào cơ sở dữ liệu.
Dưới đây là mã để tạo lệnh gọi lại trong lớp WordRoomDatabase
:
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
populateDatabase(database.wordDao())
}
}
}
suspend fun populateDatabase(wordDao: WordDao) {
// Delete all content here.
wordDao.deleteAll()
// Add sample words.
var word = Word("Hello")
wordDao.insert(word)
word = Word("World!")
wordDao.insert(word)
// TODO: Add your own words!
}
}
Cuối cùng, hãy thêm lệnh gọi lại vào trình tự tạo cơ sở dữ liệu ngay trước khi gọi .build()
trên Room.databaseBuilder()
:
.addCallback(WordDatabaseCallback(scope))
Sau đây là giao diện của mã hoàn chỉnh:
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
var wordDao = database.wordDao()
// Delete all content here.
wordDao.deleteAll()
// Add sample words.
var word = Word("Hello")
wordDao.insert(word)
word = Word("World!")
wordDao.insert(word)
// TODO: Add your own words!
word = Word("TODO!")
wordDao.insert(word)
}
}
}
}
companion object {
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
)
.addCallback(WordDatabaseCallback(scope))
.build()
INSTANCE = instance
// return instance
instance
}
}
}
}
Thêm các tài nguyên chuỗi sau vào values/strings.xml
:
<string name="hint_word">Word...</string>
<string name="button_save">Save</string>
<string name="empty_not_saved">Word not saved because it is empty.</string>
Thêm tài nguyên màu này vào value/colors.xml
:
<color name="buttonLabel">#FFFFFF</color>
Tạo một tệp tài nguyên phương diện mới:
- Nhấp vào mô-đun ứng dụng trong cửa sổ Project (Dự án).
- Chọn File > New > Android Resource File (Tệp > Mới > Tệp tài nguyên Android)
- Trong số các Bộ hạn định sẵn có, hãy chọn Phương diện
- Đặt tên tệp: dimens
Thêm các tài nguyên phương diện này vào values/dimens.xml
:
<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>
Tạo một Activity
Android trống mới bằng mẫu Empty Activity (Hoạt động trống):
- Chọn File > New > Activity > Empty Activity (Tệp > Mới > Hoạt động > Hoạt động trống)
- Nhập
NewWordActivity
cho tên Hoạt động. - Xác minh rằng hoạt động mới đã được thêm vào Tệp kê khai Android.
<activity android:name=".NewWordActivity"></activity>
Cập nhật tệp activity_new_word.xml
trong thư mục bố cục bằng đoạn mã sau:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/edit_word"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/min_height"
android:fontFamily="sans-serif-light"
android:hint="@string/hint_word"
android:inputType="textAutoComplete"
android:layout_margin="@dimen/big_padding"
android:textSize="18sp" />
<Button
android:id="@+id/button_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:text="@string/button_save"
android:layout_margin="@dimen/big_padding"
android:textColor="@color/buttonLabel" />
</LinearLayout>
Cập nhật mã cho hoạt động:
class NewWordActivity : AppCompatActivity() {
private lateinit var editWordView: EditText
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_new_word)
editWordView = findViewById(R.id.edit_word)
val button = findViewById<Button>(R.id.button_save)
button.setOnClickListener {
val replyIntent = Intent()
if (TextUtils.isEmpty(editWordView.text)) {
setResult(Activity.RESULT_CANCELED, replyIntent)
} else {
val word = editWordView.text.toString()
replyIntent.putExtra(EXTRA_REPLY, word)
setResult(Activity.RESULT_OK, replyIntent)
}
finish()
}
}
companion object {
const val EXTRA_REPLY = "com.example.android.wordlistsql.REPLY"
}
}
Bước cuối cùng là kết nối giao diện người dùng với cơ sở dữ liệu bằng cách lưu những từ mới mà người dùng nhập và hiển thị nội dung hiện tại của cơ sở dữ liệu từ trong RecyclerView
.
Để hiển thị nội dung hiện tại của cơ sở dữ liệu, hãy thêm một đối tượng tiếp nhận dữ liệu để theo dõi LiveData
trong ViewModel
.
Bất cứ khi nào dữ liệu thay đổi, lệnh gọi lại onChanged()
sẽ được gọi, lệnh gọi này sẽ gọi phương thức setWords()
của bộ chuyển đổi để cập nhật dữ liệu được lưu vào bộ nhớ đệm của bộ chuyển đổi và làm mới danh sách được hiển thị.
Trong MainActivity
, hãy tạo một biến thành viên cho ViewModel
:
private lateinit var wordViewModel: WordViewModel
Sử dụng ViewModelProvider
để liên kết ViewModel
với Activity
.
Khi Activity
khởi động lần đầu tiên, ViewModelProviders
sẽ tạo ViewModel
. Khi hoạt động bị huỷ (ví dụ: thông qua thay đổi cấu hình), ViewModel
sẽ vẫn tồn tại. Khi hoạt động được tạo lại, ViewModelProviders
sẽ trả về ViewModel
hiện có. Để biết thêm thông tin, hãy xem ViewModel
.
Trong onCreate()
bên dưới khối mã RecyclerView
, hãy lấy một ViewModel
từ ViewModelProvider
:
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
Ngoài ra, trong onCreate()
, hãy thêm một đối tượng tiếp nhận dữ liệu cho thuộc tính allWords LiveData
từ WordViewModel
.
Phương thức onChanged()
(phương thức mặc định cho Lambda của chúng ta) sẽ kích hoạt khi dữ liệu được quan sát thay đổi và hoạt động ở nền trước:
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})
Chúng ta muốn mở NewWordActivity
khi nhấn vào FAB và khi quay lại MainActivity
, hãy chèn từ mới vào cơ sở dữ liệu hoặc hiển thị Toast
. Để đạt được điều này, hãy bắt đầu bằng cách xác định mã yêu cầu:
private val newWordActivityRequestCode = 1
Trong MainActivity
, hãy thêm mã onActivityResult()
cho NewWordActivity
.
Nếu hoạt động trả về với RESULT_OK
, hãy chèn từ được trả về vào cơ sở dữ liệu bằng cách gọi phương thức insert()
của WordViewModel
:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
val word = Word(it)
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}
Trong MainActivity,
start NewWordActivity
khi người dùng nhấn vào FAB. Trong MainActivity
onCreate
, hãy tìm FAB rồi thêm một onClickListener
bằng mã sau:
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
Mã hoàn tất của bạn sẽ có dạng như sau:
class MainActivity : AppCompatActivity() {
private const val newWordActivityRequestCode = 1
private lateinit var wordViewModel: WordViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
val word = Word(it)
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}
}
Giờ thì hãy chạy ứng dụng của bạn! Khi bạn thêm một từ vào cơ sở dữ liệu trong NewWordActivity
, giao diện người dùng sẽ tự động cập nhật.
Giờ khi bạn đã có một ứng dụng hoạt động, hãy cùng tóm tắt những gì bạn đã tạo. Dưới đây là cấu trúc ứng dụng:
Các thành phần của ứng dụng là:
MainActivity
: hiển thị các từ trong danh sách bằng cách dùngRecyclerView
vàWordListAdapter
. TrongMainActivity
, có mộtObserver
quan sát LiveData của các từ trong cơ sở dữ liệu và nhận được thông báo khi các từ đó thay đổi.NewWordActivity:
thêm một từ mới vào danh sách.WordViewModel
: cung cấp các phương thức để truy cập vào lớp dữ liệu và trả về LiveData để MainActivity có thể thiết lập mối quan hệ đối tượng tiếp nhận dữ liệu.*LiveData<List<Word>>
: Cho phép tự động cập nhật trong các thành phần giao diện người dùng. TrongMainActivity
, có mộtObserver
quan sát LiveData của các từ trong cơ sở dữ liệu và nhận được thông báo khi các từ đó thay đổi.Repository:
quản lý một hoặc nhiều nguồn dữ liệu.Repository
cung cấp các phương thức để ViewModel tương tác với trình cung cấp dữ liệu cơ bản. Trong ứng dụng này, phần phụ trợ đó là một cơ sở dữ liệu Room.Room
: là một trình bao bọc và triển khai cơ sở dữ liệu SQLite. Room giúp bạn thực hiện nhiều việc mà trước đây bạn phải tự làm.- DAO: ánh xạ các lệnh gọi phương thức đến các truy vấn cơ sở dữ liệu, sao cho khi Kho lưu trữ gọi một phương thức như
getAlphabetizedWords()
, Room có thể thực thiSELECT * from word_table ORDER BY word ASC
. Word
: là lớp thực thể chứa một từ duy nhất.
* Views
và Activities
(cũng như Fragments
) chỉ tương tác với dữ liệu thông qua ViewModel
. Do đó, nguồn gốc của dữ liệu không quan trọng.
Luồng dữ liệu để tự động cập nhật giao diện người dùng (Giao diện người dùng phản ứng)
Có thể tự động cập nhật vì chúng ta đang sử dụng LiveData. Trong MainActivity
, có một Observer
quan sát LiveData của các từ trong cơ sở dữ liệu và nhận được thông báo khi các từ đó thay đổi. Khi có thay đổi, phương thức onChange()
của trình quan sát sẽ được thực thi và cập nhật mWords
trong WordListAdapter
.
Bạn có thể thấy dữ liệu này vì dữ liệu đó là LiveData
. Và những gì được quan sát là LiveData<List<Word>>
do thuộc tính WordViewModel
allWords
trả về.
WordViewModel
ẩn mọi thông tin về phần phụ trợ khỏi lớp giao diện người dùng. Nó cung cấp các phương thức để truy cập vào lớp dữ liệu và trả về LiveData
để MainActivity
có thể thiết lập mối quan hệ của đối tượng tiếp nhận dữ liệu. Views
và Activities
(cũng như Fragments
) chỉ tương tác với dữ liệu thông qua ViewModel
. Do đó, nguồn gốc của dữ liệu không quan trọng.
Trong trường hợp này, dữ liệu đến từ một Repository
. ViewModel
không cần biết Kho lưu trữ đó tương tác với những gì. Nó chỉ cần biết cách tương tác với Repository
thông qua các phương thức do Repository
hiển thị.
Kho lưu trữ quản lý một hoặc nhiều nguồn dữ liệu. Trong ứng dụng WordListSample
, phần phụ trợ đó là một cơ sở dữ liệu Room. Room là một trình bao bọc và triển khai cơ sở dữ liệu SQLite. Room giúp bạn thực hiện nhiều việc mà trước đây bạn phải tự làm. Ví dụ: Room thực hiện mọi việc mà bạn từng làm với lớp SQLiteOpenHelper
.
DAO ánh xạ các lệnh gọi phương thức đến các truy vấn cơ sở dữ liệu, sao cho khi Kho lưu trữ gọi một phương thức như getAllWords()
, Room có thể thực thi SELECT * from word_table ORDER BY word ASC
.
Vì kết quả trả về từ truy vấn là LiveData
được quan sát, nên mỗi khi dữ liệu trong Room thay đổi, phương thức onChanged()
của giao diện Observer
sẽ được thực thi và giao diện người dùng sẽ được cập nhật.
[Không bắt buộc] Tải mã giải pháp xuống
Bạn có thể xem mã giải pháp cho lớp học lập trình này (nếu chưa có). Bạn có thể xem kho lưu trữ GitHub hoặc tải mã xuống tại đây:
Giải nén tệp zip đã tải xuống. Thao tác này sẽ giải nén một thư mục gốc, android-room-with-a-view-kotlin
, chứa toàn bộ ứng dụng.