Android Room with a View - Kotlin

จุดประสงค์ของ Architecture Components คือการให้คำแนะนำเกี่ยวกับสถาปัตยกรรมของแอป โดยมีไลบรารีสำหรับงานทั่วไป เช่น การจัดการวงจร และการคงอยู่ของข้อมูล คอมโพเนนต์สถาปัตยกรรมช่วยให้คุณจัดโครงสร้างแอปในลักษณะที่แข็งแกร่ง ทดสอบได้ และบำรุงรักษาได้โดยมีโค้ดบอยเลอร์เพลตน้อยลง ไลบรารีคอมโพเนนต์สถาปัตยกรรมเป็นส่วนหนึ่งของ Android Jetpack

นี่คือ Codelab เวอร์ชัน Kotlin ดูเวอร์ชันในภาษาโปรแกรม Java ได้ที่นี่

หากพบปัญหา (ข้อบกพร่องของโค้ด ข้อผิดพลาดทางไวยากรณ์ คำที่ไม่ชัดเจน ฯลฯ) ขณะทำตาม Codelab นี้ โปรดรายงานปัญหาผ่านลิงก์รายงานข้อผิดพลาดที่มุมซ้ายล่างของ Codelab

ข้อกำหนดเบื้องต้น

คุณต้องคุ้นเคยกับ Kotlin, แนวคิดการออกแบบเชิงออบเจ็กต์ และพื้นฐานการพัฒนาแอป Android โดยเฉพาะอย่างยิ่ง

นอกจากนี้ การทำความคุ้นเคยกับรูปแบบสถาปัตยกรรมซอฟต์แวร์ที่แยกข้อมูลออกจากอินเทอร์เฟซผู้ใช้ เช่น MVP หรือ MVC ก็เป็นประโยชน์เช่นกัน Codelab นี้จะใช้สถาปัตยกรรมที่กำหนดไว้ในคู่มือสถาปัตยกรรมแอป

Codelab นี้มุ่งเน้นที่คอมโพเนนต์สถาปัตยกรรมของ Android เรามีแนวคิดและโค้ดที่ไม่เกี่ยวข้องให้คุณคัดลอกและวางได้ง่ายๆ

หากคุณไม่คุ้นเคยกับ Kotlin เรามี Codelab เวอร์ชันนี้ในภาษาโปรแกรม Java ที่นี่

สิ่งที่คุณต้องทำ

ในโค้ดแล็บนี้ คุณจะได้เรียนรู้วิธีออกแบบและสร้างแอปโดยใช้ Room, ViewModel และ LiveData ของ Architecture Components รวมถึงสร้างแอปที่ทำสิ่งต่อไปนี้

  • ใช้สถาปัตยกรรมที่แนะนำโดยใช้คอมโพเนนต์สถาปัตยกรรมของ Android
  • ทำงานร่วมกับฐานข้อมูลเพื่อรับและบันทึกข้อมูล รวมถึงป้อนข้อมูลล่วงหน้าในฐานข้อมูลด้วยคำบางคำ
  • แสดงคำทั้งหมดใน RecyclerView ใน MainActivity
  • เปิดกิจกรรมที่ 2 เมื่อผู้ใช้แตะปุ่ม + เมื่อผู้ใช้ป้อนคำ ระบบจะเพิ่มคำนั้นลงในฐานข้อมูลและรายการ

แอปนี้ใช้งานง่าย แต่ก็มีความซับซ้อนเพียงพอที่จะใช้เป็นเทมเพลตในการต่อยอดได้ ตัวอย่าง

สิ่งที่คุณต้องมี

  • Android Studio 3.0 ขึ้นไป และความรู้เกี่ยวกับวิธีใช้งาน ตรวจสอบว่า Android Studio ได้รับการอัปเดตแล้ว รวมถึง SDK และ Gradle
  • อุปกรณ์หรือโปรแกรมจำลอง Android

Codelab นี้มีโค้ดทั้งหมดที่คุณต้องใช้ในการสร้างแอปที่สมบูรณ์

การใช้ Architecture Components และการใช้สถาปัตยกรรมที่แนะนำมีหลายขั้นตอน สิ่งสำคัญที่สุดคือการสร้างโมเดลในใจว่าเกิดอะไรขึ้น ทำความเข้าใจว่าชิ้นส่วนต่างๆ เชื่อมโยงกันอย่างไรและข้อมูลไหลเวียนอย่างไร ขณะทำ Codelab นี้ อย่าเพียงแค่คัดลอกและวางโค้ด แต่ให้พยายามสร้างความเข้าใจภายใน

เพื่อเป็นการแนะนำคำศัพท์ ต่อไปนี้คือข้อมูลเบื้องต้นเกี่ยวกับ Architecture Components และวิธีการทำงานร่วมกัน โปรดทราบว่า Codelab นี้มุ่งเน้นที่คอมโพเนนต์บางส่วน ได้แก่ LiveData, ViewModel และ Room ระบบจะอธิบายแต่ละคอมโพเนนต์เพิ่มเติมเมื่อคุณใช้งาน

แผนภาพนี้แสดงรูปแบบพื้นฐานของสถาปัตยกรรม

เอนทิตี: คลาสที่ใส่คำอธิบายประกอบซึ่งอธิบายตารางฐานข้อมูลเมื่อทำงานกับ Room

ฐานข้อมูล SQLite: ที่เก็บข้อมูลในอุปกรณ์ ไลบรารี Room Persistence จะสร้างและดูแลฐานข้อมูลนี้ให้คุณ

DAO: ออบเจ็กต์การเข้าถึงข้อมูล การแมปการค้นหา SQL กับฟังก์ชัน เมื่อใช้ DAO คุณจะเรียกใช้เมธอดและ Room จะจัดการส่วนที่เหลือให้

ฐานข้อมูล Room: ลดความซับซ้อนในการทำงานกับฐานข้อมูลและทำหน้าที่เป็นจุดเข้าถึงฐานข้อมูล SQLite ที่อยู่เบื้องหลัง (ซ่อน SQLiteOpenHelper) ฐานข้อมูล Room ใช้ DAO เพื่อออกคำค้นหาไปยังฐานข้อมูล SQLite

ที่เก็บ: คลาสที่คุณสร้างขึ้นซึ่งใช้เพื่อจัดการแหล่งข้อมูลหลายแหล่งเป็นหลัก

ViewModel: ทำหน้าที่เป็นศูนย์กลางการสื่อสารระหว่างที่เก็บ (ข้อมูล) กับ UI UI ไม่จำเป็นต้องกังวลเกี่ยวกับแหล่งที่มาของข้อมูลอีกต่อไป อินสแตนซ์ ViewModel จะยังคงอยู่เมื่อมีการสร้าง Activity/Fragment ขึ้นมาใหม่

LiveData: คลาสที่เก็บข้อมูลซึ่งสังเกตได้ จัดเก็บ/แคชข้อมูลเวอร์ชันล่าสุดเสมอ และจะแจ้งให้ผู้สังเกตการณ์ทราบเมื่อมีการเปลี่ยนแปลงข้อมูล LiveData ตระหนักถึงวงจร คอมโพเนนต์ UI จะสังเกตข้อมูลที่เกี่ยวข้องเท่านั้น และจะไม่หยุดหรือกลับมาสังเกต LiveData จะจัดการทั้งหมดนี้โดยอัตโนมัติเนื่องจากทราบการเปลี่ยนแปลงสถานะวงจรที่เกี่ยวข้องขณะสังเกตการณ์

ภาพรวมสถาปัตยกรรม RoomWordSample

แผนภาพต่อไปนี้แสดงส่วนประกอบทั้งหมดของแอป กรอบสี่เหลี่ยมแต่ละกรอบ (ยกเว้นฐานข้อมูล SQLite) แสดงถึงคลาสที่คุณจะสร้าง

  1. เปิด Android Studio แล้วคลิกเริ่มโปรเจ็กต์ Android Studio ใหม่
  2. ในหน้าต่างสร้างโปรเจ็กต์ใหม่ ให้เลือกกิจกรรมว่าง แล้วคลิกถัดไป
  3. ในหน้าจอถัดไป ให้ตั้งชื่อแอปว่า RoomWordSample แล้วคลิกเสร็จสิ้น

จากนั้นคุณจะต้องเพิ่มไลบรารีคอมโพเนนต์ลงในไฟล์ Gradle

  1. ใน Android Studio ให้คลิกแท็บโปรเจ็กต์ แล้วขยายโฟลเดอร์สคริปต์ Gradle

เปิด build.gradle (Module: app)

  1. ใช้ปลั๊กอิน Kotlin kapt Annotation Processor โดยเพิ่มปลั๊กอินหลังจากปลั๊กอินอื่นๆ ที่กำหนดไว้ที่ด้านบนของไฟล์ build.gradle (Module: app)
apply plugin: 'kotlin-kapt'
  1. เพิ่มบล็อก packagingOptions ภายในบล็อก android เพื่อยกเว้นโมดูลฟังก์ชันย่อยออกจากแพ็กเกจและป้องกันไม่ให้มีคำเตือน
android {
    // other configuration (buildTypes, defaultConfig, etc.)

    packagingOptions {
        exclude 'META-INF/atomicfu.kotlin_module'
    }
}
  1. เพิ่มโค้ดต่อไปนี้ที่ส่วนท้ายของบล็อก 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. ในไฟล์ build.gradle (Project: RoomWordsSample) ให้เพิ่มหมายเลขเวอร์ชันที่ส่วนท้ายของไฟล์ตามที่ระบุไว้ในโค้ดด้านล่าง
ext {
    roomVersion = '2.2.5'
    archLifecycleVersion = '2.2.0'
    coreTestingVersion = '2.1.0'
    materialVersion = '1.1.0'
    coroutines = '1.3.4'
}

ข้อมูลของแอปนี้คือคำ และคุณจะต้องมีตารางอย่างง่ายเพื่อเก็บค่าเหล่านั้น

Room ช่วยให้คุณสร้างตารางผ่าน Entity ได้ มาเริ่มกันเลย

  1. สร้างไฟล์คลาส Kotlin ใหม่ชื่อ Word ซึ่งมี Word data class
    คลาสนี้จะอธิบายเอนทิตี (ซึ่งแสดงตาราง SQLite) สำหรับคำของคุณ พร็อพเพอร์ตี้แต่ละรายการในคลาสจะแสดงคอลัมน์ในตาราง ท้ายที่สุดแล้ว Room จะใช้พร็อพเพอร์ตี้เหล่านี้ทั้งในการสร้างตารางและสร้างออบเจ็กต์จากแถวในฐานข้อมูล

รหัสมีดังนี้

data class Word(val word: String)

หากต้องการให้คลาส Word มีความหมายต่อฐานข้อมูล Room คุณต้องใส่คำอธิบายประกอบ คำอธิบายประกอบจะระบุว่าแต่ละส่วนของคลาสนี้เกี่ยวข้องกับรายการในฐานข้อมูลอย่างไร Room ใช้ข้อมูลนี้เพื่อสร้างโค้ด

หากคุณพิมพ์คำอธิบายประกอบด้วยตนเอง (แทนการวาง) Android Studio จะนำเข้าคลาสคำอธิบายประกอบโดยอัตโนมัติ

  1. อัปเดตWordคลาสด้วยคำอธิบายประกอบตามที่แสดงในโค้ดนี้
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)

มาดูกันว่าคำอธิบายประกอบเหล่านี้ทำอะไรได้บ้าง

  • @Entity(tableName = "word_table")
    คลาส @Entity แต่ละคลาสแสดงถึงตาราง SQLite ใส่คำอธิบายประกอบการประกาศคลาสเพื่อระบุว่าเป็นเอนทิตี คุณระบุชื่อตารางได้หากต้องการให้ชื่อต่างจากชื่อของคลาส ซึ่งจะตั้งชื่อตารางเป็น "word_table"
  • @PrimaryKey
    เอนทิตีทุกรายการต้องมีคีย์หลัก เพื่อความสะดวก คำแต่ละคำจะทำหน้าที่เป็นคีย์หลักของตัวเอง
  • @ColumnInfo(name = "word")
    ระบุชื่อของคอลัมน์ในตารางหากต้องการให้แตกต่างจากชื่อของตัวแปรสมาชิก ซึ่งจะตั้งชื่อคอลัมน์เป็น "word"
  • พร็อพเพอร์ตี้ทุกรายการที่จัดเก็บไว้ในฐานข้อมูลต้องมีระดับการมองเห็นเป็นสาธารณะ ซึ่งเป็นค่าเริ่มต้นของ Kotlin

ดูรายการคำอธิบายประกอบทั้งหมดได้ในข้อมูลอ้างอิงสรุปแพ็กเกจห้อง

DAO คืออะไร

ใน DAO (ออบเจ็กต์การเข้าถึงข้อมูล) คุณระบุการค้นหา SQL และเชื่อมโยงกับการเรียกเมธอด คอมไพเลอร์จะตรวจสอบ SQL และสร้างการค้นหาจากคำอธิบายประกอบที่สะดวกสำหรับการค้นหาทั่วไป เช่น @Insert Room ใช้ DAO เพื่อสร้าง API ที่สะอาดสำหรับโค้ดของคุณ

DAO ต้องเป็นอินเทอร์เฟซหรือคลาสแบบนามธรรม

โดยค่าเริ่มต้น ระบบจะเรียกใช้การค้นหาทั้งหมดในเธรดแยกต่างหาก

Room รองรับโครูทีน ซึ่งช่วยให้คุณใส่คำอธิบายประกอบในคําค้นหาด้วยตัวแก้ไข suspend แล้วเรียกใช้จากโครูทีนหรือจากฟังก์ชันระงับอื่นได้

ใช้ DAO

มาเขียน DAO ที่ให้การค้นหาสำหรับสิ่งต่อไปนี้กัน

  • การเรียงคำทั้งหมดตามลำดับตัวอักษร
  • การแทรกคำ
  • การลบคำทั้งหมด
  1. สร้างไฟล์คลาส Kotlin ใหม่ชื่อ WordDao
  2. คัดลอกและวางโค้ดต่อไปนี้ลงใน WordDao และแก้ไขการนำเข้าตามที่จำเป็นเพื่อให้คอมไพล์ได้
@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()
}

มาดูขั้นตอนกัน

  • WordDao เป็นอินเทอร์เฟซ DAO ต้องเป็นอินเทอร์เฟซหรือคลาสแบบนามธรรม
  • คำอธิบายประกอบ @Dao จะระบุว่าเป็นคลาส DAO สำหรับ Room
  • suspend fun insert(word: Word) : ประกาศฟังก์ชันระงับเพื่อแทรกคำ 1 คำ
  • @InsertAnnotation เป็น Annotation ของเมธอด DAO แบบพิเศษที่คุณไม่ต้องระบุ SQL ใดๆ (นอกจากนี้ยังมีคำอธิบายประกอบ @Delete และ @Update สำหรับการลบและอัปเดตแถว แต่คุณไม่ได้ใช้คำอธิบายประกอบเหล่านั้นในแอปนี้)
  • onConflict = OnConflictStrategy.IGNORE: กลยุทธ์ onConflict ที่เลือกจะละเว้นคำใหม่หากคำนั้นเหมือนกับคำที่มีอยู่ในรายการอยู่แล้ว ดูข้อมูลเพิ่มเติมเกี่ยวกับกลยุทธ์การแก้ไขข้อขัดแย้งที่มีได้ในเอกสารประกอบ
  • suspend fun deleteAll(): ประกาศฟังก์ชันระงับเพื่อลบคำทั้งหมด
  • ไม่มีคำอธิบายประกอบที่สะดวกสำหรับการลบเอนทิตีหลายรายการ จึงมีการใส่คำอธิบายประกอบด้วย @Query ทั่วไป
  • @Query("DELETE FROM word_table"): @Query requires that you provide a SQL query as a string parameter to the annotation, allowing for complex read queries and other operations.
  • fun getAlphabetizedWords(): List<Word>: วิธีรับคำทั้งหมดและให้ส่งคืน List ของ Words
  • @Query("SELECT * from word_table ORDER BY word ASC"): คำค้นหาที่แสดงผลรายการคำที่จัดเรียงจากน้อยไปมาก

เมื่อข้อมูลเปลี่ยนแปลง คุณมักต้องการดำเนินการบางอย่าง เช่น แสดงข้อมูลที่อัปเดตแล้วใน UI ซึ่งหมายความว่าคุณต้องสังเกตข้อมูลเพื่อให้สามารถตอบสนองได้เมื่อข้อมูลเปลี่ยนแปลง

ซึ่งอาจทำได้ยากขึ้นอยู่กับวิธีจัดเก็บข้อมูล การสังเกตการเปลี่ยนแปลงข้อมูลในคอมโพเนนต์หลายรายการของแอปอาจสร้างเส้นทางการอ้างอิงที่ชัดเจนและเข้มงวดระหว่างคอมโพเนนต์ ซึ่งทำให้การทดสอบและการแก้ไขข้อบกพร่องเป็นเรื่องยาก

LiveData คลาสไลบรารีวงจร สำหรับการสังเกตข้อมูลจะช่วยแก้ปัญหานี้ได้ ใช้ค่าที่ส่งคืนประเภท LiveData ในคำอธิบายเมธอด แล้ว Room จะสร้างโค้ดที่จำเป็นทั้งหมดเพื่ออัปเดต LiveData เมื่อมีการอัปเดตฐานข้อมูล

ใน WordDao ให้เปลี่ยนลายเซ็นของเมธอด getAlphabetizedWords() เพื่อให้ List<Word> ที่ส่งคืนมี LiveData ครอบอยู่

   @Query("SELECT * from word_table ORDER BY word ASC")
   fun getAlphabetizedWords(): LiveData<List<Word>>

ในโค้ดแล็บนี้ คุณจะติดตามการเปลี่ยนแปลงข้อมูลผ่าน Observer ใน MainActivity ในภายหลัง

ฐานข้อมูล Room คืออะไร

  • Room เป็นเลเยอร์ฐานข้อมูลที่อยู่เหนือฐานข้อมูล SQLite
  • Room จะจัดการงานที่น่าเบื่อซึ่งคุณเคยจัดการด้วย SQLiteOpenHelper
  • Room ใช้ DAO เพื่อออกคำค้นหาไปยังฐานข้อมูล
  • โดยค่าเริ่มต้น Room จะไม่อนุญาตให้คุณออกคำค้นหาในเทรดหลักเพื่อหลีกเลี่ยงประสิทธิภาพ UI ที่ไม่ดี เมื่อการค้นหาใน Room แสดงผล LiveData ระบบจะเรียกใช้การค้นหาแบบอะซิงโครนัสโดยอัตโนมัติในเธรดเบื้องหลัง
  • Room มีการตรวจสอบคำสั่ง SQLite ในเวลาคอมไพล์

ใช้ฐานข้อมูล Room

คลาสฐานข้อมูล Room ต้องเป็นคลาส Abstract และขยาย RoomDatabase โดยปกติแล้ว คุณจะต้องมีฐานข้อมูล Room เพียงอินสแตนซ์เดียวสำหรับทั้งแอป

มาสร้างกันเลย

  1. สร้างไฟล์คลาส Kotlin ชื่อ WordRoomDatabase แล้วเพิ่มโค้ดนี้ลงในไฟล์
// 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
            }
        }
   }
}

มาดูโค้ดกัน

  • คลาสฐานข้อมูลสำหรับ Room ต้องเป็น abstract และขยาย RoomDatabase
  • คุณใส่คำอธิบายประกอบในคลาสให้เป็นฐานข้อมูล Room ด้วย @Database และใช้พารามิเตอร์คำอธิบายประกอบเพื่อประกาศเอนทิตีที่อยู่ในฐานข้อมูลและตั้งค่าหมายเลขเวอร์ชัน แต่ละเอนทิตีจะสอดคล้องกับตารางที่จะสร้างในฐานข้อมูล การย้ายข้อมูลฐานข้อมูลอยู่นอกขอบเขตของ Codelab นี้ เราจึงตั้งค่า exportSchema เป็นเท็จที่นี่เพื่อหลีกเลี่ยงคำเตือนในการสร้าง ในแอปจริง คุณควรพิจารณาตั้งค่าไดเรกทอรีเพื่อให้ Room ใช้ส่งออกสคีมา เพื่อให้คุณสามารถตรวจสอบสคีมาปัจจุบันในระบบควบคุมเวอร์ชันได้
  • ฐานข้อมูลจะแสดง DAO ผ่านเมธอด "getter" ที่เป็นนามธรรมสำหรับ @Dao แต่ละรายการ
  • เราได้กำหนดซิงเกิลตัน WordRoomDatabase, เพื่อป้องกันไม่ให้เปิดอินสแตนซ์ของฐานข้อมูลหลายรายการพร้อมกัน
  • getDatabase จะแสดงผล Singleton โดยจะสร้างฐานข้อมูลเมื่อมีการเข้าถึงเป็นครั้งแรก โดยใช้เครื่องมือสร้างฐานข้อมูลของ Room เพื่อสร้างออบเจ็กต์ RoomDatabase ในบริบทของแอปพลิเคชันจากคลาส WordRoomDatabase และตั้งชื่อว่า "word_database"

ที่เก็บคืออะไร

คลาสที่เก็บจะแยกการเข้าถึงแหล่งข้อมูลหลายแหล่ง ที่เก็บไม่ได้เป็นส่วนหนึ่งของไลบรารี Architecture Components แต่เป็นแนวทางปฏิบัติแนะนำสำหรับการแยกโค้ดและสถาปัตยกรรม คลาส Repository มี API ที่สะอาดสำหรับการเข้าถึงข้อมูลในส่วนอื่นๆ ของแอปพลิเคชัน

เหตุผลที่ควรใช้ที่เก็บ

ที่เก็บจะจัดการการค้นหาและช่วยให้คุณใช้แบ็กเอนด์หลายรายการได้ ในตัวอย่างที่พบบ่อยที่สุด Repository จะใช้ตรรกะในการตัดสินใจว่าจะดึงข้อมูลจากเครือข่ายหรือใช้ผลลัพธ์ที่แคชไว้ในฐานข้อมูลภายใน

การใช้ที่เก็บ

สร้างไฟล์คลาส Kotlin ชื่อ WordRepository แล้ววางโค้ดต่อไปนี้ลงไป

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

ประเด็นสำคัญ

  • ระบบจะส่ง DAO ไปยังตัวสร้างที่เก็บแทนที่จะส่งฐานข้อมูลทั้งหมด เนื่องจากแอปต้องการสิทธิ์เข้าถึง DAO เท่านั้น เพราะ DAO มีเมธอดอ่าน/เขียนทั้งหมดสำหรับฐานข้อมูล คุณไม่จำเป็นต้องเปิดเผยทั้งฐานข้อมูลไปยังที่เก็บ
  • รายการคำเป็นสมบัติสาธารณะ โดยจะเริ่มต้นด้วยการรับLiveDataรายการคำจาก Room ซึ่งเราทำได้เนื่องจากวิธีที่เรากำหนดเมธอด getAlphabetizedWords เพื่อส่งคืน LiveData ในขั้นตอน "คลาส LiveData" Room จะเรียกใช้การค้นหาทั้งหมดในเธรดแยก จากนั้น LiveData จะแจ้งให้ผู้สังเกตการณ์ทราบในเทรดหลักเมื่อข้อมูลมีการเปลี่ยนแปลง
  • ตัวแก้ไข suspend จะบอกคอมไพเลอร์ว่าต้องเรียกใช้จากโครูทีนหรือฟังก์ชันระงับอื่น

ViewModel คืออะไร

ViewModelมีบทบาทในการให้ข้อมูลแก่ UI และคงอยู่ได้แม้จะมีการเปลี่ยนแปลงการกำหนดค่า ViewModel ทำหน้าที่เป็นศูนย์กลางการสื่อสารระหว่างที่เก็บและ UI นอกจากนี้ คุณยังใช้ ViewModel เพื่อแชร์ข้อมูลระหว่าง Fragment ได้ด้วย ViewModel เป็นส่วนหนึ่งของไลบรารีวงจร

ดูคำแนะนำเบื้องต้นเกี่ยวกับหัวข้อนี้ได้ที่ ViewModel Overview หรือบล็อกโพสต์ ViewModels: ตัวอย่างง่ายๆ

เหตุผลที่ควรใช้ ViewModel

ViewModel จะเก็บข้อมูล UI ของแอปในลักษณะที่คำนึงถึงวงจรของแอปและยังคงอยู่ได้แม้จะมีการเปลี่ยนแปลงการกำหนดค่า การแยกข้อมูล UI ของแอปออกจากคลาส Activity และ Fragment ช่วยให้คุณปฏิบัติตามหลักการความรับผิดเดียวได้ดียิ่งขึ้น โดยกิจกรรมและ Fragment จะมีหน้าที่ดึงข้อมูลไปยังหน้าจอ ในขณะที่ ViewModel จะมีหน้าที่จัดเก็บและประมวลผลข้อมูลทั้งหมดที่จำเป็นสำหรับ UI

ใน ViewModel ให้ใช้ LiveData สำหรับข้อมูลที่เปลี่ยนแปลงได้ซึ่ง UI จะใช้หรือแสดง การใช้ LiveData มีประโยชน์หลายประการ ดังนี้

  • คุณสามารถตั้งค่า Observer ในข้อมูล (แทนที่จะสำรวจการเปลี่ยนแปลง) และอัปเดต
    UI เมื่อข้อมูลมีการเปลี่ยนแปลงจริงเท่านั้น
  • ที่เก็บและ UI จะแยกกันโดยสมบูรณ์ด้วย ViewModel
  • ไม่มีการเรียกฐานข้อมูลจาก ViewModel (การดำเนินการทั้งหมดนี้จะได้รับการจัดการในที่เก็บ) ซึ่งทำให้โค้ดทดสอบได้มากขึ้น

viewModelScope

ใน Kotlin โครูทีนทั้งหมดจะทำงานภายใน CoroutineScope ขอบเขตจะควบคุมอายุการใช้งานของโครูทีนผ่านงานของโครูทีน เมื่อยกเลิกงานของขอบเขต ระบบจะยกเลิกโครูทีนทั้งหมดที่เริ่มต้นในขอบเขตนั้น

ไลบรารี AndroidX lifecycle-viewmodel-ktx จะเพิ่ม viewModelScope เป็นฟังก์ชันส่วนขยายของคลาส ViewModel ซึ่งช่วยให้คุณทำงานกับขอบเขตได้

ดูข้อมูลเพิ่มเติมเกี่ยวกับการทำงานกับ Coroutines ใน ViewModel ได้ที่ขั้นตอนที่ 5 ของ Codelab การใช้ Kotlin Coroutines ในแอป Android หรือบล็อกโพสต์ Easy Coroutines ใน Android: viewModelScope

ใช้ ViewModel

สร้างไฟล์คลาส Kotlin สำหรับ WordViewModel แล้วเพิ่มโค้ดนี้ลงในไฟล์

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

เราได้ดำเนินการต่อไปนี้

  • สร้างคลาสชื่อ WordViewModel ที่รับ Application เป็นพารามิเตอร์และขยาย AndroidViewModel
  • เพิ่มตัวแปรสมาชิกส่วนตัวเพื่อเก็บการอ้างอิงไปยังที่เก็บ
  • เพิ่มLiveDataตัวแปรสมาชิกสาธารณะเพื่อแคชรายการคำ
  • สร้างบล็อก init ที่รับการอ้างอิงไปยัง WordDao จาก WordRoomDatabase
  • ในบล็อก init ได้สร้าง WordRepository ตาม WordRoomDatabase
  • ในบล็อก init ให้เริ่มต้น allWords LiveData โดยใช้ที่เก็บ
  • สร้างเมธอด Wrapper insert() ที่เรียกใช้เมธอด insert() ของที่เก็บ ด้วยวิธีนี้ การใช้งาน insert() จะได้รับการแคปซูลจาก UI เราไม่ต้องการให้การแทรกบล็อกเทรดหลัก จึงเปิดตัวโครูทีนใหม่และเรียกใช้การแทรกของที่เก็บ ซึ่งเป็นฟังก์ชันระงับ ดังที่ได้กล่าวไว้ ViewModel มีขอบเขตของโครูทีนตามวงจรของตัวเองที่เรียกว่า viewModelScope ซึ่งเราจะใช้ที่นี่

จากนั้นคุณจะต้องเพิ่มเลย์เอาต์ XML สำหรับรายการและไอเทม

Codelab นี้ถือว่าคุณคุ้นเคยกับการสร้างเลย์เอาต์ใน XML อยู่แล้ว เราจึงจะให้โค้ดแก่คุณ

สร้างธีมของแอปพลิเคชันโดยตั้งค่า AppTheme ระดับบนเป็น Theme.MaterialComponents.Light.DarkActionBar เพิ่มสไตล์สำหรับรายการใน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>

วิธีเพิ่มเลย์เอาต์ 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>

ใน layout/activity_main.xml ให้แทนที่ TextView ด้วย RecyclerView แล้วเพิ่มปุ่มการทำงานแบบลอย (FAB) ตอนนี้เลย์เอาต์ควรมีลักษณะดังนี้

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

ลักษณะของ FAB ควรตรงกับการดำเนินการที่ใช้ได้ ดังนั้นเราจึงต้องการแทนที่ไอคอนด้วยสัญลักษณ์ "+"

ก่อนอื่น เราต้องเพิ่มชิ้นงานเวกเตอร์ใหม่โดยทำดังนี้

  1. เลือกไฟล์ > ใหม่ > ชิ้นงานเวกเตอร์
  2. คลิกไอคอนหุ่นยนต์ Android ในช่องภาพคลิปอาร์ต:
  3. ค้นหา "เพิ่ม" แล้วเลือกชิ้นงาน "+" คลิกตกลง
  4. หลังจากนั้น ให้คลิกถัดไป
  5. ยืนยันเส้นทางไอคอนเป็น main > drawable แล้วคลิกเสร็จสิ้นเพื่อเพิ่มชิ้นงาน
  6. ยังอยู่ใน layout/activity_main.xml ให้อัปเดต FAB เพื่อรวม Drawable ใหม่
<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"/>

คุณจะแสดงข้อมูลในRecyclerView ซึ่งดูดีกว่าการใส่ข้อมูลในTextView Codelab นี้ถือว่าคุณทราบวิธีการทำงานของ RecyclerView, RecyclerView.LayoutManager, RecyclerView.ViewHolder และ RecyclerView.Adapter

โปรดทราบว่าตัวแปร words ในแคชของอแดปเตอร์จะแคชข้อมูล ในงานถัดไป คุณจะเพิ่มโค้ดที่อัปเดตข้อมูลโดยอัตโนมัติ

สร้างไฟล์คลาส Kotlin สำหรับ WordListAdapter ที่ขยาย RecyclerView.Adapter รหัสมีดังนี้

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
}

เพิ่ม RecyclerView ในวิธีการ onCreate() ของ MainActivity

ในonCreate()วิธีหลังsetContentView

   val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
   val adapter = WordListAdapter(this)
   recyclerView.adapter = adapter
   recyclerView.layoutManager = LinearLayoutManager(this)

เรียกใช้แอปเพื่อให้แน่ใจว่าทุกอย่างทำงานได้ ไม่มีรายการเนื่องจากคุณยังไม่ได้เชื่อมต่อข้อมูล

ไม่มีข้อมูลในฐานข้อมูล คุณจะเพิ่มข้อมูลได้ 2 วิธี ได้แก่ เพิ่มข้อมูลบางส่วนเมื่อเปิดฐานข้อมูล และเพิ่ม Activity สำหรับเพิ่มคำ

หากต้องการลบเนื้อหาทั้งหมดและป้อนข้อมูลในฐานข้อมูลใหม่ทุกครั้งที่เริ่มแอป ให้สร้าง RoomDatabase.Callback และลบล้าง onOpen() เนื่องจากคุณไม่สามารถดำเนินการกับฐานข้อมูล Room ในเทรด UI ได้ onOpen() จึงเปิดใช้โครูทีนในตัวจัดสรร IO

หากต้องการเปิดใช้โครูทีน เราต้องมี CoroutineScope อัปเดตเมธอด getDatabase ของคลาส WordRoomDatabase เพื่อรับขอบเขตของโครูทีนเป็นพารามิเตอร์ด้วย

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

อัปเดตตัวเริ่มต้นการดึงข้อมูลฐานข้อมูลในบล็อก init ของ WordViewModel เพื่อส่งขอบเขตด้วย

val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()

ใน WordRoomDatabase เราจะสร้างการติดตั้งใช้งาน RoomDatabase.Callback() ที่กำหนดเอง ซึ่งจะรับ CoroutineScope เป็นพารามิเตอร์ของตัวสร้างด้วย จากนั้นเราจะลบล้างวิธีการ onOpen เพื่อป้อนข้อมูลลงในฐานข้อมูล

นี่คือโค้ดสำหรับการสร้างการเรียกกลับภายในคลาส 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!
    }
}

สุดท้าย ให้เพิ่มการเรียกกลับไปยังลำดับการสร้างฐานข้อมูลก่อนที่จะเรียกใช้ .build() ใน Room.databaseBuilder():

.addCallback(WordDatabaseCallback(scope))

โค้ดสุดท้ายควรมีลักษณะดังนี้

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

เพิ่มทรัพยากรสตริงต่อไปนี้ใน 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>

เพิ่มทรัพยากรสีนี้ใน value/colors.xml

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

สร้างไฟล์ทรัพยากรมิติข้อมูลใหม่

  1. คลิกโมดูลแอปในหน้าต่างโปรเจ็กต์
  2. เลือก File > New > Android Resource File
  3. จากตัวกรองที่มี ให้เลือกมิติข้อมูล
  4. ตั้งชื่อไฟล์เป็น dimens

เพิ่มแหล่งข้อมูลมิติข้อมูลต่อไปนี้ใน values/dimens.xml

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

สร้างโปรเจ็กต์ Android ใหม่ที่ว่างเปล่า Activity โดยใช้เทมเพลต Empty Activity ดังนี้

  1. เลือก File > New > Activity > Empty Activity
  2. ป้อน NewWordActivity สำหรับชื่อกิจกรรม
  3. ตรวจสอบว่าได้เพิ่มกิจกรรมใหม่ลงในไฟล์ AndroidManifest.xml แล้ว
<activity android:name=".NewWordActivity"></activity>

อัปเดตไฟล์ activity_new_word.xml ในโฟลเดอร์เลย์เอาต์ด้วยโค้ดต่อไปนี้

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

อัปเดตโค้ดสำหรับกิจกรรม

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

ขั้นตอนสุดท้ายคือการเชื่อมต่อ UI กับฐานข้อมูลโดยการบันทึกคำใหม่ที่ผู้ใช้ป้อนและแสดงเนื้อหาปัจจุบันของฐานข้อมูลคำใน RecyclerView

หากต้องการแสดงเนื้อหาปัจจุบันของฐานข้อมูล ให้เพิ่ม Observer ที่สังเกต LiveData ใน ViewModel

เมื่อใดก็ตามที่ข้อมูลเปลี่ยนแปลง ระบบจะเรียกใช้onChanged() Callback ซึ่งจะเรียกใช้เมธอด setWords() ของอแดปเตอร์เพื่ออัปเดตข้อมูลที่แคชของอแดปเตอร์และรีเฟรชรายการที่แสดง

ใน MainActivity ให้สร้างตัวแปรสมาชิกสำหรับ ViewModel ดังนี้

private lateinit var wordViewModel: WordViewModel

ใช้ ViewModelProvider เพื่อเชื่อมโยง ViewModel กับ Activity

เมื่อActivityเริ่มทำงานครั้งแรก ViewModelProvidersจะสร้าง ViewModel เมื่อมีการทำลายกิจกรรม เช่น ผ่านการเปลี่ยนแปลงการกำหนดค่า ViewModel จะยังคงอยู่ เมื่อสร้างกิจกรรมขึ้นใหม่ ViewModelProviders ให้ส่งคืน ViewModel ที่มีอยู่ ดูข้อมูลเพิ่มเติมได้ที่ ViewModel

ใน onCreate() ด้านล่างบล็อกโค้ด RecyclerView ให้รับ ViewModel จาก ViewModelProvider ดังนี้

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

นอกจากนี้ ใน onCreate() ให้เพิ่ม Observer สำหรับพร็อพเพอร์ตี้ allWords LiveData จาก WordViewModel

onChanged() (ซึ่งเป็นวิธีเริ่มต้นสำหรับ Lambda ของเรา) จะเริ่มทำงานเมื่อข้อมูลที่สังเกตมีการเปลี่ยนแปลงและกิจกรรมอยู่ในเบื้องหน้า

wordViewModel.allWords.observe(this, Observer { words ->
            // Update the cached copy of the words in the adapter.
            words?.let { adapter.setWords(it) }
})

เราต้องการเปิด NewWordActivity เมื่อแตะ FAB และเมื่อกลับมาที่ MainActivity แล้ว ให้แทรกคำใหม่ลงในฐานข้อมูลหรือแสดง Toast เรามาเริ่มด้วยการกำหนดรหัสคำขอเพื่อบรรลุเป้าหมายนี้กัน

private val newWordActivityRequestCode = 1

ใน MainActivity ให้เพิ่มโค้ด onActivityResult() สำหรับ NewWordActivity

หากกิจกรรมแสดงผลด้วย RESULT_OK ให้แทรกคำที่แสดงผลลงในฐานข้อมูลโดยเรียกใช้เมธอด insert() ของ 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()
    }
}

ใน MainActivity,start NewWordActivity เมื่อผู้ใช้แตะ FAB ใน MainActivity onCreate ให้ค้นหา FAB แล้วเพิ่ม onClickListener ด้วยรหัสนี้

val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
  val intent = Intent(this@MainActivity, NewWordActivity::class.java)
  startActivityForResult(intent, newWordActivityRequestCode)
}

โค้ดที่เสร็จสมบูรณ์แล้วควรมีลักษณะดังนี้

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

ตอนนี้เรียกใช้แอปได้แล้ว เมื่อคุณเพิ่มคำลงในฐานข้อมูลใน NewWordActivity UI จะอัปเดตโดยอัตโนมัติ

ตอนนี้คุณมีแอปที่ใช้งานได้แล้ว มาสรุปสิ่งที่คุณสร้างกัน โครงสร้างแอปอีกครั้งมีดังนี้

คอมโพเนนต์ของแอปมีดังนี้

  • MainActivity: แสดงคำในรายการโดยใช้ RecyclerView และ WordListAdapter ใน MainActivity มี Observer ที่สังเกตคำว่า LiveData จากฐานข้อมูลและจะได้รับการแจ้งเตือนเมื่อมีการเปลี่ยนแปลง
  • NewWordActivity: เพิ่มคำใหม่ลงในรายการ
  • WordViewModel: มีวิธีการเข้าถึงชั้นข้อมูล และจะแสดงผล LiveData เพื่อให้ MainActivity ตั้งค่าความสัมพันธ์ของ Observer ได้*
  • LiveData<List<Word>>: ช่วยให้การอัปเดตอัตโนมัติในคอมโพเนนต์ UI เป็นไปได้ ใน MainActivity มี Observer ที่สังเกตคำว่า LiveData จากฐานข้อมูลและได้รับการแจ้งเตือนเมื่อมีการเปลี่ยนแปลง
  • Repository: จัดการแหล่งข้อมูลอย่างน้อย 1 รายการ Repository แสดงเมธอดเพื่อให้ ViewModel โต้ตอบกับผู้ให้บริการข้อมูลพื้นฐาน ในแอปนี้ แบ็กเอนด์ดังกล่าวคือฐานข้อมูล Room
  • Room: เป็น Wrapper รอบๆ และใช้ฐานข้อมูล SQLite Room ช่วยคุณทำงานหลายอย่างที่คุณเคยต้องทำด้วยตัวเอง
  • DAO: แมปการเรียกใช้เมธอดกับคําค้นหาฐานข้อมูล เพื่อให้เมื่อที่เก็บเรียกใช้เมธอด เช่น getAlphabetizedWords() Room จะเรียกใช้ SELECT * from word_table ORDER BY word ASC ได้
  • Word: คือคลาสเอนทิตีที่มีคำเดียว

* Views และ Activities (และ Fragments) จะโต้ตอบกับข้อมูลผ่าน ViewModel เท่านั้น ดังนั้น ไม่ว่าข้อมูลจะมาจากที่ใดก็ไม่สำคัญ

ขั้นตอนการทำงานของข้อมูลสำหรับการอัปเดต UI อัตโนมัติ (UI แบบรีแอกทีฟ)

การอัปเดตอัตโนมัติเป็นไปได้เนื่องจากเราใช้ LiveData ใน MainActivity มี Observer ที่สังเกตคำว่า LiveData จากฐานข้อมูลและได้รับการแจ้งเตือนเมื่อมีการเปลี่ยนแปลง เมื่อมีการเปลี่ยนแปลง ระบบจะเรียกใช้เมธอด onChange() ของ Observer และอัปเดต mWords ใน WordListAdapter

คุณจะเห็นข้อมูลนี้เนื่องจากเป็น LiveData และสิ่งที่สังเกตได้คือ LiveData<List<Word>> ที่พร็อพเพอร์ตี้ WordViewModel allWords แสดงผล

WordViewModel จะซ่อนทุกอย่างเกี่ยวกับแบ็กเอนด์จากเลเยอร์ UI โดยจะให้วิธีการเข้าถึงชั้นข้อมูล และส่งคืน LiveData เพื่อให้ MainActivity ตั้งค่าความสัมพันธ์ของ Observer ได้ Views และ Activities (และ Fragments) จะโต้ตอบกับข้อมูลผ่าน ViewModel เท่านั้น ดังนั้น ไม่ว่าข้อมูลจะมาจากที่ใดก็ไม่สำคัญ

ในกรณีนี้ ข้อมูลมาจาก Repository ViewModel ไม่จำเป็นต้องทราบว่าที่เก็บนั้นโต้ตอบกับอะไร เพียงแค่ต้องรู้วิธีโต้ตอบกับ Repository ซึ่งทำได้ผ่านเมธอดที่ Repository แสดง

ที่เก็บจะจัดการแหล่งข้อมูลอย่างน้อย 1 รายการ ในแอป WordListSample แบ็กเอนด์ดังกล่าวคือฐานข้อมูล Room Room เป็น Wrapper รอบฐานข้อมูล SQLite และใช้ฐานข้อมูล SQLite Room ช่วยคุณทำงานหลายอย่างที่คุณเคยต้องทำด้วยตัวเอง เช่น Room จะทำทุกอย่างที่คุณเคยทำกับSQLiteOpenHelper ชั้นเรียน

เมธอด DAO จะแมปการเรียกเมธอดกับการค้นหาฐานข้อมูล เพื่อให้เมื่อที่เก็บเรียกเมธอด เช่น getAllWords() Room จะเรียกใช้ SELECT * from word_table ORDER BY word ASC ได้

เนื่องจากผลลัพธ์ที่ได้จากการค้นหาคือ LiveData ทุกครั้งที่ข้อมูลใน Room เปลี่ยนแปลง ระบบจะเรียกใช้เมธอด onChanged() ของอินเทอร์เฟซ Observer และอัปเดต UI

[ไม่บังคับ] ดาวน์โหลดโค้ดโซลูชัน

หากยังไม่ได้ดู คุณสามารถดูโค้ดโซลูชันสำหรับ Codelab ได้ คุณดูที่เก็บ GitHub หรือดาวน์โหลดโค้ดได้ที่นี่

ดาวน์โหลดซอร์สโค้ด

คลายไฟล์ ZIP ที่ดาวน์โหลด การดำเนินการนี้จะแตกโฟลเดอร์รูท android-room-with-a-view-kotlin ซึ่งมีแอปที่สมบูรณ์