หลักพื้นฐานของ Android Kotlin 05.1: ViewModel และ ViewModelFactory

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

หน้าจอชื่อ

หน้าจอเกม

หน้าจอคะแนน

บทนำ

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

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

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

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

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

  • วิธีใช้สถาปัตยกรรมแอป Android ที่แนะนำ
  • วิธีใช้คลาส Lifecycle, ViewModel และ ViewModelFactory ในแอป
  • วิธีเก็บข้อมูล UI ไว้เมื่อมีการเปลี่ยนแปลงการกำหนดค่าอุปกรณ์
  • รูปแบบการออกแบบเมธอดของโรงงานคืออะไรและวิธีใช้งาน
  • วิธีสร้างViewModelออบเจ็กต์โดยใช้อินเทอร์เฟซViewModelProvider.Factory

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

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

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

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

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

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

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

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

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

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

ขั้นตอนที่ 2: ดูโค้ดทีละบรรทัด

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

MainActivity.kt

ไฟล์นี้มีเฉพาะโค้ดเริ่มต้นที่สร้างจากเทมเพลต

res/layout/main_activity.xml

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

UI Fragments

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

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

screens/title/TitleFragment.kt

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

screens/game/GameFragment.kt

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

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

screens/score/ScoreFragment.kt

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

res/navigation/main_navigation.xml

กราฟการนำทางแสดงวิธีเชื่อมต่อ Fragment ผ่านการนำทาง

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

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

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

ปัญหาในแอป

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

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

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

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

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

ตัวควบคุม UI

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

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

ViewModel

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

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

ViewModelFactory

ViewModelFactory สร้างออบเจ็กต์ ViewModel โดยมีหรือไม่มีพารามิเตอร์ของตัวสร้างก็ได้

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

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

ในงานนี้ คุณจะเพิ่ม ViewModel แรกในแอป ซึ่งก็คือ GameViewModel สำหรับ GameFragment นอกจากนี้ คุณยังได้เรียนรู้ความหมายของ ViewModel ที่รับรู้ถึงวงจรขององค์ประกอบ

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

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

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

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

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

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

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

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

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

  1. ในGameFragment คลาส ให้เพิ่มฟิลด์ประเภท GameViewModel ที่ระดับบนสุดเป็นตัวแปรคลาส
private lateinit var viewModel: GameViewModel

ขั้นตอนที่ 4: เริ่มต้น ViewModel

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

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

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

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

  1. ในGameFragment class ให้เริ่มต้นตัวแปร 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 ไม่ควรมีการอ้างอิงถึง Fragment, Activity หรือ View เนื่องจาก Activity, Fragment และ View จะไม่คงอยู่เมื่อมีการเปลี่ยนแปลงการกำหนดค่า

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

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

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

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

ย้ายช่องข้อมูลและวิธีการต่อไปนี้จาก GameFragment ไปยัง GameViewModel

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

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

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

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

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

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

โค้ดสำหรับคลาส 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 ในเมธอด onSkip() และ onCorrect() ให้แทนที่การเรียกใช้ nextWord() ด้วย updateScoreText() และ updateWordText() ซึ่งวิธีเหล่านี้จะแสดงข้อมูลบนหน้าจอ
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() ให้ค้นหารหัสที่ตั้งค่าเครื่องมือตรวจหาการคลิกสำหรับปุ่มรับทราบและข้าม ตั้งค่าเครื่องมือฟังการคลิกสำหรับปุ่มจบเกมใต้ 2 บรรทัดนี้ ใช้ตัวแปรการเชื่อมโยง binding ภายในเครื่องมือฟังการคลิก ให้เรียกใช้เมธอด onEndGame()
binding.endGameButton.setOnClickListener { onEndGame() }
  1. ใน GameFragment ให้เพิ่มเมธอดที่ชื่อ gameFinished() เพื่อไปยังหน้าจอคะแนนในแอป ส่งคะแนนเป็นอาร์กิวเมนต์โดยใช้ Safe Args
/**
* 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 โดยใช้รูปแบบเมธอด Factory

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

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

  1. สร้างคลาส Kotlin ใหม่ชื่อ ScoreViewModel ในscore โดยชั้นเรียนนี้จะเป็น 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. สร้างคลาส Kotlin อีกคลาสหนึ่งชื่อ ScoreViewModelFactory ในแพ็กเกจ score คลาสนี้จะรับผิดชอบในการสร้างออบเจ็กต์ 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() ส่งบริบทของ Fragment คะแนนที่เชื่อมโยงและ viewModelFactory ซึ่งจะสร้างออบเจ็กต์ ScoreViewModel โดยใช้วิธีการ Factory ที่กำหนดไว้ในคลาส 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

ViewModel

ตัวอย่างของตัวควบคุม UI คือ ScoreFragment ที่คุณสร้างในโค้ดแล็บนี้

ตัวอย่างของ ViewModel คือ ScoreViewModel ที่คุณสร้างในโค้ดแล็บนี้

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

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

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

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

ทำลายและสร้างใหม่ทุกครั้งที่มีการเปลี่ยนแปลงการกำหนดค่า

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

มีมุมมอง

ไม่ควรมีการอ้างอิงถึงกิจกรรม Fragment หรือ View เนื่องจากองค์ประกอบเหล่านี้จะไม่คงอยู่เมื่อมีการเปลี่ยนแปลงการกำหนดค่า แต่ ViewModel จะคงอยู่

มีการอ้างอิงถึง ViewModel ที่เกี่ยวข้อง

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

หลักสูตร Udacity:

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

อื่นๆ:

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

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

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

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

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

คำถามที่ 1

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

  • ViewModel
  • LiveData
  • Fragment
  • Activity

คำถามที่ 2

ViewModel ไม่ควรมีการอ้างอิงถึง Fragment, Activity หรือ View จริงหรือเท็จ

  • จริง
  • เท็จ

คำถามที่ 3

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

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

คำถามที่ 4

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

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

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

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