จุดประสงค์ของ Architecture Components คือการให้คำแนะนำเกี่ยวกับสถาปัตยกรรมของแอป โดยมีไลบรารีสำหรับงานทั่วไป เช่น การจัดการวงจร และการคงอยู่ของข้อมูล คอมโพเนนต์สถาปัตยกรรมช่วยให้คุณจัดโครงสร้างแอปในลักษณะที่แข็งแกร่ง ทดสอบได้ และบำรุงรักษาได้โดยมีโค้ดบอยเลอร์เพลตน้อยลง ไลบรารีคอมโพเนนต์สถาปัตยกรรมเป็นส่วนหนึ่งของ Android Jetpack
นี่คือ Codelab เวอร์ชัน Kotlin ดูเวอร์ชันในภาษาโปรแกรม Java ได้ที่นี่
หากพบปัญหา (ข้อบกพร่องของโค้ด ข้อผิดพลาดทางไวยากรณ์ คำที่ไม่ชัดเจน ฯลฯ) ขณะทำตาม Codelab นี้ โปรดรายงานปัญหาผ่านลิงก์รายงานข้อผิดพลาดที่มุมซ้ายล่างของ Codelab
ข้อกำหนดเบื้องต้น
คุณต้องคุ้นเคยกับ Kotlin, แนวคิดการออกแบบเชิงออบเจ็กต์ และพื้นฐานการพัฒนาแอป Android โดยเฉพาะอย่างยิ่ง
RecyclerView
และอะแดปเตอร์- ฐานข้อมูล SQLite และภาษาการค้นหา SQLite
- โคโรทีนพื้นฐาน (หากไม่คุ้นเคยกับโคโรทีน โปรดดูการใช้โคโรทีน 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) แสดงถึงคลาสที่คุณจะสร้าง
- เปิด Android Studio แล้วคลิกเริ่มโปรเจ็กต์ Android Studio ใหม่
- ในหน้าต่างสร้างโปรเจ็กต์ใหม่ ให้เลือกกิจกรรมว่าง แล้วคลิกถัดไป
- ในหน้าจอถัดไป ให้ตั้งชื่อแอปว่า RoomWordSample แล้วคลิกเสร็จสิ้น
จากนั้นคุณจะต้องเพิ่มไลบรารีคอมโพเนนต์ลงในไฟล์ Gradle
- ใน Android Studio ให้คลิกแท็บโปรเจ็กต์ แล้วขยายโฟลเดอร์สคริปต์ Gradle
เปิด build.gradle
(Module: app)
- ใช้ปลั๊กอิน Kotlin
kapt
Annotation Processor โดยเพิ่มปลั๊กอินหลังจากปลั๊กอินอื่นๆ ที่กำหนดไว้ที่ด้านบนของไฟล์build.gradle
(Module: app)
apply plugin: 'kotlin-kapt'
- เพิ่มบล็อก
packagingOptions
ภายในบล็อกandroid
เพื่อยกเว้นโมดูลฟังก์ชันย่อยออกจากแพ็กเกจและป้องกันไม่ให้มีคำเตือน
android {
// other configuration (buildTypes, defaultConfig, etc.)
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
}
- เพิ่มโค้ดต่อไปนี้ที่ส่วนท้ายของบล็อก
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"
- ในไฟล์
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 ได้ มาเริ่มกันเลย
- สร้างไฟล์คลาส Kotlin ใหม่ชื่อ
Word
ซึ่งมีWord
data class
คลาสนี้จะอธิบายเอนทิตี (ซึ่งแสดงตาราง SQLite) สำหรับคำของคุณ พร็อพเพอร์ตี้แต่ละรายการในคลาสจะแสดงคอลัมน์ในตาราง ท้ายที่สุดแล้ว Room จะใช้พร็อพเพอร์ตี้เหล่านี้ทั้งในการสร้างตารางและสร้างออบเจ็กต์จากแถวในฐานข้อมูล
รหัสมีดังนี้
data class Word(val word: String)
หากต้องการให้คลาส Word
มีความหมายต่อฐานข้อมูล Room คุณต้องใส่คำอธิบายประกอบ คำอธิบายประกอบจะระบุว่าแต่ละส่วนของคลาสนี้เกี่ยวข้องกับรายการในฐานข้อมูลอย่างไร Room ใช้ข้อมูลนี้เพื่อสร้างโค้ด
หากคุณพิมพ์คำอธิบายประกอบด้วยตนเอง (แทนการวาง) Android Studio จะนำเข้าคลาสคำอธิบายประกอบโดยอัตโนมัติ
- อัปเดต
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 ที่ให้การค้นหาสำหรับสิ่งต่อไปนี้กัน
- การเรียงคำทั้งหมดตามลำดับตัวอักษร
- การแทรกคำ
- การลบคำทั้งหมด
- สร้างไฟล์คลาส Kotlin ใหม่ชื่อ
WordDao
- คัดลอกและวางโค้ดต่อไปนี้ลงใน
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 คำ@Insert
Annotation เป็น 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 เพียงอินสแตนซ์เดียวสำหรับทั้งแอป
มาสร้างกันเลย
- สร้างไฟล์คลาส 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 ควรตรงกับการดำเนินการที่ใช้ได้ ดังนั้นเราจึงต้องการแทนที่ไอคอนด้วยสัญลักษณ์ "+"
ก่อนอื่น เราต้องเพิ่มชิ้นงานเวกเตอร์ใหม่โดยทำดังนี้
- เลือกไฟล์ > ใหม่ > ชิ้นงานเวกเตอร์
- คลิกไอคอนหุ่นยนต์ Android ในช่องภาพคลิปอาร์ต:
- ค้นหา "เพิ่ม" แล้วเลือกชิ้นงาน "+" คลิกตกลง
- หลังจากนั้น ให้คลิกถัดไป
- ยืนยันเส้นทางไอคอนเป็น
main > drawable
แล้วคลิกเสร็จสิ้นเพื่อเพิ่มชิ้นงาน - ยังอยู่ใน
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>
สร้างไฟล์ทรัพยากรมิติข้อมูลใหม่
- คลิกโมดูลแอปในหน้าต่างโปรเจ็กต์
- เลือก File > New > Android Resource File
- จากตัวกรองที่มี ให้เลือกมิติข้อมูล
- ตั้งชื่อไฟล์เป็น dimens
เพิ่มแหล่งข้อมูลมิติข้อมูลต่อไปนี้ใน values/dimens.xml
<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>
สร้างโปรเจ็กต์ Android ใหม่ที่ว่างเปล่า Activity
โดยใช้เทมเพลต Empty Activity ดังนี้
- เลือก File > New > Activity > Empty Activity
- ป้อน
NewWordActivity
สำหรับชื่อกิจกรรม - ตรวจสอบว่าได้เพิ่มกิจกรรมใหม่ลงในไฟล์ 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 โต้ตอบกับผู้ให้บริการข้อมูลพื้นฐาน ในแอปนี้ แบ็กเอนด์ดังกล่าวคือฐานข้อมูล RoomRoom
: เป็น 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
ซึ่งมีแอปที่สมบูรณ์