Phòng Android có chế độ xem – Kotlin

Mục đích của Thành phần cấu 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 dành cho các tác vụ thông thường 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 tra và bảo 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ể tìm thấy phiên bản bằng ngôn ngữ lập trình Java tại đây.

Nếu bạn gặp phải bất kỳ vấn đề nào (lỗi mã, lỗi ngữ pháp, từ ngữ không rõ ràng, v.v.) khi bạn làm việc qua lớp học lập trình này, vui lòng báo cáo vấn đề qua liên kết Báo cáo lỗi ở góc dưới bên trái của lớp học lập trình.

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

Bạn cần phải làm quen với Kotlin, các khái niệm thiết kế hướng đối tượng và các kiến thức cơ bản về việc phát triển Android, cụ thể là:

Bạn cũng nên làm quen với các mẫu cấu trúc phần mềm phân 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 mã này triển khai cấu 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 cấu trúc của Android. Các khái niệm và mã lạc đề được cung cấp để bạn chỉ cần sao chép và dán.

Nếu bạn không quen với Kotlin, phiên bản lớp học lập trình này 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 ứng dụng bằng Phòng thành phần cấu trúc, ViewModel, và LiveData và xây dựng một ứng dụng như sau:

  • Triển khai cấu trúc đề xuất của chúng tôi bằng Thành phần cấu trúc Android.
  • Hoạt động với cơ sở dữ liệu để nhận và lưu dữ liệu, đồng thời điền trước cơ sở dữ liệu bằng một số từ.
  • Hiển thị tất cả các từ trong RecyclerView trong MainActivity.
  • Mở 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 đơn giản nhưng đủ phức tạp để bạn có thể dùng làm mẫu để xây dựng. Đây là bản xem trước:

Bạn cần có

  • Android Studio 3.0 trở lên và 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 của mình.
  • Một thiết bị Android hoặc trình mô phỏng.

Lớp học lập trình này cung cấp mọi mã bạn cần để xây dựng ứng dụng hoàn chỉnh.

Bạn sẽ thực hiện 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 đề xuất. Điều quan trọng nhất là tạo ra một mô hình tinh thần về những gì đang xảy ra, hiểu cách các mảnh khớ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 rồi dán mã đó, mà hãy thử bắt đầu việc xây dựng kiến thức bên trong đó.

Để giới thiệu thuật ngữ, sau đây là giới thiệu ngắn về Thành phần cấu trúc và cách các thành phần này hoạt động cùng 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 rõ hơn khi bạn sử dụng thành phần đó.

Sơ đồ này cho thấy một dạng kiến trúc cơ bản:

Thực thể: Lớp có chú thích mô tả bảng cơ sở dữ liệu khi làm việc với Phòng.

Cơ sở dữ liệu SQLite: Trên bộ nhớ của thiết bị. Thư viện lưu trữ Phòng 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. Ánh xạ các truy vấn SQL đến các hàm. Khi bạn dùng DAO, bạn sẽ gọi các phương thức và Phòng sẽ thực hiện các việc còn lại.

Cơ sở dữ liệu phòng: Đơn giản hóa cơ sở dữ liệu và hoạt động như một điểm truy cập vào cơ sở dữ liệu SQLite cơ bản (ẩn SQLiteOpenHelper)). Cơ sở dữ liệu Phòng sử dụng DAO để đưa ra các truy vấn tới 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 phải lo lắng về nguồn gốc của dữ liệu nữa. Các thực thể của ViewModel vẫn tồn tại hoạt động giải trí cho Hoạt động/Mảnh.

LiveData: Một lớp lưu giữ dữ liệu có thể quan sát. Luôn lưu giữ/lưu bộ nhớ đệm phiên bản mới nhất của dữ liệu và thông báo cho người quan sát phiên bản này khi dữ liệu thay đổi. LiveData nhận biết vòng đời. Các thành phần giao diện người dùng chỉ quan sát dữ liệu có liên quan và không dừng hoặc tiếp tục quan sát. LiveData tự động quản lý tất cả những việc này vì nó nhận biết được các thay đổi liên quan đến trạng thái vòng đời trong khi quan sát.

Tổng quan về kiến trúc RoomRoomSample

Sơ đồ dưới đây cho thấy tất cả các phần của ứng dụng. Mỗi ô 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.

  1. Mở Android Studio rồi nhấp vào Bắt đầu dự án Android Studio mới.
  2. Trong cửa sổ Tạo dự án mới, hãy chọn Hoạt động trống rồi nhấp vào Tiếp theo.
  3. Trên màn hình tiếp theo, hãy đặt tên cho ứng dụng RoomWordSample rồi nhấp vào 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 của mình.

  1. Trong Android Studio, nhấp vào thẻ Dự án và mở rộng thư mục Tập lệnh Gradle.

Mở build.gradle (Mô-đun: ứng dụng).

  1. Áp dụng trình bổ trợ Kotlin bộ xử lý chú thích kapt bằng cách thêm trình bổ trợ sau các trình bổ trợ khác được xác định ở đầu tệp build.gradle (Mô-đun: ứng dụng).
apply plugin: 'kotlin-kapt'
  1. Thêm khối packagingOptions bên trong khối android để loại trừ mô-đun hàm nguyên tử khỏi gói và ngăn các cảnh báo.
android {
    // other configuration (buildTypes, defaultConfig, etc.)

    packagingOptions {
        exclude 'META-INF/atomicfu.kotlin_module'
    }
}
  1. 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"
  1. Trong tệp build.gradle (Project: RoomWordsSample), hãy thêm số phiên bản vào cuối tệp, như được mã 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 để giữ những giá trị đó:

Phòng cho phép bạn tạo bảng thông qua Thực thể. Bây giờ, hãy làm việc này.

  1. Tạo một tệp mới trong lớp Kotlin có tên là Word, trong đó có chứa lớp dữ liệu Word.
    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 tài sản trong lớp đại diện cho một cột trong bảng. Cuối cùng, Phòng sẽ sử dụng các thuộc tính này để vừa tạo bảng vừa tạo thực thể cho đố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àm cho lớp Word có ý nghĩa với cơ sở dữ liệu Phòng, bạn cần chú thích lớp đó. Chú thích xác định cách mỗi phần của lớp này liên quan đến một mục trong cơ sở dữ liệu. Phòng sử dụng thông tin này để tạo mã.

Nếu bạn tự nhập chú thích (thay vì dán), Android Studio sẽ tự động nhập các lớp chú thích đó.

  1. Cập nhật lớp Word của bạn với các chú thích như hiển thị trong mã này:
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)

Hãy xem chức năng của những chú thích này:

  • @Entity(tableName = "word_table")
    Mỗi lớp @Entity đại diện cho một bảng SQLite. Chú thích khai báo của lớp để cho biết rằng đó là thực thể. Bạn có thể chỉ định tên của bảng nếu muốn bảng này khác với tên của lớp. Đây là tên của bảng "word_table".
  • @PrimaryKey
    Mỗi thực thể đều cần có một khóa chính. Để đơn giản hóa mọi thứ, mỗi từ đóng vai trò là khóa chính.
  • @ColumnInfo(name = "word")
    Chỉ định tên của cột trong bảng nếu bạn muốn cột này khác với tên của biến thành viên. Đây là tên cột "word".
  • Mọi thuộc tính được lưu trữ trong cơ sở dữ liệu đều phải 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 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ú thích thuận tiện cho các truy vấn phổ biến, chẳng hạn như @Insert. Phòng sử dụng DAO để tạo một API không có lỗi cho mã của bạn.

Đối tượng truy cập dữ liệu (DAO) phải là một giao diện hoặc lớp trừu tượng.

Theo mặc định, tất cả truy vấn phải được thực thi trên một chuỗi riêng.

Phòng hỗ trợ coroutine, cho phép các truy vấn của bạn được chú thích bằng từ khóa xác định suspend, sau đó được gọi 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 xóa tất cả các từ
  1. Tạo tệp lớp Kotlin mới tên là WordDao.
  2. Sao chép và dán mã sau vào WordDao và sửa lỗi khi cần nhập để biên dịch.
@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 xem qua:

  • WordDao là một giao diện; DAO phải là giao diện hoặc lớp trừu tượng.
  • Chú thích @Dao sẽ xác định lớp đó là lớp DAO cho Phòng.
  • suspend fun insert(word: Word) : Khai báo hàm tạm ngưng để chèn một từ.
  • Chú thích @Insert là chú thích phương thức DAO đặc biệt, trong đó bạn không phải cung cấp bất kỳ SQL nào! (Ngoài ra còn có chú thích @Delete@Update để xóa và cập nhật hàng, nhưng bạn hiện không sử dụng chúng trong ứng dụng này.)
  • onConflict = OnConflictStrategy.IGNORE: Chiến lược on Huỷ được chọn sẽ bỏ qua một từ mới nếu từ đó giống hệt với từ đã có trong danh sách. Để biết thêm về các chiến lược xung đột có thể sử dụng, hãy xem tài liệu này.
  • suspend fun deleteAll(): Khai báo một hàm tạm ngưng để xóa tất cả các từ.
  • Không có chú thích tiện lợi cho việc xóa nhiều thực thể, vì vậy, chú thích này sẽ được chú thích 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 của chú thích, cho phép 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 để nhận tất cả các từ và trả về List trong tổng số Words.
  • @Query("SELECT * from word_table ORDER BY word ASC"): Truy vấn trả về danh sách 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 hành động nào đó, chẳng hạn như hiển thị dữ liệu cập nhật trên 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 để khi dữ liệu đó thay đổi, bạn có thể bày tỏ cảm xúc.

Tuỳ thuộc vào cách lưu trữ dữ liệu, điều này có thể phức tạp. 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ần phụ thuộc rõ ràng, cứng nhắc giữa các thành phần. Điều này khiến việc thử nghiệm và gỡ lỗi trở nên khó khăn, cùng nhiều tính năng khác.

LiveData, một lớp thư viện vòng đời để quan sát dữ liệu, giải quyết vấn đề này. Sử dụng giá trị trả về thuộc loại LiveData trong phần mô tả phương thức và Phòng sẽ tạo tất cả cá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ý của 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>>

Sau này trong lớp học lập trình này, bạn theo dõi nội dung thay đổi dữ liệu thông qua một Observer trong MainActivity.

Cơ sở dữ liệu Phòng là gì?

  • Phòng là một lớp cơ sở dữ liệu ở đầu một cơ sở dữ liệu SQLite.
  • Phòng sẽ xử lý các tác vụ mơ hồ mà bạn đã từng xử lý bằng SQLiteOpenHelper.
  • Phòng sử dụng DAO để đưa ra các truy vấn tới cơ sở dữ liệu của phòng.
  • Theo mặc định, để tránh hiệu suất giao diện người dùng kém, Phòng không cho phép bạn đưa ra truy vấn trên chuỗi chính. Khi truy vấn Phòng 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.
  • Phòng cung cấp tính năng kiểm tra thời gian biên dịch của câu lệnh SQLite.

Triển khai cơ sở dữ liệu Phòng

Lớp cơ sở dữ liệu Phòng của bạn phải là 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 Phòng cho toàn bộ ứng dụng.

Hãy tạo ngay bây giờ.

  1. Tạo một tệp lớp Kotlin có tên là WordRoomDatabase và thêm 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 xem qua mã:

  • Lớp cơ sở dữ liệu cho Phòng phải là abstract và mở rộng RoomDatabase
  • Bạn chú thích lớp này là cơ sở dữ liệu Phòng với @Database và sử dụng các thông số chú thích để khai báo các thực thể thuộc cơ sở dữ liệu và đặt số 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. Quá 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 tôi đặt exportSchema thành false ở đây để tránh cảnh báo về bản dựng. Trong một ứng dụng thực tế, bạn nên cân nhắc đặt thư mục cho Phòng để xuất giản đồ nhằm kiểm tra giản đồ hiện tại vào hệ thống kiểm soát phiên bản của mình.
  • Cơ sở dữ liệu cung cấp DAO thông qua một phương thức trừu tượng "getter" cho mỗi @Dao.
  • Chúng tôi đã xác định 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. Lớp 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 Phòng để tạo đối tượng RoomDatabase trong bối cảnh ứng dụng từ lớp WordRoomDatabase và đặt tên cho cơ sở dữ liệu này là "word_database".

Kho lưu trữ là gì?

Một lớp kho lưu trữ trừu tượng 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 mà chúng tôi đề xuất để phân tách mã và cấu trúc. Lớp Kho lưu trữ cung cấp một API không có lỗi để truy cập dữ liệu vào phần còn lại của ứng dụng.

Lý do 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 sử 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 tìm nạp dữ liệu từ mạng hay sử dụng kết quả lưu trong 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 có tên là WordRepository và dá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 cần ghi nhớ:

  • Đối tượng truy cập dữ liệu (DAO) được chuyển vào hàm dựng kho lưu trữ thay vì toàn bộ cơ sở dữ liệu. Nguyên nhân là vì DAO chỉ cần quyề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. Không cần phải hiển thị toàn bộ cơ sở dữ liệu với kho lưu trữ.
  • Danh sách từ là tài sản công khai. Hàm này được khởi tạo bằng cách lấy danh sách LiveData từ ngữ từ Phòng; chúng ta có thể làm việc này vì cách chúng ta xác định phương thức getAlphabetizedWords để trả về LiveData trong "Lớp LiveData" bước. Phòng thực thi tất cả các truy vấn trên một chuỗi riêng. 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.
  • Công cụ sửa đổi suspend cho trình biên dịch biết rằng cần phải gọi phương thức này từ coroutine hoặc một hàm tạm ngưng khác.

ViewModel là gì?

Vai trò của ViewModel33 là cung cấp dữ liệu cho giao diện người dùng và tồn tại trong 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: A Example Example.

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 chú trọng đến vòng đời khi tồn tại các thay đổi về 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 lớp ActivityFragment cho phép bạn tuân thủ tốt hơn một nguyên tắc trách nhiệm duy nhất: Hoạt động và mảnh của bạn 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 lưu 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 sau:

  • Bạn có thể đặt một đối tượng tiếp nhận dữ liệu vào dữ liệu (thay vì thăm dò ý kiến để 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 được phân tách hoàn toàn bằng ViewModel.
  • Không có lệnh gọi cơ sở dữ liệu từ ViewModel (điều này tất cả được xử lý trong Kho lưu trữ), làm cho mã có thể kiểm tra được nhiều hơn.

viewModelScope

Trong Kotlin, tất cả coroutine đều chạy trong CoroutineScope. Phạm vi kiểm soát thời gian hoạt động của coroutine thông qua công việc. 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 lifecycle-viewmodel-ktx trên AndroidX thêm viewModelScope làm một hàm mở rộng 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 làm việc với coroutine trong ViewModel, hãy xem Bước 5 của Lớp học sử dụng coroutine của Kotlin trong Ứng dụng Android của bạn hoặc Bài đăng trên blog dễ dàng trong Android: viewModelScope.

Triển khai ViewModel

Tạo một lớp lớp Kotlin cho WordViewModel và thêm mã này vào:

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)
    }
}

Tại đây, chúng tôi:

  • Đã tạo một lớp có tên là WordViewModel, trong đó lấy Application làm thông số và mở rộng AndroidViewModel.
  • 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 LiveData thành viên công khai để lưu vào bộ nhớ đệm danh sách từ.
  • Đã tạo một khối init tham chiếu đến WordDao từ WordRoomDatabase.
  • Trong khối init, đã tạo WordRepository dựa trên WordRoomDatabase.
  • Trong khối init, hãy khởi chạy LiveData allWords bằng kho lưu trữ.
  • Đã tạo một phương thức insert() của trình bao bọc gọi phương thức insert() của Kho lưu trữ. Bằng cách này, việc triển khai insert() sẽ được đóng gói từ giao diện người dùng. Chúng tôi không muốn chèn để chặn luồng chính, vì vậy, chúng tôi sẽ khởi chạy một coroutine mới và gọi tệp chèn kho lưu trữ, đây là một hàm tạm ngưng. Như đã đề cập, ViewModel có 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ẽ 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 mã 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 tài liệu giao diện ứng dụng của bạn bằng cách đặt AppTheme gốc thành Theme.MaterialComponents.Light.DarkActionBar. Thêm kiểu cho mục trong danh sách ở 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 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 hành động có sẵn, vì vậy, chúng tôi sẽ muốn thay thế biểu tượng bằng một biểu tượng ' +'.

Trước tiên, chúng ta cần thêm một Tài sản EIDR mới:

  1. Chọn Tệp > Mới > Tài sản Vector.
  2. Nhấp vào biểu tượng robot Android trong Hình mẫu: trường.
  3. Tìm kiếm "add" và chọn tài sản #39;+#39; Nhấp vào OK.
  4. Sau đó, hãy nhấp vào Tiếp theo.
  5. Xác nhận đường dẫn biểu tượng là main > drawable và nhấp vào Hoàn tất để thêm tài sản.
  6. Vẫn trong layout/activity_main.xml, hãy cập nhật FAB để thêm hình vẽ 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 RecyclerView. Thao tác này tốt hơn một chút so với việc gửi dữ liệu trong 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.ViewHolderRecyclerView.Adapter.

Xin lưu ý rằng biến words trong bộ chuyển đổi lưu vào bộ nhớ đệm dữ liệu. Trong tác vụ tiếp theo, bạn thêm mã sẽ 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 của bạn để đảm bảo mọi thứ 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 mở, và thêm một Activity để thêm từ.

Để xóa tất cả nội dung và điền lại cơ sở dữ liệu bất cứ khi nào ứng dụng được khởi động, bạn sẽ tạo một RoomDatabase.Callback và ghi đè onOpen(). Vì bạn không thể thực hiện các thao tác với cơ sở dữ liệu Phòng trên chuỗi giao diện người dùng, nên onOpen() sẽ chạy coroutine trên Trình điều phối IO.

Để chạy coroutine, chúng ta cần có CoroutineScope. Cập nhật phương thức getDatabase của lớp WordRoomDatabase để cũng nhận được phạm vi coroutine dưới dạng thông số:

fun getDatabase(
       context: Context,
       scope: CoroutineScope
  ): WordRoomDatabase {
...
}

Cập nhật trình chạy truy xuất cơ sở dữ liệu trong khối init của WordViewModel để chuyển 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 tùy chỉnh cho RoomDatabase.Callback(), đồng thời nhận được thông số CoroutineScope dưới dạng thông số hàm dựng. Sau đó, chúng ta ghi đè phương thức onOpen để điền 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ự xây dựng cơ sở dữ liệu ngay trước khi gọi .build() trên Room.databaseBuilder():

.addCallback(WordDatabaseCallback(scope))

Mã cuối cùng sẽ trông như sau:

@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 này trong 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 trong value/colors.xml:

<color name="buttonLabel">#FFFFFF</color>

Tạo tệp tài nguyên thứ nguyên mới:

  1. Nhấp vào mô-đun ứng dụng trong cửa sổ Dự án.
  2. Chọn Tệp > Mới > Tệp tài nguyên Android
  3. Trong các Bộ hạn định có sẵn, hãy chọn Thứ nguyên
  4. Đặt tên tệp: dimen

Thêm các tài nguyên thứ nguyên này trong values/dimens.xml:

<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>

Tạo một tệp Activity trống cho Android mới bằng mẫu Hoạt động trống:

  1. Chọn Tệp > Mới > Hoạt động > Hoạt động trống
  2. Nhập NewWordActivity cho tên Hoạt động.
  3. 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 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 các 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 quan sát 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, phương thức này sẽ gọi phương thức setWords() của bộ chuyển đổi để cập nhật dữ liệu đã 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 biến thành viên cho ViewModel:

private lateinit var wordViewModel: WordViewModel

Sử dụng ViewModelProvider để liên kết ViewModel của bạn với Activity.

Khi lần đầu tiên Activity bắt đầu, ViewModelProviders sẽ tạo ViewModel. Khi hoạt động bị hủy bỏ, ví dụ: thông qua sự thay đổi về cấu hình, ViewModel 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 ViewModel từ ViewModelProvider:

wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)

Ngoài ra, trong onCreate(), hãy thêm đối tượng tiếp nhận dữ liệu cho thuộc tính allWords LiveData của WordViewModel.

Phương thức onChanged() (phương thức mặc định cho Lambda của chúng tôi) kích hoạt khi dữ liệu ghi nhận được thay đổi và hoạt động chạy ở 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à sau khi quay lại MainActivity, để chèn từ mới vào cơ sở dữ liệu hoặc hiện 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ề bằng 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,bắt đầu NewWordActivity khi người dùng nhấn vào FAB. Trong MainActivity onCreate, tìm FAB và thêm onClickListener bằng mã này:

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ẽ trông giố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()
       }
   }
}

Bây giờ, 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ờ đây, khi bạn đã có một ứng dụng hoạt động, hãy tóm tắt những nội dung bạn đã xây dựng. Dưới đây là cấu trúc ứng dụng một lần nữa:

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 RecyclerViewWordListAdapter. Trong MainActivity, có một Observer quan sát các từ LiveData từ cơ sở dữ liệu và được thông báo khi các từ đó thay đổi.
  • NewWordActivity: sẽ 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 thành phần giao diện người dùng. Trong MainActivity, có một Observer quan sát các từ LiveData từ cơ sở dữ liệu và được thông báo khi chúng thay đổi.
  • Repository: quản lý một hoặc nhiều nguồn dữ liệu. Repository hiển thị các phương thức để ViewModel tương tác với 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 của Phòng.
  • Room: là một trình bao bọc xung quanh và triển khai cơ sở dữ liệu SQLite. Phòng làm rất nhiều việc cho bạn trước đây bạn phải tự làm.
  • Đối tượng truy cập dữ liệu (DAO): ánh xạ các lệnh gọi tới phương thức truy vấn cơ sở dữ liệu để khi Kho lưu trữ gọi một phương thức như getAlphabetizedWords(), Phòng có thể thực thi SELECT * from word_table ORDER BY word ASC.
  • Word: là lớp thực thể chứa một từ duy nhất.

* ViewsActivities (và Fragments) chỉ tương tác với dữ liệu thông qua ViewModel. Do đó, dữ liệu không quan trọng đến từ đâu.

Luồng dữ liệu cho nội dung cập nhật tự động về giao diện người dùng (Giao diện người dùng phản hồi)

Có thể cập nhật tự động vì chúng ta đang sử dụng LiveData. Trong MainActivity, có một Observer quan sát các từ LiveData từ cơ sở dữ liệu và được thông báo khi chúng thay đổi. Khi có thay đổi, phương thức onChange() của đối tượng tiếp nhận dữ liệu được thực thi và cập nhật mWords trong WordListAdapter.

Có thể quan sát dữ liệu vì dữ liệu là LiveData. Và quan sát được là LiveData<List<Word>> được 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. Lớp này 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. ViewsActivities (và Fragments) chỉ tương tác với dữ liệu thông qua ViewModel. Do đó, dữ liệu không quan trọng đến từ đâu.

Trong trường hợp này, dữ liệu này đến từ Repository. ViewModel không cần phải biết Kho lưu trữ đó tương tác với gì. Tài sản này chỉ cần biết cách tương tác với Repository, thông qua các phương thức mà Repository trình bày.

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 Phòng. Phòng là một trình bao bọc xung quanh và triển khai cơ sở dữ liệu SQLite. Phòng làm rất nhiều việc cho bạn trước đây bạn phải tự làm. Ví dụ: Phòng sẽ làm mọi việc mà bạn từng làm với một lớp SQLiteOpenHelper.

Phương thức DAO gọi các truy vấn cơ sở dữ liệu để khi Kho lưu trữ gọi một phương thức như getAllWords(), Phòng có thể thực thi SELECT * from word_table ORDER BY word ASC.

Vì kết quả trả về từ truy vấn được ghi nhận là LiveData, mỗi khi dữ liệu trong Phòng thay đổi, phương thức Observer giao diện\39;s onChanged() sẽ được thực thi và giao diện người dùng đã được cập nhật.

[Không bắt buộc] Tải mã giải pháp xuống

Nếu chưa có, bạn có thể xem mã giải pháp cho lớp học lập trình. Bạn có thể xem Kho lưu trữ github hoặc tải mã xuống tại đây:

Tải mã nguồn xuống

Giải nén tệp zip đã tải xuống. Thao tác này sẽ giải nén thư mục gốc (android-room-with-a-view-kotlin) chứa ứng dụng hoàn chỉnh.