จุดประสงค์ของคอมโพเนนต์สถาปัตยกรรมคือการแนะนําสถาปัตยกรรมของแอป โดยมีไลบรารีสําหรับงานทั่วไป เช่น การจัดการอายุการใช้งานและความต่อเนื่องของข้อมูล คอมโพเนนต์สถาปัตยกรรมจะช่วยในการจัดโครงสร้างแอปในลักษณะที่มีประสิทธิภาพ ทดสอบได้ และบํารุงรักษาได้ด้วยโค้ด 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) แสดงถึงคลาสที่คุณจะสร้าง
- เปิด Android Studio แล้วคลิกเริ่มโครงการ Android Studio ใหม่
- เลือกกิจกรรมเปล่าในหน้าต่าง "สร้างโครงการใหม่" แล้วคลิกถัดไป
- ในหน้าจอถัดไป ให้ตั้งชื่อแอป RoomWordSample แล้วคลิกเสร็จสิ้น
ต่อไป คุณจะต้องเพิ่มไลบรารีคอมโพเนนต์ลงในไฟล์ Gradle
- ใน Android Studio ให้คลิกแท็บโปรเจ็กต์และขยายโฟลเดอร์ Gradle Scripts
เปิด build.gradle
(โมดูล: แอป)
- ใช้ปลั๊กอิน Kotlin สําหรับตัวประมวลผลหมายเหตุ
kapt
โดยเพิ่มหลังปลั๊กอินอื่นๆ ที่กําหนดที่ด้านบนของไฟล์build.gradle
(โมดูล: แอป)
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'
}
ข้อมูลของแอปนี้คือคํา และคุณจะต้องมีตารางที่เรียบง่ายเพื่อเก็บค่าเหล่านั้น
ห้องแชทช่วยให้คุณสร้างตารางผ่านเอนทิตีได้ มาเริ่มกันเลย
- สร้างไฟล์คลาส Kotlin ใหม่ชื่อ
Word
ซึ่งมีคลาสข้อมูลWord
คลาสนี้จะอธิบายเอนทิตี (ซึ่งแสดงถึงตาราง SQLite) สําหรับคําของคุณ แต่ละพร็อพเพอร์ตี้ในชั้นเรียนแสดงถึงคอลัมน์ในตาราง ซึ่งท้ายที่สุดแล้ว พร็อพเพอร์ตี้จะใช้พร็อพเพอร์ตี้เหล่านี้ในการสร้างตารางและสร้างอินสแตนซ์ออบเจ็กต์จากแถวในฐานข้อมูล
รหัสคือ
data class Word(val word: String)
หากต้องการให้ชั้นเรียน Word
มีความหมายสําหรับฐานข้อมูลห้องแชท คุณจะต้องใส่คําอธิบายประกอบ หมายเหตุระบุว่าแต่ละส่วนของชั้นเรียนนี้เกี่ยวข้องกับรายการในฐานข้อมูลอย่างไร ห้องแชทจะใช้ข้อมูลนี้เพื่อสร้างโค้ด
หากคุณพิมพ์คําอธิบายประกอบด้วยตนเอง (แทนการวาง) 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 ต้องเป็นอินเทอร์เฟซหรือระดับนามธรรม
โดยค่าเริ่มต้น การค้นหาทั้งหมดต้องดําเนินการในชุดข้อความแยกต่างหาก
ห้องมีการรองรับ Coroutine ทําให้คําถามของคุณมีคําอธิบายประกอบด้วยตัวปรับแต่ง suspend
แล้วเรียกใช้จาก Coroutine หรือจากฟังก์ชันการระงับอื่น
ใช้ 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 สําหรับห้องแชท 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 อินสแตนซ์สําหรับทั้งแอป
มาเริ่มกันเลย
- สร้างไฟล์คลาส 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' เพื่อสร้างออบเจ็กต์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
' คือการให้ข้อมูลกับ 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 ใหม่และเรียกใช้การเรียกที่เก็บ ' ซึ่งเป็นฟังก์ชันระงับ ดังที่กล่าวไว้ โมเดลการดูมีขอบเขต 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' ควรสอดคล้องกับการดําเนินการที่มีอยู่ เราจึงต้องแทนที่ไอคอนด้วยสัญลักษณ์ '+'
ก่อนอื่น เราจะต้องเพิ่มเนื้อหาเวกเตอร์เวกเตอร์ใหม่
- เลือก File > New > Vector Asset
- คลิกไอคอนหุ่นยนต์ Android ในช่อง Clip Art:
- ค้นหา "add" แล้วเลือกเนื้อหา '+' คลิกตกลง
- หลังจากนั้น ให้คลิกถัดไป
- ยืนยันเส้นทางไอคอนเป็น
main > drawable
แล้วคลิกเสร็จสิ้นเพื่อเพิ่มชิ้นงาน - เมื่ออยู่ใน
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>
วิธีสร้างไฟล์มิติข้อมูลมิติข้อมูลใหม่
- คลิกโมดูลแอปในหน้าต่างโปรเจ็กต์
- เลือก File > New > Android Resource File
- เลือกมิติข้อมูลจากตัวระบุที่มีอยู่
- ตั้งชื่อไฟล์: dimens
เพิ่มแหล่งข้อมูลมิติข้อมูลเหล่านี้ใน values/dimens.xml
<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>
สร้าง Activity
ใหม่ของ Android ที่ว่างเปล่าด้วยเทมเพลตกิจกรรมเปล่า
- เลือกไฟล์ > ใหม่ > กิจกรรม > กิจกรรมว่างเปล่า
- ป้อน
NewWordActivity
สําหรับชื่อกิจกรรม - ยืนยันว่าเพิ่มกิจกรรมใหม่ลงในไฟล์ 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
ซึ่งมีแอปที่สมบูรณ์