Android Kotlin Fundamentals 05.1: Viewmodel และ ViewmodelFactory

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

หน้าจอชื่อ

หน้าจอเกม

หน้าจอคะแนน

บทนำ

ใน Codelab นี้ คุณได้เรียนรู้เกี่ยวกับหนึ่งในคอมโพเนนต์สถาปัตยกรรม Android ViewModel

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

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

  • วิธีสร้างแอป Android ขั้นพื้นฐานใน Kotlin
  • วิธีใช้กราฟการนําทางเพื่อไปยังส่วนต่างๆ ของแอป
  • วิธีเพิ่มรหัสเพื่อไปยังปลายทางต่างๆ ของแอปและส่งผ่านข้อมูลระหว่างปลายทางของการนําทาง
  • วิธีการทํางานของกิจกรรมและวงจรส่วนย่อย
  • วิธีเพิ่มข้อมูลการบันทึกลงในแอปและอ่านบันทึกโดยใช้ Logcat ใน Android Studio

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

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

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

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

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

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

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

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

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

ขั้นตอนที่ 1: เริ่มต้นใช้งาน

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

ขั้นตอนที่ 2: ดําเนินการแนะนําโค้ด

  1. ใน Android Studio สํารวจโค้ดเพื่อทําความเข้าใจวิธีการทํางานของแอป
  2. ตรวจสอบไฟล์ที่อธิบายไว้ด้านล่างซึ่งสําคัญอย่างยิ่ง

MainActivity.kt

ไฟล์นี้มีโค้ดเริ่มต้นแบบสร้างโดยเทมเพลตเท่านั้น

res/layout/main_activity.xml

ไฟล์นี้มีเลย์เอาต์หลักของแอป NavHostFragment จะโฮสต์ส่วนย่อยอื่นๆ เมื่อผู้ใช้ไปยังส่วนต่างๆ ของแอป

ส่วนย่อย UI

โค้ดเริ่มต้นมีส่วนย่อย 3 ส่วนในแพ็กเกจ 3 รายการที่อยู่ในแพ็กเกจ com.example.android.guesstheword.screens ดังนี้

  • title/TitleFragment สําหรับหน้าจอชื่อ
  • game/GameFragment สําหรับหน้าจอเกม
  • score/ScoreFragment สําหรับหน้าจอคะแนน

Screen/title/TitleFragment.kt

ส่วนย่อยของชื่อคือหน้าจอแรกที่แสดงเมื่อมีการเปิดแอป เครื่องจัดการการคลิกมีการตั้งค่าเป็นปุ่มเล่น เพื่อไปยังหน้าจอเกม

screen/game/GameFragment.kt

นี่คือส่วนย่อยหลักซึ่งเป็นการดําเนินการส่วนใหญ่ของเกม

  • ระบบจะกําหนดตัวแปรสําหรับคําปัจจุบันและคะแนนปัจจุบัน
  • wordList ที่กําหนดภายในเมธอด resetList() เป็นรายการตัวอย่างคําศัพท์ที่จะใช้ในเกม
  • เมธอด onSkip() คือเครื่องจัดการคลิกสําหรับปุ่มข้าม ซึ่งจะลดคะแนนลง 1 แล้วแสดงคําถัดไปโดยใช้เมธอด nextWord()
  • เมธอด onCorrect() คือเครื่องจัดการคลิกสําหรับปุ่มรับทราบ วิธีการนี้มีการใช้คล้ายกับวิธีการ onSkip() ความแตกต่างเพียงอย่างเดียวคือวิธีนี้จะเพิ่ม 1 ในคะแนนแทนการลบ

Screen/score/ScoreFragment.kt

ScoreFragment คือหน้าจอสุดท้ายในเกม และจะแสดงคะแนนสุดท้ายของผู้เล่น ใน Codelab นี้ ให้เพิ่มการติดตั้งใช้งานเพื่อแสดงหน้าจอนี้และแสดงคะแนนสรุป

res/Navigation/main_Navigation.xml

กราฟการนําทางจะแสดงวิธีเชื่อมต่อส่วนย่อยผ่านการนําทาง ดังนี้

  • จากส่วนย่อยของชื่อ ผู้ใช้จะไปยังส่วน Fragment เกมได้
  • Fragment ของผู้ใช้จะไปยังส่วน Fragment คะแนนได้
  • ผู้ใช้ Fragment จะย้อนกลับไปยัง Fragment ของเกมได้

ในงานนี้ คุณจะพบปัญหาเกี่ยวกับแอป GuessTheWord เงื่อนไขเริ่มต้น

  1. เรียกใช้โค้ดเริ่มต้นและเล่นเกมผ่านคํา 2-3 คํา แตะข้ามหรือรับทราบหลังคําแต่ละคํา
  2. หน้าจอเกมแสดงคําและคะแนนปัจจุบัน เปลี่ยนการวางแนวหน้าจอโดยการหมุนอุปกรณ์หรือโปรแกรมจําลอง โปรดสังเกตว่าคะแนนปัจจุบันจะหายไป
  3. เล่นเกมอีก 2-3 คํา เมื่อแสดงหน้าจอเกมพร้อมคะแนนบางส่วน ให้ปิดและเปิดแอปอีกครั้ง โปรดทราบว่าเกมจะรีสตาร์ทตั้งแต่ต้น เนื่องจากไม่มีการบันทึกสถานะแอป
  4. เล่นเกมผ่านคํา 2-3 คํา จากนั้นแตะปุ่มจบเกม โปรดทราบว่าไม่มีอะไรเกิดขึ้น

ปัญหาในแอป

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

คุณจะแก้ไขปัญหาเหล่านี้ได้โดยใช้คอมโพเนนต์สถาปัตยกรรมแอปที่คุณเรียนรู้ใน Codelab นี้

สถาปัตยกรรมแอป

สถาปัตยกรรมแอปคือวิธีในการออกแบบแอป ชั้นเรียน และความสัมพันธ์ระหว่างแอปดังกล่าว เช่น การจัดระเบียบโค้ด ทํางานได้ดีในสถานการณ์บางอย่าง และใช้งานง่าย ในชุด Codelab ชุดนี้ การปรับปรุงที่คุณทําในแอป GuessTheWord เป็นไปตามหลักเกณฑ์ของสถาปัตยกรรมแอป Android และคุณใช้คอมโพเนนต์สถาปัตยกรรม Android สถาปัตยกรรมแอป Android คล้ายกับรูปแบบสถาปัตยกรรม MVVM (model-view-viewmodel)

แอป GuessTheWord เป็นไปตามหลักการการออกแบบการแยกแยะข้อกังวลและแบ่งออกเป็นชั้นเรียน โดยแต่ละชั้นเรียนจะจัดการกับข้อกังวลแยกจากกัน ใน Codelab แรกของบทเรียนนี้ ชั้นเรียนที่คุณร่วมงานด้วยจะเป็นตัวควบคุม UI, ViewModel และ ViewModelFactory

ตัวควบคุม UI

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

ในโค้ดตัวควบคุม GuessTheWord ตัวควบคุม UI จะแบ่งออกเป็น 3 ส่วนคือ GameFragment, ScoreFragment, และ TitleFragment จากหลัก ""การแยกแยะข้อกังวล" และ "หลักการออกแบบ" GameFragment มีหน้าที่เพียงวาดองค์ประกอบเกมบนหน้าจอและรู้ว่าผู้ใช้แตะปุ่มเมื่อใดเท่านั้น เมื่อผู้ใช้แตะปุ่ม ระบบจะส่งข้อมูลนี้ไปยัง GameViewModel

ViewModel

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

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

มุมมองโรงงาน

ViewModelFactory จะจําลองออบเจ็กต์ ViewModel ที่มีหรือไม่มีพารามิเตอร์เครื่องมือสร้าง

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

ชั้นเรียน ViewModel ออกแบบมาเพื่อจัดเก็บและจัดการข้อมูลที่เกี่ยวข้องกับ UI ในแอปนี้ ViewModel แต่ละรายการจะเชื่อมโยงกับส่วนย่อย 1 รายการ

ในงานนี้ คุณเพิ่ม ViewModel รายการแรกลงในแอป GameViewModel สําหรับ GameFragment คุณยังได้เรียนรู้ด้วยว่า ViewModel รับรู้ถึงวงจรการใช้งานอย่างไร

ขั้นตอนที่ 1: เพิ่มคลาส GameViewmodel

  1. เปิดไฟล์ build.gradle(module:app) ภายในบล็อก dependencies ให้เพิ่มทรัพยากร Gradle สําหรับ ViewModel

    หากคุณใช้ไลบรารี เวอร์ชันล่าสุด แอปโซลูชันควรรวบรวมตามที่คาดไว้ หากไม่ใช่ ให้ลองแก้ปัญหาหรือเปลี่ยนกลับเป็นเวอร์ชันที่แสดงด้านล่าง
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. สร้างโฟลเดอร์ Kotlin ใหม่ที่ชื่อว่า GameViewModel ในโฟลเดอร์ screens/game/
  2. กําหนดให้คลาส GameViewModel ขยายคลาสนามธรรม ViewModel
  3. เพิ่มบล็อก init ด้วยคําสั่ง log เพื่อช่วยให้คุณเข้าใจ ViewModel ได้ดีขึ้น
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

ขั้นตอนที่ 2: ลบล้าง onCleared() และเพิ่มการบันทึก

ViewModel จะถูกทําลายเมื่อแยกส่วนย่อยที่เกี่ยวข้องออกหรือเมื่อกิจกรรมเสร็จสิ้น ก่อนที่ ViewModel จะถูกทําลาย ระบบเรียกโค้ดเรียกกลับ onCleared() เพื่อล้างทรัพยากร

  1. ในชั้นเรียน GameViewModel ให้ลบล้างเมธอด onCleared()
  2. เพิ่มคําสั่งบันทึกภายใน onCleared() เพื่อติดตามวงจรของ GameViewModel
override fun onCleared() {
   super.onCleared()
   Log.i("GameViewModel", "GameViewModel destroyed!")
}

ขั้นตอนที่ 3: เชื่อมโยง GameViewmodel กับส่วนย่อยของเกม

ต้องเชื่อมโยง ViewModel กับตัวควบคุม UI หากต้องการเชื่อมโยงทั้ง 2 อย่าง ให้สร้างการอ้างอิงไปยัง ViewModel ในตัวควบคุม UI

ในขั้นตอนนี้ คุณจะต้องสร้างการอ้างอิงของ GameViewModel ภายในตัวควบคุม UI ที่เกี่ยวข้อง ซึ่งก็คือ GameFragment

  1. ในชั้นเรียน GameFragment ให้เพิ่มช่องประเภท GameViewModel ที่ระดับบนสุดเป็นตัวแปรของชั้นเรียน
private lateinit var viewModel: GameViewModel

ขั้นตอนที่ 4: เริ่มต้นมุมมองโมเดล

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

วิธีการทํางานของ ViewModelProvider

  • ViewModelProvider แสดงผล ViewModel ที่มีอยู่หากมีอยู่ หรือจะสร้างรายการใหม่หากยังไม่มี
  • ViewModelProvider จะสร้างอินสแตนซ์ ViewModel ตามขอบเขตที่ระบุ (กิจกรรมหรือส่วนย่อย)
  • ระบบจะเก็บรักษา ViewModel ที่สร้างขึ้นไว้ตราบเท่าที่ขอบเขตยังใช้ได้ เช่น หากขอบเขตเป็นส่วนย่อย ระบบจะเก็บรักษา ViewModel ไว้จนกว่าจะมีการแยกส่วนย่อย

เริ่มต้น ViewModel โดยใช้เมธอด ViewModelProviders.of() เพื่อสร้าง ViewModelProvider:

  1. ในตัวแปร GameFragment ให้เริ่มต้นตัวแปร viewModel วางโค้ดนี้ไว้ภายใน onCreateView() หลังคําจํากัดความของตัวแปรการเชื่อมโยง ใช้เมธอด ViewModelProviders.of() แล้วส่งในบริบท GameFragment ที่เกี่ยวข้องและคลาส GameViewModel
  2. เหนือการเริ่มต้นออบเจ็กต์ ViewModel เพิ่มคําสั่งบันทึกเพื่อบันทึกการเรียกใช้เมธอด ViewModelProviders.of()
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
  1. เรียกใช้แอป ใน Android Studio ให้เปิดแผง Logcat และกรองใน Game แตะปุ่มเล่นในอุปกรณ์หรือโปรแกรมจําลอง หน้าจอเกมจะเปิดขึ้น

    ดังที่แสดงใน Logcat เมธอด onCreateView() ของ GameFragment จะเรียกใช้เมธอด ViewModelProviders.of() เพื่อสร้าง GameViewModel คําสั่งบันทึกที่คุณเพิ่มลงใน GameFragment และ GameViewModel จะปรากฏใน Logcat

  1. เปิดใช้การตั้งค่าการหมุนอัตโนมัติในอุปกรณ์หรือโปรแกรมจําลอง แล้วเปลี่ยนการวางแนวหน้าจอ 2-3 ครั้ง GameFragment จะถูกทําลายและสร้างขึ้นใหม่ในแต่ละครั้ง ดังนั้น ViewModelProviders.of() จะถูกเรียกใช้ทุกครั้ง แต่ GameViewModel จะสร้างขึ้นเพียงครั้งเดียว และจะไม่สร้างขึ้นใหม่หรือถูกทําลายจากการโทรแต่ละครั้ง
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
  1. ออกจากเกมหรือออกจากส่วนย่อยของเกม GameFragment ถูกทําลาย GameViewModel ที่เกี่ยวข้องก็ถูกทําลายเช่นกันและระบบจะเรียกใช้ onCleared() โค้ดเรียกกลับ
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel destroyed!

ViewModel รอดชีวิตจากการเปลี่ยนแปลงการกําหนดค่า ดังนั้นจึงเป็นที่ที่ดีสําหรับข้อมูลที่ต้องการยกเว้นการเปลี่ยนแปลงการกําหนดค่าต่อไปนี้

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

เพื่อเป็นการเปรียบเทียบ นี่คือวิธีจัดการข้อมูล UI ของ GameFragment ในแอปเริ่มต้นก่อนเพิ่ม ViewModel และหลังจากเพิ่ม ViewModel

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

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

ขั้นตอนที่ 1: ย้ายช่องข้อมูลและการประมวลผลข้อมูลไปยัง ViewModel

ย้ายช่องและข้อมูลต่อไปนี้จาก GameFragment ไปที่ GameViewModel:

  1. ย้ายช่องข้อมูล word, score และ wordList ตรวจสอบว่า word และ score ไม่ใช่ private

    อย่าย้ายตัวแปรการเชื่อมโยง GameFragmentBinding เนื่องจากมีการอ้างอิงไปยังข้อมูลพร็อพเพอร์ตี้ ตัวแปรนี้ใช้เพิ่มเลย์เอาต์ ตั้งค่า Listener การคลิกให้สูงเกินจริง และแสดงข้อมูลบนหน้าจอ ได้แก่ ความรับผิดชอบของส่วนย่อย
  2. ย้ายเมธอด resetList() และ nextWord() วิธีการเหล่านี้จะเป็นตัวกําหนดคําที่จะแสดงบนหน้าจอ
  3. จากภายในเมธอด onCreateView() ให้ย้ายเมธอดไปยัง resetList() และ nextWord() ไปที่บล็อก init ของ GameViewModel

    วิธีการเหล่านี้จะต้องอยู่ในบล็อก init เนื่องจากคุณควรรีเซ็ตรายการคําเมื่อสร้าง ViewModel ไม่ใช่ทุกครั้งที่สร้างส่วนย่อย คุณลบคําสั่งบันทึกในบล็อก init ของ GameFragment ได้

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

สําหรับตอนนี้ ให้เพิ่มวิธีการที่เหมือนกันในทั้ง 2 ที่

  1. คัดลอกเมธอด onSkip() และ onCorrect() จาก GameFragment ไปยัง GameViewModel
  2. ใน GameViewModel ให้ตรวจสอบว่าเมธอด onSkip() และ onCorrect() ไม่ใช่ private เนื่องจากคุณจะอ้างอิงวิธีการเหล่านี้จากส่วนย่อย

โค้ดสําหรับชั้นเรียน GameViewModel หลังจากรีแฟคเตอร์แล้ว:

class GameViewModel : ViewModel() {
   // The current word
   var word = ""
   // The current score
   var score = 0
   // The list of words - the front of the list is the next word to guess
   private lateinit var wordList: MutableList<String>

   /**
    * Resets the list of words and randomizes the order
    */
   private fun resetList() {
       wordList = mutableListOf(
               "queen",
               "hospital",
               "basketball",
               "cat",
               "change",
               "snail",
               "soup",
               "calendar",
               "sad",
               "desk",
               "guitar",
               "home",
               "railway",
               "zebra",
               "jelly",
               "car",
               "crow",
               "trade",
               "bag",
               "roll",
               "bubble"
       )
       wordList.shuffle()
   }

   init {
       resetList()
       nextWord()
       Log.i("GameViewModel", "GameViewModel created!")
   }
   /**
    * Moves to the next word in the list
    */
   private fun nextWord() {
       if (!wordList.isEmpty()) {
           //Select and remove a word from the list
           word = wordList.removeAt(0)
       }
       updateWordText()
       updateScoreText()
   }
 /** Methods for buttons presses **/
   fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }

   override fun onCleared() {
       super.onCleared()
       Log.i("GameViewModel", "GameViewModel destroyed!")
   }
}

โค้ดสําหรับชั้นเรียน GameFragment หลังจากรีแฟคเตอร์มีดังนี้

/**
* Fragment where the game is played
*/
class GameFragment : Fragment() {


   private lateinit var binding: GameFragmentBinding


   private lateinit var viewModel: GameViewModel


   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {

       // Inflate view and obtain an instance of the binding class
       binding = DataBindingUtil.inflate(
               inflater,
               R.layout.game_fragment,
               container,
               false
       )

       Log.i("GameFragment", "Called ViewModelProviders.of")
       viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)

       binding.correctButton.setOnClickListener { onCorrect() }
       binding.skipButton.setOnClickListener { onSkip() }
       updateScoreText()
       updateWordText()
       return binding.root

   }


   /** Methods for button click handlers **/

   private fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   private fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }


   /** Methods for updating the UI **/

   private fun updateWordText() {
       binding.wordText.text = word
   }

   private fun updateScoreText() {
       binding.scoreText.text = score.toString()
   }
}

ขั้นตอนที่ 2: อัปเดตการอ้างอิงถึงเครื่องจัดการการคลิกและช่องข้อมูลใน GameFragment

  1. ใน GameFragment ให้อัปเดตเมธอด onSkip() และ onCorrect() นําโค้ดออกเพื่ออัปเดตคะแนนและเรียกเมธอด onSkip() และ onCorrect() ที่เกี่ยวข้องใน viewModel แทน
  2. เนื่องจากคุณย้ายเมธอด nextWord() ไปที่ ViewModel แล้ว Fragment ของเกมจึงไม่สามารถเข้าถึงได้อีกต่อไป

    ใน GameFragment ระบบจะใช้วิธีแทนที่ nextWord() ด้วย updateScoreText() และ updateWordText() ในเมธอด onSkip() และ onCorrect() วิธีการเหล่านี้จะแสดงข้อมูลบนหน้าจอ
private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}
  1. ใน GameFragment ให้อัปเดตตัวแปร score และ word เพื่อใช้ตัวแปร GameViewModel เนื่องจากตัวแปรเหล่านี้อยู่ใน GameViewModel แล้ว
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}
  1. ใน GameViewModel ในส่วนวิธี nextWord() ให้นําการเรียกออกจากเมธอด updateWordText() และ updateScoreText() ตอนนี้เรากําลังเรียกใช้เมธอดจาก GameFragment
  2. โปรดสร้างแอปและตรวจสอบว่าไม่มีข้อผิดพลาด หากมีข้อผิดพลาด ให้ทําความสะอาดและสร้างโปรเจ็กต์อีกครั้ง
  3. เรียกใช้แอปและเล่นเกมผ่านคําบางคํา ขณะอยู่ในหน้าจอเกม ให้หมุนอุปกรณ์ โปรดสังเกตว่าคะแนนปัจจุบันและคําปัจจุบันจะยังคงอยู่หลังจากการเปลี่ยนแปลงการวางแนว

เก่งมาก ตอนนี้ข้อมูลทั้งหมดของแอปจะเก็บไว้ใน ViewModel ข้อมูลจึงเก็บรักษาไว้ระหว่างการเปลี่ยนแปลงการกําหนดค่า

ในการดําเนินการนี้ คุณจะใช้ Listener การคลิกสําหรับปุ่มสิ้นสุดเกม

  1. ใน GameFragment ให้เพิ่มเมธอดชื่อ onEndGame() ระบบจะเรียกใช้เมธอด onEndGame() เมื่อผู้ใช้แตะปุ่มสิ้นสุดเกม
private fun onEndGame() {
   }
  1. ใน GameFragment ภายในโค้ด onCreateView() ให้ค้นหาโค้ดที่ตั้งค่า Listener การคลิกสําหรับปุ่มรับทราบและข้าม ใต้ 2 บรรทัดนี้ ให้ตั้งค่า Listener การคลิกสําหรับปุ่มสิ้นสุดเกม ใช้ตัวแปรการเชื่อมโยง binding ภายใน Listener การคลิก ให้เรียกใช้เมธอด onEndGame()
binding.endGameButton.setOnClickListener { onEndGame() }
  1. ใน GameFragment ให้เพิ่มเมธอดที่ชื่อ gameFinished() เพื่อไปยังส่วนต่างๆ ของแอปไปยังหน้าจอคะแนน ส่งคะแนนเป็นอาร์กิวเมนต์โดยใช้ Google Safe Browsing
/**
* Called when the game is finished
*/
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score
   NavHostFragment.findNavController(this).navigate(action)
}
  1. ในเมธอด onEndGame() ให้เรียกเมธอด gameFinished()
private fun onEndGame() {
   gameFinished()
}
  1. เรียกใช้แอป เล่นเกม และเลื่อนดูคําบางคํา แตะปุ่มสิ้นสุดเกม โปรดสังเกตว่าแอปจะไปยังหน้าจอคะแนน แต่คะแนนสรุปจะไม่แสดง คุณแก้ไขนี้ได้ในงานถัดไป

เมื่อผู้ใช้จบเกม ScoreFragment จะไม่แสดงคะแนน คุณต้องการให้ ViewModel ระงับคะแนนให้แสดงโดย ScoreFragment คุณจะส่งค่าคะแนนระหว่างการเริ่มต้นของ ViewModel โดยใช้รูปแบบวิธีการเป็นค่าเริ่มต้น

รูปแบบวิธีการเริ่มต้นเป็นรูปแบบการออกแบบที่สร้างสรรค์ที่ใช้วิธีการเป็นค่าเริ่มต้นในการสร้างวัตถุ วิธีการเริ่มต้นคือเมธอดที่ส่งคืนอินสแตนซ์ของคลาสเดียวกัน

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

  1. ใต้แพ็กเกจ score ให้สร้างคลาส Kotlin ใหม่ที่ชื่อว่า ScoreViewModel คลาสนี้จะเป็น ViewModel สําหรับส่วนย่อยของคะแนน
  2. ขยายคลาส ScoreViewModel จาก ViewModel. เพิ่มพารามิเตอร์เครื่องมือสร้างสําหรับคะแนนสุดท้าย เพิ่มบล็อก init ที่มีคําสั่งบันทึก
  3. ในชั้นเรียน ScoreViewModel ให้เพิ่มตัวแปรที่ชื่อ score เพื่อบันทึกคะแนนสุดท้าย
class ScoreViewModel(finalScore: Int) : ViewModel() {
   // The final score
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}
  1. ใต้แพ็กเกจ score ให้สร้างชั้นเรียน Kotlin อีกชั้นเรียนหนึ่งที่ชื่อ ScoreViewModelFactory คลาสนี้จะมีหน้าที่แทนที่ออบเจ็กต์ ScoreViewModel
  2. ขยายคลาส ScoreViewModelFactory จาก ViewModelProvider.Factory เพิ่มพารามิเตอร์ตัวสร้างสําหรับคะแนนสุดท้าย
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  1. ใน ScoreViewModelFactory Android Studio จะแสดงข้อผิดพลาดเกี่ยวกับสมาชิกนามธรรมที่ไม่มีการใช้งาน หากต้องการลบล้าง ให้ลบล้างเมธอด create() ในเมธอด create() ให้แสดงผลออบเจ็กต์ ScoreViewModel ที่สร้างขึ้นใหม่
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
       return ScoreViewModel(finalScore) as T
   }
   throw IllegalArgumentException("Unknown ViewModel class")
}
  1. ใน ScoreFragment ให้สร้างตัวแปรคลาสสําหรับ ScoreViewModel และ ScoreViewModelFactory
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. ใน ScoreFragment ภายใน onCreateView() หลังจากเริ่มต้นตัวแปร binding แล้ว ให้เริ่มต้น viewModelFactory ใช้ ScoreViewModelFactory ส่งคะแนนสุดท้ายจากกลุ่มอาร์กิวเมนต์เป็นพารามิเตอร์เครื่องมือสร้างไปยัง ScoreViewModelFactory()
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. ใน onCreateView() หลังจากเริ่มต้น viewModelFactory แล้ว ให้เริ่มต้นออบเจ็กต์ viewModel เรียกใช้เมธอด ViewModelProviders.of() แล้วส่งบริบทส่วนย่อยของคะแนนที่เกี่ยวข้องและ viewModelFactory การดําเนินการนี้จะสร้างออบเจ็กต์ ScoreViewModel โดยใช้เมธอดของโรงงานที่กําหนดไว้ในคลาส viewModelFactory.
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. ในวิธี onCreateView() หลังจากเริ่มต้น viewModel ให้ตั้งค่าข้อความของมุมมอง scoreText เป็นคะแนนสุดท้ายที่กําหนดไว้ใน ScoreViewModel
binding.scoreText.text = viewModel.score.toString()
  1. เรียกใช้แอปและเล่นเกม เลื่อนดูคําบางคําหรือทุกคํา แล้วแตะสิ้นสุดเกม โปรดทราบว่าส่วนย่อยของคะแนนจะแสดงคะแนนสุดท้ายแล้ว

  1. ไม่บังคับ: ตรวจสอบบันทึก ScoreViewModel ใน Logcat โดยการกรองใน ScoreViewModel ค่าคะแนนควรแสดง
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15

ในงานนี้ คุณได้นํา ScoreFragment ไปใช้ ViewModel คุณยังได้เรียนรู้วิธีสร้างเครื่องมือสร้างพารามิเตอร์สําหรับ ViewModel โดยใช้อินเทอร์เฟซ ViewModelFactory

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

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

  • หลักเกณฑ์ของสถาปัตยกรรมแอปของ Android แนะนําให้แยกชั้นเรียนที่มีหน้าที่รับผิดชอบแตกต่างกัน
  • ตัวควบคุม UI คือคลาสที่ใช้ UI เช่น Activity หรือ Fragment ตัวควบคุม UI ควรมีตรรกะที่จัดการ UI และการโต้ตอบกับระบบปฏิบัติการเท่านั้น และควรมีข้อมูลที่จะแสดงใน UI ใส่ข้อมูลนั้นใน ViewModel
  • ชั้นเรียน ViewModel จะจัดเก็บและจัดการข้อมูลที่เกี่ยวข้องกับ UI คลาส ViewModel ช่วยให้ข้อมูลผ่านการเปลี่ยนแปลงการกําหนดค่า เช่น การหมุนหน้าจอ
  • ViewModel เป็นหนึ่งในคอมโพเนนต์สถาปัตยกรรม Android ที่แนะนํา
  • ViewModelProvider.Factory คืออินเทอร์เฟซที่คุณใช้สร้างออบเจ็กต์ ViewModel ได้

ตารางด้านล่างเปรียบเทียบตัวควบคุม UI กับอินสแตนซ์ ViewModel ที่มีข้อมูล

ตัวควบคุม UI

โมเดลการดู

ตัวอย่างตัวควบคุม UI คือ ScoreFragment ที่คุณสร้างใน Codelab นี้

ตัวอย่างของ ViewModel คือ ScoreViewModel ที่คุณสร้างใน Codelab นี้

ไม่มีข้อมูลที่จะแสดงใน UI

มีข้อมูลที่ตัวควบคุม UI แสดงใน UI

มีโค้ดสําหรับแสดงข้อมูล และโค้ดเหตุการณ์ผู้ใช้ เช่น Listener การคลิก

มีโค้ดสําหรับการประมวลผลข้อมูล

ถูกทําลายและสร้างใหม่ในระหว่างการเปลี่ยนแปลงการกําหนดค่าทั้งหมด

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

มีข้อมูลพร็อพเพอร์ตี้

ไม่ควรมีการอ้างอิงกิจกรรม ส่วนย่อย หรือมุมมอง เนื่องจากองค์ประกอบเหล่านี้ไม่เปลี่ยนแปลงการเปลี่ยนแปลง แต่ ViewModel มีผลอยู่

มีการอ้างอิง ViewModel

ไม่มีการอ้างอิงถึงตัวควบคุม UI ที่เกี่ยวข้อง

หลักสูตร Udacity:

เอกสารประกอบสําหรับนักพัฒนาซอฟต์แวร์ Android

อื่นๆ:

ส่วนนี้จะอธิบายการบ้านและรายงานสําหรับนักเรียนที่ทํางานผ่าน Codelab นี้ซึ่งเป็นส่วนหนึ่งของหลักสูตรที่นําโดยผู้สอน สิ่งที่ผู้สอนต้องทํามีดังนี้

  • มอบหมายการบ้านหากจําเป็น
  • สื่อสารกับนักเรียนเกี่ยวกับวิธีส่งงานทําการบ้าน
  • ตัดเกรดการบ้าน

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

หากคุณใช้ Codelab ด้วยตัวเอง ก็ให้ใช้การบ้านเพื่อทดสอบความรู้ของคุณได้

ตอบคําถามเหล่านี้

คำถามที่ 1

คุณควรบันทึกข้อมูลแอปไว้ในคลาสใด เพื่อหลีกเลี่ยงการสูญเสียข้อมูลระหว่างการเปลี่ยนแปลงการกําหนดค่าอุปกรณ์

  • ViewModel
  • LiveData
  • Fragment
  • Activity

คำถามที่ 2

ViewModel ไม่ควรมีการอ้างอิงส่วน กิจกรรม หรือการดู จริงหรือเท็จ

  • จริง
  • ไม่จริง

คำถามที่ 3

ViewModel จะถูกทําลายเมื่อใด

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

คำถามที่ 4

อินเทอร์เฟซ ViewModelFactory มีไว้เพื่ออะไร

  • กําลังเรียกคืนออบเจ็กต์ ViewModel
  • การเก็บรักษาข้อมูลระหว่างการเปลี่ยนการวางแนว
  • การรีเฟรชข้อมูลที่แสดงบนหน้าจอ
  • การรับการแจ้งเตือนเมื่อมีการเปลี่ยนแปลงข้อมูลแอป

เริ่มบทเรียนถัดไป: 5.2: ผู้สังเกตการณ์ LiveData และ LiveData

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