Android Kotlin Fundamentals 05.2: การสังเกตการณ์เกี่ยวกับ LiveData และ LiveData

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

ข้อมูลเบื้องต้น

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

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

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

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

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

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

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

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

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

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

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

ผู้เล่นคนแรกจะทําตามคําและระวังอย่าพูดคํานั้นจริงๆ

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

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

หน้าจอชื่อ

หน้าจอเกม

หน้าจอคะแนน

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

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

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

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

ในบทเรียนนี้ คุณจะได้เรียนรู้วิธีรวมข้อมูลทุกประเภทไว้ในออบเจ็กต์ LiveData โดยการแปลงคะแนนปัจจุบันและข้อมูลคําปัจจุบันใน GameViewModel เป็น LiveData ในงานถัดไป คุณต้องเพิ่มผู้สังเกตการณ์ลงในออบเจ็กต์ 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 ให้เปลี่ยนเมธอด score เป็น score.value ในเมธอด onSkip() สังเกตข้อผิดพลาดเกี่ยวกับ score ที่อาจเป็น null คุณจะแก้ไขข้อผิดพลาดถัดไปได้
  2. หากต้องการแก้ไขข้อผิดพลาด ให้เพิ่มการตรวจสอบ null ลงใน score.value ใน onSkip() จากนั้นเรียกใช้ฟังก์ชัน minus() ใน score ซึ่งจะดําเนินการลบด้วยความปลอดภัยของ null
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 ให้เปลี่ยนการอ้างอิง word เป็นเมธอด word word เป็น word.value
private fun nextWord() {
   if (!wordList.isEmpty()) {
       //Select and remove a word from the list
       word.value = wordList.removeAt(0)
   }
}
  1. ใน GameFragment ให้เปลี่ยนการอ้างอิงเป็น viewModel ภายในเมธอด updateWordText() เป็น 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 ให้เปลี่ยนการอ้างอิงเป็น viewModel ภายในเมธอด gameFinished() เป็น 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. ผู้สังเกตที่คุณเพิ่งสร้างจะได้รับเหตุการณ์เมื่อข้อมูลที่ออบเจ็กต์ 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

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

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

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

MutableLiveData เทียบกับ LiveData:

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

หากต้องการสร้างกลยุทธ์นี้ ให้ใช้พร็อพเพอร์ตี้การสํารองข้อมูลของ Kotlin พร็อพเพอร์ตี้สํารองช่วยให้คุณส่งคืนบางอย่างจาก Getter ที่ไม่ใช่ออบเจ็กต์ที่ตรงกันทุกประการได้ ในงานนี้ คุณจะได้ใช้พร็อพเพอร์ตี้การสํารองข้อมูลสําหรับออบเจ็กต์ 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 ไม่สามารถเข้าถึงตัวตั้งค่าได้อีกต่อไป หากต้องการดูข้อมูลเพิ่มเติมเกี่ยวกับ Getter และ Setter ใน Kotlin โปรดดู Getters และ Setters

    หากต้องการแก้ไขข้อผิดพลาด ให้ลบล้างเมธอด 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แล้ว

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

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

รูปแบบผู้สังเกตการณ์

รูปแบบเซิร์ฟเวอร์โฆษณาคือรูปแบบการออกแบบซอฟต์แวร์ ซึ่งจะระบุการสื่อสารระหว่างออบเจ็กต์ต่างๆ ได้แก่ สังเกตได้ ("subject" จากการสังเกตการณ์) และการสังเกตการณ์ สิ่งที่สังเกตได้คือออบเจ็กต์ที่สังเกตเห็นผู้สังเกตการณ์เกี่ยวกับการเปลี่ยนแปลงสถานะ

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

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

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

  1. ใน GameViewModel ให้สร้างออบเจ็กต์ MutableLiveData ของ Boolean ชื่อ _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 ให้แนบผู้สังเกตการณ์กับ eventGameFinish ใช้เมธอด observe() เรียกเมธอด gameFinished() ในฟังก์ชันแลมบ์ดา
// Observer for the Game finished event
viewModel.eventGameFinish.observe(this, Observer<Boolean> { hasFinished ->
   if (hasFinished) gameFinished()
})
  1. ลงแอป เล่นเกม และอ่านให้ครบทุกคํา แอปจะนําทางไปยังหน้าจอคะแนนโดยอัตโนมัติ แทนที่จะเป็นส่วนย่อยของเกมจนกว่าคุณจะแตะจบเกม

    หลังจากลิสต์คําว่างเปล่าแล้ว eventGameFinish จะตั้งค่าแล้ว ระบบจะเรียกใช้เมธอดผู้สังเกตที่เกี่ยวข้องใน Fragment เกม และแอปจะไปยังส่วน Fragment ของหน้าจอ
  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 ครั้ง และคุณอาจเห็นข้อความโทสต์ทุกครั้ง กรณีนี้เป็นข้อบกพร่อง เนื่องจากข้อความโทสต์ควรแสดงเพียงครั้งเดียวเมื่อเกมเล่นจบ ข้อความโทสต์ไม่ควรแสดงทุกครั้งที่มีการสร้างส่วนย่อยใหม่ คุณแก้ไขปัญหานี้ได้ในงานถัดไป

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

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

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

ในงานนี้ คุณจะแก้ไขปัญหานี้และแสดงข้อความโทสต์เพียงครั้งเดียว โดยรีเซ็ตแฟล็ก 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 ของเกมจะไปยังส่วน Fragment คะแนน

ในงานนี้ คุณจะได้เปลี่ยนคะแนนเป็นออบเจ็กต์ LiveData ใน ScoreViewModel และแนบการสังเกตการณ์ไปที่ออบเจ็กต์นั้น งานนี้คล้ายกับงานที่คุณเพิ่ม 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 ให้แนบผู้สังเกตการณ์สําหรับคะแนน LiveData กําหนดค่านิพจน์เป็นมุมมองข้อความคะแนนในนิพจน์แลมบ์ดา นําโค้ดที่กําหนดมุมมองข้อความออกด้วยค่าคะแนนออกจาก 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 และผู้สังเกตการณ์เพื่ออัปเดตคะแนน

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

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

  1. ใน res/layout/score_fragment.xml สําหรับปุ่ม play_again_button ให้เปลี่ยนแอตทริบิวต์ visibility's เป็น 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 ภายในนิพจน์แลมบ์ดา ให้กลับไปที่หน้าจอเกมและรีเซ็ต 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() ให้เพิ่ม Listener การคลิกลงในปุ่ม PlayAgain แล้วเรียก viewModel.onPlayAgain()
binding.playAgainButton.setOnClickListener {  viewModel.onPlayAgain()  }
  1. เรียกใช้แอปและเล่นเกม เมื่อเกมเล่นจบ หน้าจอคะแนนจะแสดงคะแนนสุดท้ายและปุ่มเล่นอีกครั้ง แตะปุ่ม PlayAgain แล้วแอปจะไปยังหน้าจอเกมเพื่อเล่นเกมอีกครั้ง

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

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

LiveData

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

วิธีเพิ่ม LiveData

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

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

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

เพื่อปกปิด LiveData

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

LiveData ที่ตรวจพบได้

  • LiveData เป็นไปตามรูปแบบของผู้สังเกตการณ์ "observable" เป็นออบเจ็กต์ LiveData และการสังเกตการณ์คือเมธอดในตัวควบคุม UI เช่น Fragment เมื่อใดก็ตามที่ข้อมูลที่อยู่ใน LiveData มีการเปลี่ยนแปลง วิธีการสังเกตการณ์ในตัวควบคุม UI จะได้รับการแจ้งเตือน
  • หากต้องการทําให้ LiveData สังเกตได้ ให้แนบออบเจ็กต์การสังเกตการณ์เข้ากับการอ้างอิง LiveData ในการสังเกตการณ์ (เช่น กิจกรรมและส่วนย่อย) โดยใช้เมธอด observe()
  • รูปแบบการสังเกตการณ์ LiveData นี้ใช้เพื่อสื่อสารจาก 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 (เช่น ส่วนย่อย) หากตัวควบคุม UI อยู่ในสถานะใด

  • กลับมาทำงานอีกครั้ง
  • เล่นขณะล็อกหน้าจอ
  • หยุดชั่วคราว
  • หยุดทำงานแล้ว

คำถามที่ 3

ในรูปแบบผู้สังเกตการณ์ LiveData มีสิ่งใดที่สังเกตได้ (สิ่งที่สังเกตได้)

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

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

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