Android Architecture Component – Phần 4: Tìm Hiểu Về Room

Posted by
Rating: 5.0/5. From 3 votes.
Please wait...

Chào mừng các bạn đã đến với bài viết về Android Architecture Component – Phần 4. Đây là bài viết trong các bài bổ sung cho chương trình học Android của Yellow Code Books.

Với việc xem hết bài viết hôm nay, thì bạn cũng đã cùng mình đi qua tổng cộng 4 thành phần lớn trong kiến trúc mới này của Android rồi đấy. Mình xin nhắc lại rằng, với việc hiểu và áp dụng các thành phần mới này vào ứng dụng, sẽ làm cho ứng dụng của bạn trở nên: mạnh mẽ hơn, dễ kiểm lỗi hơn, và dễ bảo trì hơn. Trước khi đi vào tìm hiểu thành phần cuối cùng này, mình xin tóm tắt lại 3 phần trước đây như sau.

  • ViewModel: đảm bảo dữ liệu luôn được bảo toàn trong suốt quá trình sống của UI. Như trong tình huống xoay màn hình chẳng hạn, dữ liệu trên UI vẫn sẽ được giữ nguyên vẹn sau khi xoay.
  • LiveData: thông báo ngay cho các view biết, khi dữ liệu mà view đó đang dùng, có sự thay đổi.
  • Lifecyle Aware Component: giúp ứng dụng hiểu và bám sát theo vòng đời của các thành phần UI. Giúp tránh các lỗi liên quan đến leak memory, là các lỗi mà ứng dụng cố gắng tương tác với các thành phần UI trong khi chúng không còn sống nữa.

Và bài học hôm nay sẽ nói về thành phần Room. Mời các bạn cùng đến với bài học.

Room Là Gì?

Theo như Google, Room là một bộ các thư viện, bao bọc lấy cơ sở dữ liệu SQLite. Nó giúp bạn có khả năng truy cập vào cơ sở dữ liệu này một cách dễ dàng hơn bao giờ hết, mà vẫn không ảnh hưởng gì đến sức mạnh vốn có của SQLite.

Còn nếu như bạn còn chưa hiểu SQLite là gì, thì mình chắc chắn rằng sẽ có một bài viết rõ ràng về nó ở các bài học Android theo chương trình của mình. Ở đây mình nói nhanh xíu. Là bất kỳ một ứng dụng Android nào rồi cũng phải xây dựng các chức năng lưu trữ dữ liệu. Tuy vậy, một ứng dụng Android lại có khá nhiều cách thức lưu trữ dữ liệu khác nhau, tùy vào loại dữ liệu cần lưu trữ. Một trong số cách lưu trữ đó là lưu trữ dữ liệu vào cơ sở dữ liệu SQLite. Cách này giúp chúng ta có thể lưu trữ vào máy một lượng thông tin tương đối lớn, như lưu danh sách các danh bạ điện thoại, lưu các ghi chú,… Mà các thao tác đọc/ghi trên thông tin đã lưu này rất nhanh và mượt mà,

Hiểu rộng hơn, SQLite chính là một phiên bản rút gọn của SQL truyền thống, nên nó sẽ phù hợp cho các thiết bị di động hoặc cho các cơ sở dữ liệu không quá lớn. Và may mắn là SQLite đã được tích hợp sẵn vào Android nên bạn cũng chẳng cần phải cài đặt gì cả. Nhưng dù là bản rút gọn, SQLite vẫn có được sức mạnh của một hệ cơ sở dữ liệu quan hệ SQL truyền thống. Dữ liệu trong SQLite vẫn được lưu trữ trên các bảng (Table), các cột (Column), có khóa chính, khóa phụ, và hầu hết các câu lệnh truy vấn (như CREATE, SELECT, INSERT, UPDATE, DELETE, DROP,…) đều có mặt trên SQLite cả.

Nếu như trước đây bạn nào từng làm việc với lưu trữ SQLite trên Android theo cách cũ, cái cách mà phải xây dựng một đối tượng con của SQLiteOpenHelper, sẽ biết được sự rườm rà trong cách viết code lúc bấy giờ. Hầu như bạn phải đọc và viết rất cẩn thận từng phương thức đọc/ghi. Nên với cách cũ, việc lãng phí code là điều hiển nhiên, và khả năng mà ứng dụng bị crash liên quan đến thao tác trên cơ sở dữ liệu diễn ra khá thường xuyên.

Nói như vậy có nghĩa là kiến thức về Room sẽ gắn chặt với kiến thức về lưu trữ cơ sở dữ liệu SQLite rồi. Nhưng nếu như bạn vẫn chưa từng bao giờ xây dựng ứng dụng với việc lưu trữ cơ sở dữ liệu bên trong, thì bài này vẫn sẽ thích hợp với bạn cho việc làm quen với thể loại ứng dụng này. Vì mình cũng sẽ trình bày lại các khái niệm liên quan đến thao tác với SQLite. Tuy vậy mình vẫn khuyến khích bạn tìm hiểu về SQLite trước khi bắt tay vào bài học thì sẽ cool hơn nhé.

Các Thành Phần Bên Trong Room

Mục này chúng ta sẽ mổ xẻ Room, để xem chúng có các thành phần nào, để mà dựa vào các thành phần này chúng ta sẽ biết cách sử dụng đến Room như thế nào với các mục bên dưới.

Entity

Như mình cũng nói trên kia, rằng SQLite là một dạng cơ sở dữ liệu được xây dựng dựa trên các Table. Vậy thì với Room, bạn hầu như không cần làm việc với khái niệm Table nữa. Mà bạn dùng chính Entity này để thay thế. Về mặt sử dụng thì Entity là một lớp, nhưng khi biên dịch Room sẽ dựa vào mỗi lớp Entity mà bạn đã định nghĩa để tạo ra các Table tương ứng. Các thuộc tính bên trong lớp Entity chính là các cột của Table đó.

DAO

Viết tắt của chữ Data Access Object. Như tên gọi, đây là một đối tượng chịu trách nhiệm làm việc chính với các Entity trong cơ sở dữ liệu. Đối tượng này sẽ giúp bạn định nghĩa các phương thức tương tác với các Entity, chẳng hạn như, insert(), getAll(), delete(),…. Khác với kiểu SQLiteDatabase ngày xưa khiến bạn tốn khá nhiều code để viết nên các phương thức này. DAO sẽ cung cấp cho chúng ta các annotation hoặc các kiểu viết câu truy vấn sao cho gọn gàng, nhanh chóng, và dễ quản lý nhất có thể.

Room Database

Chúng ta đang nói đến việc thao tác trên cơ sở dữ liệu SQLite. Nhưng chúng ta không cần bận tâm về cơ sở dữ liệu này. Chính Room Database sẽ bao bọc lấy cơ sở dữ liệu. Nó giúp khởi tạo một “cửa ngõ” để ứng dụng bắt đầu can thiệp với cơ sở dữ liệu. Ở Room Database chúng ta có thể khai báo ra các Entity cần dùng, các DAO liên quan đến các Entity. Và nơi đây cũng là nơi định nghĩa phiên bản của cơ sở dữ liệu của ứng dụng.

Toàn bộ mối tương quan giữa Room Database, DAOEntity được mô tả trong mô hình dưới đây.

Mo hình Room
Mô hình Room Database

Theo sơ đồ trên, bạn có thể hiểu rằng là. Để có thể dùng đến cơ sở dữ liệu SQLite, bạn, hay ứng dụng của bạn không cần quá hiểu biết về cơ sở dữ liệu này (vì sơ đồ có nói đến cơ sở dữ liệu đâu). Ứng dụng sẽ thông qua Room Database để khai báo ra một thể hiện của cơ sở dữ liệu, đồng thời khai báo các Entity sẽ dùng, mỗi Entity sẽ có một DAO để quản lý. Ứng dụng sẽ sử dụng các DAO này để mà đọc, ghi, xóa, sửa gì đó trên các Entity. Vậy là xong, phần bên dưới cơ sở dữ liệu thì Room sẽ lo liệu.

Lý thuyết thì bao gồm các nội dung trên đây như mình mô tả, nhưng có vẻ như lý thuyết này rất là đau đầu đúng không nào. Vậy mình mời các bạn cùng mình xây dựng một ứng dụng cụ thể như sau nhé.

Sử Dụng Room Để Xây Dựng Ứng Dụng Ghi Chú

Sau này với TourNote hoàn chỉnh mình cũng sẽ sử dụng kỹ thuật Room ở bài này. Nhưng hôm nay chúng ta sẽ xây dựng một TourNote đơn giản nhất, sao cho có thể tận dụng Room để lưu trữ lại các ghi chú đơn giản trong ứng dụng.

Mình xin nhắc lại là có nhiều hình thức để lưu trữ dữ liệu cho ứng dụng. Nhưng tùy vào đặc thù của dữ liệu mà chúng ta chọn lựa ra một cách thức lưu trữ thích hợp. Với kiểu ứng dụng lưu lại các ghi chú của bài viết hôm nay thì việc tổ chức lưu trữ với SQLite là thích hợp nhất. Tại sao vậy? Điều này cần rất nhiều tới kinh nghiệm xây dựng ứng dụng, nhưng hãy theo mình xây dựng nên ứng dụng của bài hôm nay, bạn sẽ phần nào hiểu được điều đó.

Ứng dụng xây dựng xong sẽ có giao diện trông như sau.

Màn hình chính của ứng dụng RoomDemo
Màn hình chính của ứng dụng

Bạn có thể thấy, giao diện của ứng dụng khá đơn sơ, vì chúng ta chỉ tập trung vào lưu trữ là chính. Các nút New Note trên ứng dụng giúp tạo ra một ghi chú mới, Find Note giúp tìm kiếm ghi chú. Các ghi chú khi thêm vào sẽ hiện ra ở danh sách màu xanh bên dưới các nút. Ngoài ra với mỗi ghi chú, chúng ta xây dựng thêm nút X để xóa từng ghi chú.

Do các bước xây dựng ứng dụng hôm nay khá dài, nên mình tách thành nhiều mục nhỏ hơn để bạn nắm.

Tạo Mới Project

Để bắt đầu, chúng ta cùng tạo mới một project Android với tùy chọn Empty Activity (xem hình dưới). Nếu bạn là người mới tiếp cận Android, hãy đọc bài này để biết cách thức tạo mới một project Android nhé.

Tạo mới project RoomDemo
Tạo mới project

Bước kế tiếp bạn có thể đặt tên project là gì cũng được, package name tùy chọn luôm. Ngôn ngữ nên để mặc định là Kotlin. Và nhớ check chọn vào “Use androidx.* artifacts” để làm quen với gói thư viện androidX mới thay vì Android Support cũ ngày xưa nhé. Dưới đây là hình ảnh khai báo project của mình.

Khai báo thông tin project RoomDemo
Khai báo thông tin cho project

Cấu Hình Cho Project

Sau khi nhấn Finish ở bước trên thì bạn đã tạo xong một project rồi đấy. Bước tiếp theo chúng ta cần phải cấu hình cho build.gradle sao cho có thể sử dụng được Room. Trong quá trình khai báo Room, chúng ta cũng nên khai báo sử dụng Coroutines, Coroutines là một thư viện tuyệt vời để đi kèm với Room, với Coroutines chúng ta sẽ dễ dàng thực thi các thread để thực hiện các tác vụ tương tác với cơ sở dữ liệu mà không làm ảnh hưởng đến UI thread. Code của Coroutines đọc vào cũng gọn và dễ hiểu nữa.

Sau đây là file build.gradle ở cấp độ module. Có thể file build.gradle của bạn sẽ hơi khác với của mình tùy thời điểm của cấu hình mặc định cho file này. Nhưng chung quy sẽ có những cập nhật như những gì mình tô sáng như sau.

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.yellowcode.roomsample"
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    packagingOptions {
        exclude 'META-INF/atomicfu.kotlin_module'
    }
}

dependencies {
    def room_version = "2.1.0-beta01"
    def coroutines_version = '1.2.0'

    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.core:core-ktx:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'

    // RecyclerView
    implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha05'

    // Coroutines
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

    // Room
    implementation "androidx.room:room-runtime:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
}

Tạo Một Entity

Bạn đã hiểu Entity là gì dựa trên giải nghĩa ở trên kia rồi đúng không nào. Bước này chúng ta xây dựng một Entity chính thức. Bạn cũng biết, vì ứng dụng của chúng ta giúp ghi lại các ghi chú của người dùng. Mình giả sử mỗi ghi chú sẽ bao gồm tiêu đề (title) và diễn đạt của ghi chú đó (content). Nên mình sẽ tạo ra một Entity có tên là Note, trong lớp này sẽ có 2 thuộc tính chính tương đương với 2 mục mà mình nói, lần lượt có tên là titlecontent. Thuộc tính id mình muốn thêm vào luôn, sẽ hữu dụng cho một vài tình huống, như tình huống sắp xếp thứ tự các ghi chú theo id tăng dần ở code bên mục DAO dưới.

@Entity(tableName = "note_table")
data class Note(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    @ColumnInfo(name = "title") val title: String,
    @ColumnInfo(name = "content") val content: String
)

Nếu bạn còn chưa hiểu lắm thì mình diễn đạt ra một xíu. Với khai báo trên, thì khi ứng dụng thực thi, người dùng tiến hành ghi chú lên cơ sở dữ liệu, thì một Table sẽ được lưu xuống SQLite dạng như thế này đây.

Room - Mô phỏng Table note_table
Diễn đạt note_table

Bạn nên biết rằng Entity Note nên là một lớp được khai báo tối thiểu, nó chỉ chứa các thuộc tính bên trong, giúp diễn đạt cho một Table trong cơ sở dữ liệu mà thôi. Do đó bạn nên tránh các khai báo phương thức rườm rà đối với lớp này. Chính vì vậy mà mình khai báo Note là một data trong Kotlin cho nó ngắn gọn.

Vậy thì các annotation xung quanh Note mà bạn tạo ra là gì thế, mình xin giải thích chúng như sau.

  • @Entity – Bạn phải khai báo annotation này trước mỗi Entity để Room hiểu rằng đây là một Entity, và vì vậy nó sẽ giúp tạo ra một Table bên trong cơ sở dữ liệu. Trong khi sử dụng annotation @Entity này, chúng ta cũng sẽ sử dụng đến thuộc tính tableName để chỉ định tên của Table, mà mình đặt là “note_table” luôn.
  • @PrimaryKey – Annotation này giúp chúng ta định nghĩa thuộc tính nào là khóa chính của Table. Khóa chính là gì ư? Bạn có thể xem đầy đủ hơn về khái niệm khóa chính (primary key) ở link này. Trong khi sử dụng annotate này, chúng ta có thể dùng đến thuộc tính autoGenerate để cho phép hệ thống tự tạo ra giá trị id này cho chúng ta.
  • @ColumnInfo – Chúng ta dùng Annotation này với thuộc tính kèm theo là name để đặt lại tên cho Column (cột) của Table. Lưu ý rằng bạn có thể không cần phải khai báo ColumnInfo như trên, khi đó Room sẽ lấy tên của thuộc tính làm tên của Column luôn.

Trên đây chỉ là một ví dụ nhỏ liên quan đến Entity, nếu bạn muốn biết nhiều hơn về cách sử dụng các annotattion liên quan đến Entity cũng như các thuộc tính kèm theo chúng, thì có thể xem thêm ở link này nhé.

Tạo Một DAO

DAO sẽ giúp ứng dụng ghi dữ liệu vào cho Entity Note trên kia. Tương tự như Entity, DAO cũng dùng đến các annotation để giúp giảm thiểu code. Mời bạn cùng đến với lớp NoteDao trước khi xem qua phần giải thích về lớp này bên dưới.

@Dao
interface NoteDao {

    @Query("SELECT * FROM note_table ORDER BY id ASC")
    suspend fun getAllNotes(): List<Note>

    @Query("SELECT * FROM note_table WHERE title LIKE :title")
    suspend fun findNoteByTitle(title: String): Note

    @Insert
    suspend fun insert(note: Note)

    @Delete
    suspend fun delete(note: Note)
}

Đầu tiên bạn nên biết lớp lãnh nhiệm chức năng là một DAO này phải là một interface. Do đó các phương thức bên trong lớp này sẽ không cần phải khai báo thân hàm. Các từ khóa suspend ở mỗi phương thức của NoteDao chính là các phương thức giúp tương thích với Coroutines thôi. Và đây là các annotation kèm theo với việc khai báo các chức năng cho lớp này.

  • @Dao – Cũng giống như @Entity trên kia. Annotation Dao này giúp Room hiểu rằng đây là một DAO và sẽ hành xử với các phương thức đi kèm nó với các annotation như sau.
  • @Insert, @Update, @Delete – Là các annotation liên quan đến các chức năng thêm, sửa, xóa đối với các table của cơ sở dữ liệu. Bạn có thể thấy phương thức insert() của mình nên đi kèm với annotation Insert. Phương thức delete() đi kèm với annotation @Delete là vậy. Bạn thấy không cần viết code lằng nhằng cho các chức năng này đúng không nào.
  • @Query – Ngoài các annotation khá tiện lợi trên đây, Dao cũng hỗ trợ chúng ta một annotation mạnh hơn đó là Query. Annotation Query giúp chúng ta viết các câu truy vấn vào trong đó. Nếu bạn nào đã quen với cơ sở dữ liệu SQL thì hiển nhiên cũng biết các câu truy vấn này rồi. Còn nếu như bạn muốn biết ngọn nguồn của tất cả các câu truy vấn bên trong SQLite, hãy dành thời gian xem qua trang này nhé.

Cũng tương tự Entity, Dao có khá nhiều kiến thức mà mình cũng khó có thể trình bày hết. Nếu bạn có thời gian, hãy tìm hiểu thêm ở link này.

Tạo Một RoomDatabase

Chúng ta đã tạo ra Table (Entity), chúng ta cũng đã xây dựng sẵn các câu truy vấn trên table đó (Dao). Nhưng chúng ta vẫn chưa có các câu lệnh liên quan đến cơ sở dữ liệu chính, để mà gom các Table và các câu truy vấn vào làm một nơi, để ứng dụng có thể dễ dàng sử dụng. RoomDatabase ở bước này sẽ giúp làm việc này.

Nào, để xây dựng một RoomDatabase, bạn hãy nhớ như sau. RoomDatabase phải là một lớp abstract. Sau đó nó phải kế thừa từ lớp RoomDatabase. Bên trong lớp này bạn phải xây dựng các phương thức abstract để lấy về các Dao đã khai báo, trường hợp của chúng ta có NoteDao nên bạn để ý code dưới đây. Và một ý nữa, bởi vì trong suốt quá trình sống của ứng dụng, chúng ta chỉ cần duy nhất một RoomDatabase thôi, do đó hãy xây dựng lớp này theo mô hình mẫu có tên Singleton.

Dài dòng quá, mời bạn xem qua code của lớp NoteRoomDatatbase.

@Database(entities = [Note::class], version = 1)
abstract class NoteRoomDatabase : RoomDatabase() {

    abstract fun noteDao(): NoteDao

    companion object {
        private var INSTANCE: NoteRoomDatabase?= null
        private val DB_NAME = "note_db"

        fun getDatabase(context: Context): NoteRoomDatabase {
            val tempInstance = INSTANCE
            if (tempInstance != null) {
                return tempInstance
            }
            synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    NoteRoomDatabase::class.java,
                    DB_NAME
                ).build()
                INSTANCE = instance
                return instance
            }
        }
    }
}

Ngoài các ghi chú trên kia thì lớp này chỉ có thêm duy nhất một annotation mà chúng ta cần quan tâm.

  • @Database – Như bao thành phần khác của Room trên kia, bạn phải khai báo annotation là Database cho lớp này để Room biết đây là một RoomDatabase. Với khai báo này bạn truyền thêm thông tin danh sách các Entity trong ứng dụng (hiện tại chúng ta chỉ có mỗi một EntityNote). Và còn truyền thông tin về version của cơ sở dữ liệu trong ứng dụng nữa. Do bản demo này là xây dựng cơ sở dữ liệu hoàn toàn mới nên version của nó bắt đầu là 1. Về sau, nếu bạn có sửa chữa nâng cấp gì đó cho cơ sở dữ liệu, như thêm một Entity mới chẳng hạn, hay bạn đơn giản muốn chuyển dữ liệu hiện tại của người dùng từ database cũ sang database mới theo kiến trúc Room này, thì nhớ nâng con số này lên, và định nghĩa thêm các phương thức chuyển đổi hoặc nâng cấp database nữa nhé. Nói chung là khá phức tạp. Khuôn khổ bài viết hôm nay của mình chỉ là tạo database mới khi người dùng lần đầu tiên sử dụng ứng dụng của chúng ta thôi.

Xây Dựng Giao Diện Ứng Dụng

Xem như phần xây dựng các phương thức liên quan đến cơ sở dữ liệu đã xong ở các bước trên kia rồi đấy. Phần này chúng ta chỉ xây dựng giao diện và gắn các thao tác đến với cơ sở dữ liệu nữa là xong rồi.

Xây Dựng Note Item

Đầu tiên chúng ta sẽ xây dựng giao diện cho mỗi ghi chú trước. Mình đặt tên file giao diện này là note_item.xml. Vì ghi chú của chúng ta bao gồm tiêu đề và nội dung, và một Button xóa ghi chú nữa. Nên note_item.xml sẽ như sau.

<?xml version="1.0" encoding="utf-8"?>
<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="wrap_content"
        android:id="@+id/linearLayout" android:background="#00BCD4">

    <TextView
            android:id="@+id/note_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#000000"
            android:textSize="15dp"
            android:textStyle="bold"
            app:layout_constraintStart_toStartOf="parent" android:layout_marginLeft="16dp"
            android:layout_marginStart="16dp" android:layout_marginBottom="4dp"
            app:layout_constraintBottom_toTopOf="@+id/note_content" tools:text="Title"
            app:layout_constraintTop_toTopOf="parent" android:layout_marginEnd="8dp"
            app:layout_constraintEnd_toStartOf="@+id/button_delete" android:layout_marginRight="8dp"
            app:layout_constraintHorizontal_bias="0.0" android:layout_marginTop="16dp"/>

    <TextView
            android:id="@+id/note_content"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:maxLines="4"
            android:ellipsize="end"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/button_delete" android:layout_marginBottom="16dp"
            tools:text="Content" app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintHorizontal_bias="0.0" android:layout_marginStart="16dp"
            android:layout_marginLeft="16dp" android:layout_marginTop="4dp"
            app:layout_constraintTop_toBottomOf="@+id/note_title" android:layout_marginEnd="8dp"
            android:layout_marginRight="8dp"/>
    <Button
            android:text="X"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/button_delete" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="16dp"
            android:layout_marginRight="16dp" android:layout_marginTop="16dp" app:layout_constraintTop_toTopOf="parent"
            android:layout_marginBottom="16dp" app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Sau đó hãy xây dựng adapter cho danh sách các ghi chú này. Mình đặt tên adapter này là NoteListAdapter. Trong adapter này có truyền vào một NoteRoomDatabase có tên là noteDB mà chúng ta sẽ khai báo sau ở bên ngoài. Dựa trên noteDB chúng ta có thể xây dựng trước phương thức xóa ghi chú, và lấy tất cả ghi chú từ cơ sở dữ liệu thông qua đoạn code như bên dưới.

class NoteListAdapter internal constructor(context: Context, val noteDB: NoteRoomDatabase) : RecyclerView.Adapter<NoteListAdapter.NoteViewHolder>() {

    private val inflater: LayoutInflater = LayoutInflater.from(context)
    private var notes = emptyList<Note>()

    private val job = Job()
    private val uiScope = CoroutineScope(Dispatchers.Main + job)

    internal fun setNotes(notes: List<Note>) {
        this.notes = notes
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder {
        val itemView = inflater.inflate(R.layout.note_item, parent, false)
        return NoteViewHolder(itemView)
    }

    override fun getItemCount(): Int {
        return notes.size
    }

    override fun onBindViewHolder(holder: NoteViewHolder, position: Int) {
        val currentNote = notes[position]

        holder.titleItemView.text = currentNote.title
        holder.contentItemView.text = currentNote.content
        holder.deleteItemView.setOnClickListener {
            uiScope.launch {
                // Delete currentNote
                noteDB?.noteDao()?.delete(currentNote)

                // Get all noted again
                notes = noteDB?.noteDao()?.getAllNotes()
                notifyDataSetChanged()
            }
        }
    }

    inner class NoteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val titleItemView = itemView.note_title
        val contentItemView = itemView.note_content
        val deleteItemView = itemView.button_delete
    }
}

Xây Dựng Giao Diện New Note

Mục này chúng ta xây dựng màn hình cho phép người dùng thêm vào một ghi chú. Giao diện cuối cùng sẽ trông như sau.

Màn hình thêm ghi chú của RoomDemo
Màn hình thêm ghi chú mới

Để tạo một màn hình hoàn toàn mới thì bạn có thể sử dụng công cụ tạo Activity của Android Studio, như các hình sau.

Room - Tạo mới Activity
Tạo mới một Activity
Room - Khai báo Activity
Khai báo các thông số cho Activity New Note

Còn đây là code của giao diện. File activity_new_note.xml.

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".activities.NewNoteActivity">

    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

        <EditText
                android:id="@+id/title_note"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="20dp"
                android:layout_marginTop="10dp"
                android:layout_marginRight="20dp"
                android:hint="Enter title"
                android:inputType="text"
                android:padding="20dp" />

        <EditText
                android:id="@+id/content_note"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="20dp"
                android:layout_marginTop="10dp"
                android:layout_marginRight="20dp"
                android:maxLines="4"
                android:hint="Enter content"
                android:inputType="textEmailAddress"
                android:padding="20dp" />

        <Button
                android:id="@+id/button_save"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_margin="20dp"
                android:padding="20dp"
                android:text="Save" />
    </LinearLayout>
</ScrollView>

Đối với file NewNoteActivity thì chúng ta cần khai báo NoteRoomDatabase có tên là noteDB. Bắt sự kiện click của Button Save và sử dụng noteDB đã khai báo để ghi thông tin ghi chú này xuống cơ sở dữ liệu.

class NewNoteActivity : AppCompatActivity(), CoroutineScope {

    private var noteDB: NoteRoomDatabase ?= null

    private lateinit var mJob: Job
    override val coroutineContext: CoroutineContext
        get() = mJob + Dispatchers.Main

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_new_note)

        mJob = Job()

        noteDB = NoteRoomDatabase.getDatabase(this)

        button_save.setOnClickListener {
            launch {
                // Save note into database
                val strTitle: String = title_note.text.toString()
                val strContent: String = content_note.text.toString()
                noteDB?.noteDao()?.insert(Note(title = strTitle, content = strContent))

                finish()
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()

        mJob.cancel()
    }
}

Xây Dựng Giao Diện Chính

Hình thực tế ứng dụng RoomDemo
Hình thực tế giao diện chính sau khi được user thêm vào 2 ghi chú

Giao diện chính của ứng dụng sau khi thực thi và khi người dùng thêm vào 2 ghi chú sẽ như trên đây. Chúng ta sẽ sử dụng RecyclerView để hiển thị danh sách các ghi chú này của người dùng. File activity_main.xml sẽ như sau.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".activities.MainActivity">
    <Button
            android:text="New Note"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:id="@+id/button_new_note" android:layout_marginTop="8dp"
            app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
            android:layout_marginLeft="8dp" android:layout_marginStart="8dp" app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp" android:layout_marginRight="8dp"/>
    <Button
            android:text="Find Note"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/button_find" android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/button_new_note"
            app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp" android:layout_marginRight="8dp"/>
    <EditText
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:inputType="textPersonName"
            android:ems="10"
            android:id="@+id/edittext_find"
            app:layout_constraintBaseline_toBaselineOf="@+id/button_find"
            android:hint="Enter note title" android:focusable="auto" android:focusableInTouchMode="true"
            android:layout_marginStart="8dp" app:layout_constraintStart_toStartOf="parent"
            android:layout_marginLeft="8dp" android:layout_marginEnd="8dp"
            app:layout_constraintEnd_toStartOf="@+id/button_find" android:layout_marginRight="8dp"/>
    <androidx.recyclerview.widget.RecyclerView
            android:layout_width="0dp"
            android:layout_height="0dp" android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/button_find" app:layout_constraintStart_toStartOf="parent"
            android:layout_marginLeft="8dp" android:layout_marginStart="8dp" app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp" android:layout_marginRight="8dp" android:id="@+id/recycler_notes"
            android:layout_marginBottom="8dp" app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

File MainActivity cũng sẽ sử dụng NoteRoomDatabase để lấy danh sách các ghi chú, hoặc tìm kiếm ghi chú. Tất cả code chính sử dụng đến cơ sở dữ liệu cũng sẽ ngắn gọn và dễ hiểu như những giao diện trên kia đúng không nào.

class MainActivity : AppCompatActivity(), CoroutineScope, View.OnClickListener {

    private var noteDB: NoteRoomDatabase? = null
    private var adapter: NoteListAdapter? = null

    private lateinit var mJob: Job
    override val coroutineContext: CoroutineContext
        get() = mJob + Dispatchers.Main

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mJob = Job()

        noteDB = NoteRoomDatabase.getDatabase(this)
        adapter = NoteListAdapter(MainActivity@this, noteDB!!)

        recycler_notes.adapter = adapter
        recycler_notes.layoutManager = LinearLayoutManager(this)

        button_new_note.setOnClickListener(this)
        button_find.setOnClickListener(this)
    }

    override fun onResume() {
        super.onResume()

        getAllNotes()
    }

    override fun onDestroy() {
        super.onDestroy()

        mJob.cancel()
    }

    override fun onClick(v: View?) {
        when(v) {
            button_new_note -> {
                val newNoteIntent = Intent(this, NewNoteActivity::class.java)
                startActivity(newNoteIntent)
            }

            button_find -> {
                findNote()
            }
        }
    }

    // Get all notes
    fun getAllNotes() {
        launch {
            val notes: List<Note>? = noteDB?.noteDao()?.getAllNotes()
            if (notes != null) {
                adapter?.setNotes(notes)
            }
        }
    }

    // Find note
    fun findNote() = launch {
        val strFind = edittext_find.text.toString()
        if (!TextUtils.isEmpty(strFind)) {
            // Find if the text is not empty
            val note: Note? = noteDB?.noteDao()?.findNoteByTitle(strFind)
            if (note != null) {
                val notes: List<Note> = mutableListOf(note)
                adapter?.setNotes(notes)
            }
        } else {
            // Else get all notes
            getAllNotes()
        }
    }
}

Giờ thì bạn có thể thực thi chương trình để xem kết quả như thế nào được rồi nhé.

Bạn vừa xem qua đủ 4 phần Android Architecture Component. Cảm ơn bạn đã đọc và ủng hộ blog.

Download Source Code Mẫu

Bạn có thể download source code mẫu của bài này ở đây.

Cảm ơn bạn đã đọc các bài viết của Yellow Code Books. Bạn hãy ủng hộ blog bằng cách:

Đánh giá 5 sao bên dưới mỗi bài nếu thấy thích.
Comment bên dưới mỗi bài nếu có thắc mắc.
Để lại địa chỉ email của bạn ở thanh bên phải để nhận được thông báo sớm nhất khi có bài viết mới.
Chia sẻ các bài viết của Yellow Code Books đến nhiều người khác.

Gửi phản hồi