ห้อง Android ที่มีมุมมอง - Kotlin

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

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

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

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

คุณจะต้องทําความคุ้นเคยกับ Kotlin แนวคิดการออกแบบที่เน้นวัตถุ และพื้นฐานการพัฒนาของ Android โดยเฉพาะ

  • RecyclerView และอะแดปเตอร์
  • ฐานข้อมูล SQLite และภาษาในการค้นหา SQLite
  • โครูทีนพื้นฐาน (หากไม่คุ้นเคยกับโครูทีน) ให้ทําตามการใช้ Kotlin Coroutines ในแอป Android)

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

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

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

สิ่งที่คุณจะทํา

ใน Codelab นี้ คุณจะได้ดูวิธีออกแบบและสร้างแอปโดยใช้ห้องแชทสําหรับคอมโพเนนต์สถาปัตยกรรมสถาปัตยกรรม โมเดลมุมมอง และ LiveData และสร้างแอปที่ทําสิ่งต่อไปนี้

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

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

สิ่งที่ต้องมี

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

Codelab นี้ให้โค้ดทั้งหมดที่จําเป็นในการสร้างแอปที่สมบูรณ์

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

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

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

เอนทิตี: ชั้นเรียนที่มีคําอธิบายประกอบซึ่งอธิบายตารางฐานข้อมูลเมื่อทํางานกับห้องแชท

ฐานข้อมูล SQL: ในพื้นที่เก็บข้อมูลของอุปกรณ์ ไลบรารีแบบถาวรของห้องจะสร้างและรักษาฐานข้อมูลนี้ไว้ให้

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

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

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

ViewModel: ทําหน้าที่เป็นศูนย์การสื่อสารระหว่างที่เก็บ (ข้อมูล) กับ UI ทั้งนี้ UI ไม่จําเป็นต้องเป็นกังวลเรื่องที่มาของข้อมูลอีกต่อไป อินสแตนซ์ ViewModel รอดชีวิตจากกิจกรรม/การแบ่งเป็นส่วนๆ ได้

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

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

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

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

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

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

เปิด build.gradle (โมดูล: แอป)

  1. ใช้ปลั๊กอิน Kotlin สําหรับตัวประมวลผลหมายเหตุkapt โดยเพิ่มหลังปลั๊กอินอื่นๆ ที่กําหนดที่ด้านบนของไฟล์ build.gradle (โมดูล: แอป)
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'
}

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

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

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

รหัสคือ

data class Word(val word: String)

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

หากคุณพิมพ์คําอธิบายประกอบด้วยตนเอง (แทนการวาง) 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 ต้องเป็นอินเทอร์เฟซหรือระดับนามธรรม

โดยค่าเริ่มต้น การค้นหาทั้งหมดต้องดําเนินการในชุดข้อความแยกต่างหาก

ห้องมีการรองรับ Coroutine ทําให้คําถามของคุณมีคําอธิบายประกอบด้วยตัวปรับแต่ง suspend แล้วเรียกใช้จาก Coroutine หรือจากฟังก์ชันการระงับอื่น

ใช้ 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 สําหรับห้องแชท
  • suspend fun insert(word: Word) : ประกาศฟังก์ชันการระงับเพื่อแทรกคํา 1 คํา
  • คําอธิบายประกอบ @Insert คือคําอธิบายประกอบเมธอด DAO พิเศษ ซึ่งคุณไม่จําเป็นต้องระบุ SQL เลย (นอกจากนี้ยังมีคําอธิบายประกอบ @Delete และ @Update สําหรับลบและอัปเดตแถว แต่คุณไม่ได้ใช้ในแอปนี้)
  • onConflict = OnConflictStrategy.IGNORE: กลยุทธ์ onความขัดแย้ง ที่เลือกจะละเว้นคําใหม่ หากเหมือนกับคําทั้งหมดในรายการอยู่แล้ว หากต้องการทราบข้อมูลเพิ่มเติมเกี่ยวกับกลยุทธ์ความขัดแย้งที่มีอยู่ โปรดดูเอกสาร
  • suspend fun deleteAll(): ประกาศฟังก์ชันการระงับเพื่อลบคําทั้งหมด
  • ไม่มีคําอธิบายประกอบความสะดวกสําหรับการลบเอนทิตีหลายรายการ จึงมีหมายเหตุ @Query ทั่วไป
  • @Query("DELETE FROM word_table"): @Query กําหนดให้คุณต้องระบุการค้นหา SQL เป็นพารามิเตอร์สตริงไปยังคําอธิบายประกอบ จึงจะทําให้มีคําค้นหาการอ่านที่ซับซ้อนและการดําเนินการอื่นๆ
  • fun getAlphabetizedWords(): List<Word>: วิธีรับคําทั้งหมดและส่งคืน List จาก Words
  • @Query("SELECT * from word_table ORDER BY word ASC"): การค้นหาที่แสดงรายการคําที่จัดเรียงจากน้อยไปมาก

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

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

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

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

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

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

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

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

ใช้ฐานข้อมูลห้องแชท

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

มาเริ่มกันเลย

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

มาดูโค้ดกัน

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

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

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

ทําไมต้องใช้ที่เก็บ

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

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

สร้างไฟล์คลาส 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 จากห้องแชท เราทําเช่นนี้ได้เพราะเรากําหนดวิธี getAlphabetizedWords เพื่อแสดงผล LiveData ใน &&tt;คลาส LiveData" ห้องแชทจะดําเนินการกับคําค้นหาทั้งหมดในชุดข้อความแยกต่างหาก จากนั้น LiveData จะสังเกตผู้สังเกตบนชุดข้อความหลักเมื่อข้อมูลมีการเปลี่ยนแปลง
  • คีย์ตัวปรับแต่ง suspend จะบอกคอมไพเลอร์ว่าจะต้องเรียก Coroutine หรือฟังก์ชันการระงับอื่นๆ

Viewmodel คืออะไร

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

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

เหตุใดจึงควรใช้ Viewmodel

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

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

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

viewmodelScope

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

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

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

นํา ViewView ไปใช้

สร้างไฟล์คลาส 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)
    }
}

ที่นี่'ve:

  • สร้างคลาสชื่อ WordViewModel ซึ่งมี Application เป็นพารามิเตอร์และขยาย AndroidViewModel
  • เพิ่มตัวแปรสมาชิกส่วนตัวเพื่อเก็บข้อมูลอ้างอิงไปยังที่เก็บ
  • เพิ่มตัวแปรสมาชิก LiveData สาธารณะเพื่อแคชรายการคํา
  • สร้างบล็อก init ที่ได้รับการอ้างอิง WordDao จาก WordRoomDatabase
  • ในบล็อก init สร้าง WordRepository ตาม WordRoomDatabase
  • ในบล็อก init ให้เริ่มต้น allWords LiveData โดยใช้ที่เก็บ
  • สร้างเมธอด Wrapper ของ insert() ที่เรียกใช้เมธอด insert() ของที่เก็บ วิธีนี้เป็นการช่วยครอบคลุมการใช้งาน insert() จาก UI เราไม่ต้องการให้แทรกบล็อกเทรดหลัก จึงเปิดตัว Coroutine ใหม่และเรียกใช้การเรียกที่เก็บ &#39 ซึ่งเป็นฟังก์ชันระงับ ดังที่กล่าวไว้ โมเดลการดูมีขอบเขต Coroutine โดยพิจารณาจากวงจรชีวิตชื่อ 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&#39 ควรสอดคล้องกับการดําเนินการที่มีอยู่ เราจึงต้องแทนที่ไอคอนด้วยสัญลักษณ์ '+'

ก่อนอื่น เราจะต้องเพิ่มเนื้อหาเวกเตอร์เวกเตอร์ใหม่

  1. เลือก File > New > Vector Asset
  2. คลิกไอคอนหุ่นยนต์ Android ในช่อง Clip Art:
  3. ค้นหา "add" แล้วเลือกเนื้อหา '+' คลิกตกลง
  4. หลังจากนั้น ให้คลิกถัดไป
  5. ยืนยันเส้นทางไอคอนเป็น main > drawable แล้วคลิกเสร็จสิ้นเพื่อเพิ่มชิ้นงาน
  6. เมื่ออยู่ใน layout/activity_main.xml ให้อัปเดต FAB ให้รวมภาพวาดใหม่:
<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() เนื่องจากไม่สามารถดําเนินการฐานข้อมูลห้องแชทในชุดข้อความ UI ได้ onOpen() จึงจะเปิด Coroutine ในอุปกรณ์ IO Dispatcher

คุณต้องใช้ CoroutineScope จึงจะเปิดตัว Coroutine ได้ อัปเดตเมธอด getDatabase ของคลาส WordRoomDatabase เพื่อรับขอบเขต Coroutine เป็นพารามิเตอร์ ดังนี้

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>

สร้าง Activity ใหม่ของ Android ที่ว่างเปล่าด้วยเทมเพลตกิจกรรมเปล่า

  1. เลือกไฟล์ > ใหม่ > กิจกรรม &gt กิจกรรมว่างเปล่า
  2. ป้อน NewWordActivity สําหรับชื่อกิจกรรม
  3. ยืนยันว่าเพิ่มกิจกรรมใหม่ลงในไฟล์ Manifest ของ Android แล้ว
<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

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

เมื่อใดก็ตามที่ข้อมูลมีการเปลี่ยนแปลง ระบบจะเรียกใช้โค้ดเรียกกลับ onChanged() ของเมธอด 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() ให้เพิ่มผู้สังเกตการณ์สําหรับพร็อพเพอร์ตี้ 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, เริ่ม 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 สร้างความสัมพันธ์ของผู้สังเกตได้*
  • LiveData<List<Word>>: ทําการอัปเดตอัตโนมัติในคอมโพเนนต์ UI ได้ ใน MainActivity มี Observer ที่สังเกตคําว่า LiveData จากฐานข้อมูล และจะได้รับการแจ้งเตือนเมื่อมีการเปลี่ยนแปลง
  • Repository: จัดการแหล่งข้อมูลอย่างน้อย 1 รายการ Repository จะแสดงเมธอดสําหรับ Viewmodel ในการโต้ตอบกับผู้ให้บริการข้อมูลที่สําคัญ แบ็กเอนด์นี้จะใช้แอปนี้เป็นฐานข้อมูลห้องแชท
  • Room: เป็น Wrapper และติดตั้งฐานข้อมูล SQLite ห้องทํางานหนักมากอย่างที่คุณต้องทําด้วยตัวเอง
  • DAO: แมปเมธอดการเรียกใช้คําค้นหาในฐานข้อมูล เพื่อให้เมื่อที่เก็บเรียกเมธอด เช่น getAlphabetizedWords() ห้องแชทจะเรียกใช้ SELECT * from word_table ORDER BY word ASC ได้
  • Word: คือคลาสเอนทิตีที่มีคําเดียว

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

โฟลว์ข้อมูลสําหรับการอัปเดต UI อัตโนมัติ (UI เชิงรับ)

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

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

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

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

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

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

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

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

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

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

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