หลักพื้นฐานของ Android Kotlin 05.2: LiveData และตัวสังเกตการณ์ LiveData

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

บทนำ

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

หากต้องการใช้คลาส LiveData คุณจะต้องตั้งค่า "ผู้สังเกตการณ์" (เช่น กิจกรรมหรือ Fragment) ที่สังเกตการเปลี่ยนแปลงในข้อมูลของแอป LiveData ตระหนักถึงวงจร ดังนั้นจึงอัปเดตเฉพาะ Observer ของคอมโพเนนต์แอปที่อยู่ในสถานะวงจรที่ใช้งานอยู่

สิ่งที่คุณควรทราบอยู่แล้ว

  • วิธีสร้างแอป Android พื้นฐานใน Kotlin
  • วิธีไปยังส่วนต่างๆ ของแอป
  • วงจรของกิจกรรมและ Fragment
  • วิธีใช้ViewModelออบเจ็กต์ในแอป
  • วิธีสร้างViewModelออบเจ็กต์โดยใช้อินเทอร์เฟซของ ViewModelProvider.Factory

สิ่งที่คุณจะได้เรียนรู้

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

สิ่งที่คุณต้องดำเนินการ

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

ใน Codelab บทที่ 5 คุณจะได้พัฒนาแอป GuessTheWord โดยเริ่มจากโค้ดเริ่มต้น GuessTheWord เป็นเกมสไตล์ใบ้คำสำหรับผู้เล่น 2 คน โดยผู้เล่นจะต้องร่วมมือกันเพื่อให้ได้คะแนนสูงสุดเท่าที่จะเป็นไปได้

ผู้เล่นคนแรกดูคำในแอปและแสดงท่าทางของคำนั้นทีละคำ โดยต้องไม่ให้ผู้เล่นคนที่ 2 เห็นคำ ผู้เล่นคนที่ 2 พยายามทายคำ

หากต้องการเล่นเกม ผู้เล่นคนแรกจะเปิดแอปในอุปกรณ์และเห็นคำ เช่น "กีตาร์" ดังที่แสดงในภาพหน้าจอด้านล่าง

ผู้เล่นคนแรกจะแสดงท่าทางตามคำนั้น โดยระมัดระวังไม่ให้พูดคำนั้นออกมาจริงๆ

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

ในโค้ดแล็บนี้ คุณจะปรับปรุงแอป GuessTheWord โดยการเพิ่มเหตุการณ์เพื่อจบเกมเมื่อผู้ใช้ดูคำทั้งหมดในแอป นอกจากนี้ คุณยังเพิ่มปุ่มเล่นอีกครั้ง ใน Fragment คะแนนเพื่อให้ผู้ใช้เล่นเกมได้อีกครั้ง

หน้าจอชื่อ

หน้าจอเกม

หน้าจอคะแนน

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

  1. (ไม่บังคับ) หากไม่ได้ใช้โค้ดจาก Codelab ก่อนหน้า ให้ดาวน์โหลดโค้ดเริ่มต้นสำหรับ Codelab นี้ คลายซิปรหัสและเปิดโปรเจ็กต์ใน Android Studio
  2. เรียกใช้แอปและเล่นเกม
  3. โปรดทราบว่าปุ่มข้ามจะแสดงคำถัดไปและลดคะแนนลง 1 คะแนน ส่วนปุ่มเข้าใจแล้วจะแสดงคำถัดไปและเพิ่มคะแนนขึ้น 1 คะแนน ปุ่มจบเกมจะสิ้นสุดเกม

LiveData คือคลาสที่เก็บข้อมูลที่สังเกตได้ซึ่งรับรู้ถึงวงจร เช่น คุณสามารถใส่ LiveData รอบคะแนนปัจจุบันในแอป GuessTheWord ได้ ใน Codelab นี้ คุณจะได้เรียนรู้ลักษณะหลายอย่างของ LiveData ดังนี้

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

ในงานนี้ คุณจะได้เรียนรู้วิธีรวมข้อมูลประเภทใดก็ได้ไว้ในออบเจ็กต์ LiveData โดยการแปลงคะแนนปัจจุบันและข้อมูลคำปัจจุบันใน GameViewModel เป็น LiveData ในงานที่ทำในภายหลัง คุณจะเพิ่ม Observer ลงในออบเจ็กต์ LiveData เหล่านี้ และเรียนรู้วิธีสังเกต LiveData

ขั้นตอนที่ 1: เปลี่ยนคะแนนและคำเพื่อใช้ LiveData

  1. เปิดไฟล์ GameViewModel ในแพ็กเกจ screens/game
  2. เปลี่ยนประเภทของตัวแปร score และ word เป็น MutableLiveData

    MutableLiveData คือ LiveData ที่เปลี่ยนค่าได้ MutableLiveData เป็นคลาสทั่วไป ดังนั้นคุณจึงต้องระบุประเภทข้อมูลที่คลาสนี้จัดเก็บ
// The current word
val word = MutableLiveData<String>()
// The current score
val score = MutableLiveData<Int>()
  1. ใน GameViewModel ภายในบล็อก init ให้เริ่มต้น score และ word หากต้องการเปลี่ยนค่าของตัวแปร LiveData ให้ใช้วิธี setValue() กับตัวแปร ใน Kotlin คุณเรียกใช้ setValue() ได้โดยใช้พร็อพเพอร์ตี้ value
init {

   word.value = ""
   score.value = 0
  ...
}

ขั้นตอนที่ 2: อัปเดตการอ้างอิงออบเจ็กต์ LiveData

ตอนนี้ตัวแปร score และ word มีประเภทเป็น LiveData แล้ว ในขั้นตอนนี้ คุณจะเปลี่ยนการอ้างอิงตัวแปรเหล่านี้โดยใช้พร็อพเพอร์ตี้ value

  1. ใน GameViewModel ในวิธีการ onSkip() ให้เปลี่ยน score เป็น score.value สังเกตข้อผิดพลาดเกี่ยวกับ score ที่อาจเป็น null คุณจะแก้ไขข้อผิดพลาดนี้ในขั้นตอนถัดไป
  2. หากต้องการแก้ไขข้อผิดพลาด ให้เพิ่มnullเช็คscore.valueในonSkip() จากนั้นเรียกใช้ฟังก์ชัน minus() ใน score ซึ่งจะทำการลบด้วย null-safety
fun onSkip() {
   if (!wordList.isEmpty()) {
       score.value = (score.value)?.minus(1)
   }
   nextWord()
}
  1. อัปเดตเมธอด onCorrect() ในลักษณะเดียวกัน โดยเพิ่มการตรวจสอบ null ลงในตัวแปร score และใช้ฟังก์ชัน plus()
fun onCorrect() {
   if (!wordList.isEmpty()) {
       score.value = (score.value)?.plus(1)
   }
   nextWord()
}
  1. ใน GameViewModel ภายในเมธอด nextWord() ให้เปลี่ยนการอ้างอิง word เป็น word.value
private fun nextWord() {
   if (!wordList.isEmpty()) {
       //Select and remove a word from the list
       word.value = wordList.removeAt(0)
   }
}
  1. ใน GameFragment ภายในเมธอด updateWordText() ให้เปลี่ยนการอ้างอิงเป็น viewModel.word เป็น viewModel.word.value.
/** Methods for updating the UI **/
private fun updateWordText() {
   binding.wordText.text = viewModel.word.value
}
  1. ใน GameFragment ภายในเมธอด updateScoreText() ให้เปลี่ยนการอ้างอิงเป็น viewModel.score เป็น viewModel.score.value.
private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.value.toString()
}
  1. ใน GameFragment ภายในเมธอด gameFinished() ให้เปลี่ยนการอ้างอิงเป็น viewModel.score เป็น viewModel.score.value เพิ่มnull-การตรวจสอบความปลอดภัยที่จำเป็น
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score.value?:0
   NavHostFragment.findNavController(this).navigate(action)
}
  1. ตรวจสอบว่าโค้ดไม่มีข้อผิดพลาด คอมไพล์และเรียกใช้แอป ฟังก์ชันการทำงานของแอปควรเหมือนกับก่อนหน้า

งานนี้เกี่ยวข้องอย่างใกล้ชิดกับงานก่อนหน้า ซึ่งคุณได้แปลงคะแนนและข้อมูลคำเป็นออบเจ็กต์ LiveData ในงานนี้ คุณจะแนบออบเจ็กต์ Observer กับออบเจ็กต์ LiveData เหล่านั้น

  1. ใน GameFragment, ภายในเมธอด onCreateView() ให้แนบออบเจ็กต์ Observer กับออบเจ็กต์ LiveData สำหรับคะแนนปัจจุบัน viewModel.score ใช้วิธี observe() และวางโค้ดหลังการเริ่มต้นของ viewModel ใช้นิพจน์แลมบ์ดาเพื่อลดความซับซ้อนของโค้ด (นิพจน์แลมบ์ดาคือฟังก์ชันที่ไม่ระบุชื่อซึ่งไม่ได้ประกาศ แต่จะส่งเป็นนิพจน์ทันที)
viewModel.score.observe(this, Observer { newScore ->
})

แก้ไขการอ้างอิงถึง Observer โดยคลิก Observer กด Alt+Enter (Option+Enter ใน Mac) แล้วนำเข้า androidx.lifecycle.Observer

  1. Observer ที่คุณเพิ่งสร้างจะได้รับเหตุการณ์เมื่อข้อมูลที่ออบเจ็กต์ LiveData ที่สังเกตการณ์ถืออยู่มีการเปลี่ยนแปลง ในออบเซิร์ฟเวอร์ ให้อัปเดตคะแนน TextView ด้วยคะแนนใหม่
/** Setting up LiveData observation relationship **/
viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. แนบออบเจ็กต์ Observer กับออบเจ็กต์ LiveData ของคำปัจจุบัน โดยทำในลักษณะเดียวกับที่คุณแนบออบเจ็กต์ Observer กับคะแนนปัจจุบัน
/** Setting up LiveData observation relationship **/
viewModel.word.observe(this, Observer { newWord ->
   binding.wordText.text = newWord
})

เมื่อค่าของ score หรือ word เปลี่ยนไป score หรือ word ที่แสดงบนหน้าจอจะอัปเดตโดยอัตโนมัติ

  1. ใน GameFragment ให้ลบเมธอด updateWordText() และ updateScoreText() รวมถึงการอ้างอิงทั้งหมดที่เกี่ยวข้อง คุณไม่จำเป็นต้องใช้ตัวแปรเหล่านี้อีกต่อไป เนื่องจากLiveDataจะอัปเดตมุมมองข้อความ
  2. เรียกใช้แอป เกมควรทํางานได้เหมือนเดิมทุกประการ แต่ตอนนี้จะใช้ LiveData และ LiveData Observer

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

ในโค้ดปัจจุบัน คลาสภายนอกใดๆ ก็สามารถแก้ไขตัวแปร score และ word ได้โดยใช้พร็อพเพอร์ตี้ value เช่น ใช้ viewModel.score.value ซึ่งอาจไม่สำคัญในแอปที่คุณกำลังพัฒนาในโค้ดแล็บนี้ แต่ในแอปเวอร์ชันที่ใช้งานจริง คุณจะต้องควบคุมข้อมูลในออบเจ็กต์ ViewModel

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

MutableLiveData เทียบกับ LiveData:

  • คุณเปลี่ยนข้อมูลในออบเจ็กต์ MutableLiveData ได้ตามชื่อ ภายใน ViewModel ควรแก้ไขข้อมูลได้ จึงใช้ MutableLiveData
  • อ่านข้อมูลในออบเจ็กต์ LiveData ได้ แต่เปลี่ยนแปลงไม่ได้ จากภายนอก ViewModel ข้อมูลควรอ่านได้ แต่แก้ไขไม่ได้ ดังนั้นจึงควรแสดงข้อมูลเป็น LiveData

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

เพิ่มพร็อพเพอร์ตี้สำรองให้กับคะแนนและคำ

  1. ใน GameViewModel ให้ทำให้ออบเจ็กต์ score ปัจจุบันเป็น private
  2. หากต้องการทำตามรูปแบบการตั้งชื่อที่ใช้ในพร็อพเพอร์ตี้สำรอง ให้เปลี่ยน score เป็น _score ตอนนี้พร็อพเพอร์ตี้ _score เป็นเวอร์ชันที่แก้ไขได้ของคะแนนเกม ซึ่งใช้ภายใน
  3. สร้างเวอร์ชันสาธารณะของLiveDataประเภทที่ชื่อscore
// The current score
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
  1. คุณเห็นข้อผิดพลาดในการเริ่มต้น ข้อผิดพลาดนี้เกิดขึ้นเนื่องจากภายใน GameFragment score เป็นการอ้างอิง LiveData และ score ไม่สามารถเข้าถึง Setter ได้อีกต่อไป ดูข้อมูลเพิ่มเติมเกี่ยวกับ Getter และ Setter ใน Kotlin ได้ที่Getter และ Setter

    หากต้องการแก้ไขข้อผิดพลาด ให้แทนที่เมธอด get() สำหรับออบเจ็กต์ score ใน GameViewModel และส่งคืนพร็อพเพอร์ตี้สำรอง _score
val score: LiveData<Int>
   get() = _score
  1. ใน GameViewModel ให้เปลี่ยนการอ้างอิงของ score เป็นเวอร์ชันภายในที่แก้ไขได้ _score
init {
   ...
   _score.value = 0
   ...
}

...
fun onSkip() {
   if (!wordList.isEmpty()) {
       _score.value = (score.value)?.minus(1)
   }
  ...
}

fun onCorrect() {
   if (!wordList.isEmpty()) {
       _score.value = (score.value)?.plus(1)
   }
   ...
}
  1. เปลี่ยนชื่อออบเจ็กต์ word เป็น _word และเพิ่มพร็อพเพอร์ตี้สำรองสำหรับออบเจ็กต์ดังกล่าวเช่นเดียวกับที่ทำกับออบเจ็กต์ score
// The current word
private val _word = MutableLiveData<String>()
val word: LiveData<String>
   get() = _word
...
init {
   _word.value = ""
   ...
}
...
private fun nextWord() {
   if (!wordList.isEmpty()) {
       //Select and remove a word from the list
       _word.value = wordList.removeAt(0)
   }
}

เยี่ยมมาก คุณได้ห่อหุ้มLiveDataออบเจ็กต์wordและscoreแล้ว

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

หากต้องการใช้ฟังก์ชันนี้ คุณต้องมีเหตุการณ์ที่จะทริกเกอร์และสื่อสารไปยัง Fragment จาก ViewModel เมื่อแสดงคำทั้งหมดแล้ว โดยคุณจะใช้LiveDataรูปแบบ Observer เพื่อสร้างโมเดลเหตุการณ์เกมจบ

รูปแบบ Observer

รูปแบบ Observer คือรูปแบบการออกแบบซอฟต์แวร์ โดยจะระบุการสื่อสารระหว่างออบเจ็กต์ ได้แก่ Observable ("Subject" ของการสังเกต) และObserver Observable คือออบเจ็กต์ที่แจ้งให้ Observer ทราบเกี่ยวกับการเปลี่ยนแปลงสถานะของตัวเอง

ในกรณีของ LiveData ในแอปนี้ ออบเจ็กต์ที่สังเกตได้ (Subject) คือออบเจ็กต์ LiveData และ Observer คือเมธอดในตัวควบคุม UI เช่น Fragment การเปลี่ยนแปลงสถานะจะเกิดขึ้นเมื่อใดก็ตามที่ข้อมูลที่อยู่ใน LiveData มีการเปลี่ยนแปลง คลาส LiveData มีความสำคัญอย่างยิ่งในการสื่อสารจาก ViewModel ไปยัง Fragment

ขั้นตอนที่ 1: ใช้ LiveData เพื่อตรวจหาเหตุการณ์เกมจบ

ในงานนี้ คุณจะใช้LiveDataรูปแบบ Observer เพื่อสร้างเหตุการณ์เกมจบ

  1. ใน GameViewModel ให้สร้างออบเจ็กต์ Boolean MutableLiveData ที่ชื่อ _eventGameFinish ออบเจ็กต์นี้จะจัดเก็บเหตุการณ์ที่เกมจบแล้ว
  2. หลังจากเริ่มต้นออบเจ็กต์ _eventGameFinish แล้ว ให้สร้างและเริ่มต้นพร็อพเพอร์ตี้สำรองที่ชื่อ eventGameFinish
// Event which triggers the end of the game
private val _eventGameFinish = MutableLiveData<Boolean>()
val eventGameFinish: LiveData<Boolean>
   get() = _eventGameFinish
  1. ใน GameViewModel ให้เพิ่มonGameFinish()วิธีการ ในเมธอด ให้ตั้งค่าเหตุการณ์เกมจบ eventGameFinish เป็น true
/** Method for the game completed event **/
fun onGameFinish() {
   _eventGameFinish.value = true
}
  1. ใน GameViewModel ภายในเมธอด nextWord() ให้จบเกมหากรายการคำว่างเปล่า
private fun nextWord() {
   if (wordList.isEmpty()) {
       onGameFinish()
   } else {
       //Select and remove a _word from the list
       _word.value = wordList.removeAt(0)
   }
}
  1. ใน GameFragment ภายใน onCreateView() หลังจากเริ่มต้น viewModel ให้แนบ Observer ไปยัง eventGameFinish ใช้วิธี observe() ภายในฟังก์ชัน Lambda ให้เรียกใช้เมธอด gameFinished()
// Observer for the Game finished event
viewModel.eventGameFinish.observe(this, Observer<Boolean> { hasFinished ->
   if (hasFinished) gameFinished()
})
  1. เรียกใช้แอป เล่นเกม และดูคำทั้งหมด แอปจะนำทางไปยังหน้าจอคะแนนโดยอัตโนมัติ แทนที่จะอยู่ในส่วนเกมจนกว่าคุณจะแตะจบเกม

    หลังจากที่รายการคำว่างเปล่า ระบบจะตั้งค่า eventGameFinish เรียกใช้เมธอด Observer ที่เชื่อมโยงในส่วนเกม และแอปจะไปยังส่วนหน้าจอ
  2. โค้ดที่คุณเพิ่มทำให้เกิดปัญหาเกี่ยวกับวงจร หากต้องการทำความเข้าใจปัญหา ให้แสดงความคิดเห็นในโค้ดการนำทางในเมธอด gameFinished() ในคลาส GameFragment อย่าลืมเก็บข้อความ Toast ไว้ในเมธอด
private fun gameFinished() {
       Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
//        val action = GameFragmentDirections.actionGameToScore()
//        action.score = viewModel.score.value?:0
//        NavHostFragment.findNavController(this).navigate(action)
   }
  1. เรียกใช้แอป เล่นเกม และดูคำทั้งหมด ข้อความโทสต์ที่ระบุว่า "เกมเพิ่งจบ" จะปรากฏขึ้นชั่วครู่ที่ด้านล่างของหน้าจอเกม ซึ่งเป็นลักษณะการทำงานที่คาดไว้

ตอนนี้ให้หมุนอุปกรณ์หรือโปรแกรมจำลอง ข้อความป๊อปอัปจะแสดงอีกครั้ง หมุนอุปกรณ์อีก 2-3 ครั้ง แล้วคุณอาจเห็นข้อความป๊อปอัปทุกครั้ง นี่เป็นข้อบกพร่องเนื่องจากข้อความป๊อปอัปควรแสดงเพียงครั้งเดียวเมื่อเกมจบลง โดยไม่ควรแสดงข้อความป๊อปอัปทุกครั้งที่สร้าง Fragment ใหม่ คุณจะแก้ไขปัญหานี้ได้ในงานถัดไป

ขั้นตอนที่ 2: รีเซ็ตเหตุการณ์เกมจบ

โดยปกติแล้ว LiveData จะส่งการอัปเดตไปยังผู้สังเกตการณ์เมื่อข้อมูลมีการเปลี่ยนแปลงเท่านั้น ข้อยกเว้นของลักษณะการทำงานนี้คือผู้สังเกตการณ์จะได้รับการอัปเดตด้วยเมื่อเปลี่ยนจากสถานะไม่ได้ใช้งานเป็นสถานะใช้งาน

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

ในงานนี้ คุณจะแก้ไขปัญหานี้และแสดงข้อความ Toast เพียงครั้งเดียวโดยการรีเซ็ตค่าสถานะ eventGameFinish ใน GameViewModel

  1. ใน GameViewModel ให้เพิ่มเมธอด onGameFinishComplete() เพื่อรีเซ็ตเหตุการณ์เกมจบ _eventGameFinish
/** Method for the game completed event **/

fun onGameFinishComplete() {
   _eventGameFinish.value = false
}
  1. ใน GameFragment เมื่อสิ้นสุด gameFinished() ให้เรียกใช้ onGameFinishComplete() ในออบเจ็กต์ viewModel (ปล่อยให้โค้ดการนำทางใน gameFinished() เป็นความคิดเห็นไปก่อน)
private fun gameFinished() {
   ...
   viewModel.onGameFinishComplete()
}
  1. เรียกใช้แอปและเล่นเกม อ่านคำทั้งหมด แล้วเปลี่ยนการวางแนวหน้าจอของอุปกรณ์ โดยข้อความป๊อปอัปจะแสดงเพียงครั้งเดียว
  2. ใน GameFragment ภายในเมธอด gameFinished() ให้ยกเลิกการแสดงความคิดเห็นของโค้ดการนำทาง

    หากต้องการยกเลิกการแสดงความคิดเห็นใน Android Studio ให้เลือกบรรทัดที่มีการแสดงความคิดเห็น แล้วกด Control+/ (Command+/ ใน Mac)
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score.value?:0
   findNavController(this).navigate(action)
   viewModel.onGameFinishComplete()
}

หาก Android Studio แจ้ง ให้นำเข้า androidx.navigation.fragment.NavHostFragment.findNavController

  1. เรียกใช้แอปและเล่นเกม ตรวจสอบว่าแอปนำทางไปยังหน้าจอคะแนนสุดท้ายโดยอัตโนมัติหลังจากที่คุณผ่านคำทั้งหมดแล้ว

เก่งมาก! แอปของคุณใช้ LiveData เพื่อทริกเกอร์เหตุการณ์เกมจบเพื่อสื่อสารจาก GameViewModel ไปยังเกม Fragment ว่ารายการคำว่างเปล่า จากนั้นส่วนเกมจะไปยังส่วนคะแนน

ในงานนี้ คุณจะเปลี่ยนคะแนนเป็นออบเจ็กต์ LiveData ใน ScoreViewModel และแนบ Observer ไปกับออบเจ็กต์นั้น งานนี้คล้ายกับสิ่งที่คุณทำเมื่อเพิ่ม LiveData ลงใน GameViewModel

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

  1. ใน ScoreViewModel ให้เปลี่ยนประเภทตัวแปร score เป็น MutableLiveData เปลี่ยนชื่อตามรูปแบบเป็น _score แล้วเพิ่มพร็อพเพอร์ตี้สำรอง
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
   get() = _score
  1. ใน ScoreViewModel ภายในบล็อก init ให้เริ่มต้น _score คุณจะนำบันทึกออกหรือปล่อยไว้ในบล็อก init ก็ได้
init {
   _score.value = finalScore
}
  1. ใน ScoreFragment ภายใน onCreateView() หลังจากเริ่มต้น viewModel ให้แนบ Observer สำหรับออบเจ็กต์คะแนน LiveData ภายในนิพจน์ Lambda ให้ตั้งค่าคะแนนเป็นมุมมองข้อความคะแนน นำโค้ดที่กำหนดมุมมองข้อความด้วยค่าคะแนนโดยตรงออกจาก ViewModel

โค้ดที่จะเพิ่ม

// Add observer for score
viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})

โค้ดที่จะนำออก

binding.scoreText.text = viewModel.score.toString()

เมื่อ Android Studio แจ้ง ให้นำเข้า androidx.lifecycle.Observer

  1. เรียกใช้แอปและเล่นเกม แอปควรทำงานได้เหมือนเดิม แต่ตอนนี้จะใช้ LiveData และ Observer เพื่ออัปเดตคะแนน

ในงานนี้ คุณจะเพิ่มปุ่มเล่นอีกครั้งลงในหน้าจอคะแนนและใช้ Listener การคลิกโดยใช้เหตุการณ์ LiveData ปุ่มนี้จะทริกเกอร์เหตุการณ์เพื่อไปยังหน้าจอเกมจากหน้าจอคะแนน

โค้ดเริ่มต้นสำหรับแอปมีปุ่มเล่นอีกครั้ง แต่ปุ่มจะซ่อนอยู่

  1. ใน res/layout/score_fragment.xml สำหรับปุ่ม play_again_button ให้เปลี่ยนค่าแอตทริบิวต์ visibility เป็น visible
<Button
   android:id="@+id/play_again_button"
...
   android:visibility="visible"
 />
  1. ใน ScoreViewModel ให้เพิ่มออบเจ็กต์ LiveData เพื่อเก็บ Boolean ที่ชื่อ _eventPlayAgain ออบเจ็กต์นี้ใช้เพื่อบันทึกเหตุการณ์ LiveData เพื่อไปยังหน้าจอเกมจากหน้าจอบอกคะแนน
private val _eventPlayAgain = MutableLiveData<Boolean>()
val eventPlayAgain: LiveData<Boolean>
   get() = _eventPlayAgain
  1. ใน ScoreViewModel ให้กำหนดเมธอดเพื่อตั้งค่าและรีเซ็ตเหตุการณ์ _eventPlayAgain
fun onPlayAgain() {
   _eventPlayAgain.value = true
}
fun onPlayAgainComplete() {
   _eventPlayAgain.value = false
}
  1. ใน ScoreFragment ให้เพิ่มผู้สังเกตการณ์สำหรับ eventPlayAgain วางโค้ดไว้ที่ส่วนท้ายของ onCreateView() ก่อนคำสั่ง return ภายในนิพจน์ Lambda ให้กลับไปที่หน้าจอเกมแล้วรีเซ็ต eventPlayAgain
// Navigates back to game when button is pressed
viewModel.eventPlayAgain.observe(this, Observer { playAgain ->
   if (playAgain) {
      findNavController().navigate(ScoreFragmentDirections.actionRestart())
       viewModel.onPlayAgainComplete()
   }
})

นำเข้า androidx.navigation.fragment.findNavController เมื่อ Android Studio แจ้ง

  1. ใน ScoreFragment ภายใน onCreateView() ให้เพิ่มเครื่องมือฟังการคลิกไปยังปุ่ม PlayAgain แล้วเรียกใช้ viewModel.onPlayAgain()
binding.playAgainButton.setOnClickListener {  viewModel.onPlayAgain()  }
  1. เรียกใช้แอปและเล่นเกม เมื่อเกมจบลง หน้าจอบอกคะแนนจะแสดงคะแนนสุดท้ายและปุ่มเล่นอีกครั้ง แตะปุ่มPlayAgain แล้วแอปจะนำคุณไปยังหน้าจอเกมเพื่อให้เล่นเกมได้อีกครั้ง

เก่งมาก คุณเปลี่ยนสถาปัตยกรรมของแอปเพื่อใช้LiveDataใน ViewModel และแนบ Observer ไปยังออบเจ็กต์ LiveData LiveData จะแจ้งออบเจ็กต์ Observer เมื่อค่าที่ LiveData ถืออยู่มีการเปลี่ยนแปลง

โปรเจ็กต์ Android Studio: GuessTheWord

LiveData

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

วิธีเพิ่ม LiveData

  • เปลี่ยนประเภทตัวแปรข้อมูลใน ViewModel เป็น LiveData หรือ MutableLiveData

MutableLiveData คือออบเจ็กต์ LiveData ที่เปลี่ยนค่าได้ MutableLiveData เป็นคลาสทั่วไป ดังนั้นคุณจึงต้องระบุประเภทข้อมูลที่คลาสนี้จัดเก็บ

  • หากต้องการเปลี่ยนค่าของข้อมูลที่ LiveData เก็บไว้ ให้ใช้วิธี setValue() ในตัวแปร LiveData

หากต้องการแคปซูล LiveData

  • LiveData ภายใน ViewModel ควรแก้ไขได้ นอกViewModel LiveData ควรอ่านได้ ซึ่งทำได้โดยใช้พร็อพเพอร์ตี้สำรองของ Kotlin
  • พร็อพเพอร์ตี้สำรองของ Kotlin ช่วยให้คุณส่งคืนสิ่งอื่นจากตัวรับค่าที่ไม่ใช่ออบเจ็กต์ที่แน่นอนได้
  • หากต้องการห่อหุ้ม LiveData ให้ใช้ private MutableLiveData ภายใน ViewModel และส่งคืนพร็อพเพอร์ตี้การสำรองข้อมูล LiveData นอก ViewModel

LiveData ที่สังเกตได้

  • LiveData เป็นไปตามรูปแบบ Observer "Observable" คือLiveDataออบเจ็กต์ และ Observer คือเมธอดในตัวควบคุม UI เช่น Fragment เมื่อใดก็ตามที่ข้อมูลที่ห่อหุ้มอยู่ภายใน LiveData มีการเปลี่ยนแปลง ระบบจะแจ้งให้เมธอด Observer ในตัวควบคุม UI ทราบ
  • หากต้องการทำให้ LiveData สังเกตได้ ให้แนบออบเจ็กต์ Observer กับการอ้างอิง LiveData ใน Observer (เช่น กิจกรรมและ Fragment) โดยใช้วิธี observe()
  • LiveData รูปแบบ Observer นี้ใช้เพื่อสื่อสารจาก ViewModel ไปยังตัวควบคุม UI ได้

หลักสูตร Udacity:

เอกสารประกอบสำหรับนักพัฒนาแอป Android

อื่นๆ:

ส่วนนี้แสดงรายการการบ้านที่เป็นไปได้สำหรับนักเรียน/นักศึกษาที่กำลังทำ Codelab นี้เป็นส่วนหนึ่งของหลักสูตรที่สอนโดยผู้สอน ผู้สอนมีหน้าที่ดำเนินการต่อไปนี้

  • มอบหมายการบ้านหากจำเป็น
  • สื่อสารกับนักเรียนเกี่ยวกับวิธีส่งงานที่ได้รับมอบหมาย
  • ให้คะแนนงานการบ้าน

ผู้สอนสามารถใช้คำแนะนำเหล่านี้ได้มากน้อยตามที่ต้องการ และควรมีอิสระในการมอบหมายการบ้านอื่นๆ ที่เห็นว่าเหมาะสม

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

ตอบคำถามต่อไปนี้

คำถามที่ 1

คุณจะห่อหุ้ม LiveData ที่จัดเก็บไว้ใน ViewModel อย่างไรเพื่อให้ออบเจ็กต์ภายนอกอ่านข้อมูลได้โดยไม่ต้องอัปเดต

  • ในออบเจ็กต์ ViewModel ให้เปลี่ยนประเภทข้อมูลของข้อมูลเป็น private LiveData ใช้พร็อพเพอร์ตี้สำรองเพื่อแสดงข้อมูลแบบอ่านอย่างเดียวของประเภท MutableLiveData
  • ในออบเจ็กต์ ViewModel ให้เปลี่ยนประเภทข้อมูลของข้อมูลเป็น private MutableLiveData ใช้พร็อพเพอร์ตี้สำรองเพื่อแสดงข้อมูลแบบอ่านอย่างเดียวของประเภท LiveData
  • ในตัวควบคุม UI ให้เปลี่ยนประเภทข้อมูลของข้อมูลเป็น private MutableLiveData ใช้พร็อพเพอร์ตี้สำรองเพื่อแสดงข้อมูลแบบอ่านอย่างเดียวของประเภท LiveData
  • ในออบเจ็กต์ ViewModel ให้เปลี่ยนประเภทข้อมูลของข้อมูลเป็น LiveData ใช้พร็อพเพอร์ตี้สำรองเพื่อแสดงข้อมูลแบบอ่านอย่างเดียวของประเภท LiveData

คำถามที่ 2

LiveData จะอัปเดตตัวควบคุม UI (เช่น Fragment) หากตัวควบคุม UI อยู่ในสถานะใดต่อไปนี้

  • ทำงานต่อ
  • ในเบื้องหลัง
  • หยุดชั่วคราว
  • หยุดทำงานแล้ว

คำถามที่ 3

ในLiveDataรูปแบบ Observer รายการที่สังเกตได้ (สิ่งที่สังเกต) คืออะไร

  • วิธีการสังเกตการณ์
  • ข้อมูลในออบเจ็กต์ LiveData
  • ตัวควบคุม UI
  • ออบเจ็กต์ ViewModel

เริ่มบทเรียนถัดไป: 5.3: การเชื่อมโยงข้อมูลกับ ViewModel และ LiveData

ดูลิงก์ไปยัง Codelab อื่นๆ ในหลักสูตรนี้ได้ที่หน้า Landing Page ของ Codelab หลักพื้นฐานของ Android Kotlin