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: เริ่มต้นใช้งาน
- ดาวน์โหลดโค้ดเริ่มต้นของเกมทายคำ แล้วเปิดโปรเจ็กต์ใน Android Studio
- เรียกใช้แอปในอุปกรณ์ที่ใช้ Android หรือในโปรแกรมจำลอง
- แตะปุ่ม โปรดทราบว่าปุ่มข้ามจะแสดงคำถัดไปและลดคะแนนลง 1 คะแนน ส่วนปุ่มเข้าใจแล้วจะแสดงคำถัดไปและเพิ่มคะแนนขึ้น 1 คะแนน ปุ่มจบเกมยังไม่ได้ใช้งาน ดังนั้นจะไม่มีอะไรเกิดขึ้นเมื่อคุณแตะปุ่มนี้
ขั้นตอนที่ 2: ดูโค้ดทีละบรรทัด
- ใน Android Studio ให้สำรวจโค้ดเพื่อทำความเข้าใจวิธีการทำงานของแอป
- โปรดดูไฟล์ที่อธิบายไว้ด้านล่าง ซึ่งมีความสำคัญเป็นพิเศษ
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
- เรียกใช้โค้ดเริ่มต้นและเล่นเกมผ่านคำ 2-3 คำ โดยแตะข้ามหรือรับทราบหลังจากแต่ละคำ
- ตอนนี้หน้าจอเกมจะแสดงคำและคะแนนปัจจุบัน เปลี่ยนการวางแนวหน้าจอโดยหมุนอุปกรณ์หรือโปรแกรมจำลอง โปรดทราบว่าคะแนนปัจจุบันจะหายไป
- ลองเล่นเกมด้วยคำอื่นๆ อีก 2-3 คำ เมื่อหน้าจอเกมแสดงคะแนน ให้ปิดและเปิดแอปอีกครั้ง โปรดทราบว่าเกมจะรีสตาร์ทตั้งแต่ต้นเนื่องจากระบบไม่ได้บันทึกสถานะของแอป
- เล่นเกมโดยใช้คำ 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
- เปิดไฟล์
build.gradle(module:app)ภายในบล็อกdependenciesให้เพิ่มการอ้างอิง Gradle สำหรับViewModel
หากคุณใช้ไลบรารีเวอร์ชันล่าสุด แอปโซลูชันควรคอมไพล์ได้ตามที่คาดไว้ หากไม่เป็นเช่นนั้น ให้ลองแก้ปัญหาหรือกลับไปใช้เวอร์ชันที่แสดงด้านล่าง
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'- ในโฟลเดอร์แพ็กเกจ
screens/game/ให้สร้างคลาส Kotlin ใหม่ชื่อGameViewModel - ทำให้คลาส
GameViewModelขยายคลาสแอบสแทรกต์ViewModel - หากต้องการช่วยให้คุณเข้าใจว่า
ViewModelตระหนักถึงวงจรของกิจกรรมอย่างไร ให้เพิ่มบล็อกinitที่มีคำสั่งlog
class GameViewModel : ViewModel() {
init {
Log.i("GameViewModel", "GameViewModel created!")
}
}ขั้นตอนที่ 2: ลบล้าง onCleared() และเพิ่มการบันทึก
ViewModel จะถูกทำลายเมื่อมีการแยก Fragment ที่เชื่อมโยงออก หรือเมื่อกิจกรรมเสร็จสิ้น ก่อนที่จะทำลาย ViewModel ระบบจะเรียกใช้การเรียกกลับ onCleared() เพื่อล้างข้อมูลทรัพยากร
- ใน
GameViewModelคลาส ให้ลบล้างเมธอดonCleared() - เพิ่มคำสั่งบันทึกภายใน
onCleared()เพื่อติดตามวงจรGameViewModel
override fun onCleared() {
super.onCleared()
Log.i("GameViewModel", "GameViewModel destroyed!")
}ขั้นตอนที่ 3: เชื่อมโยง GameViewModel กับ Fragment ของเกม
ViewModel ต้องเชื่อมโยงกับตัวควบคุม UI หากต้องการเชื่อมโยงทั้ง 2 อย่าง ให้สร้างการอ้างอิงถึง ViewModel ภายในตัวควบคุม UI
ในขั้นตอนนี้ คุณจะสร้างการอ้างอิงของ GameViewModel ภายในตัวควบคุม UI ที่เกี่ยวข้อง ซึ่งก็คือ GameFragment
- ใน
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
- ใน
GameFragmentclass ให้เริ่มต้นตัวแปรviewModelวางโค้ดนี้ไว้ในonCreateView()หลังจากคำจำกัดความของตัวแปรการเชื่อมโยง ใช้วิธีViewModelProviders.of()และส่งบริบทGameFragmentที่เชื่อมโยงและคลาสGameViewModel - เหนือการเริ่มต้นออบเจ็กต์
ViewModelให้เพิ่มคำสั่งบันทึกเพื่อบันทึกการเรียกใช้เมธอดViewModelProviders.of()
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)- เรียกใช้แอป ใน Android Studio ให้เปิดแผง Logcat แล้วกรองตาม
Gameแตะปุ่มเล่นบนอุปกรณ์หรือโปรแกรมจำลอง หน้าจอเกมจะเปิดขึ้น
ดังที่แสดงใน LogcatonCreateView()เมธอดของGameFragmentเรียกใช้เมธอดViewModelProviders.of()เพื่อสร้างGameViewModelคำสั่งบันทึกที่คุณเพิ่มลงในGameFragmentและGameViewModelจะปรากฏใน Logcat

- เปิดใช้การตั้งค่าหมุนอัตโนมัติในอุปกรณ์หรือโปรแกรมจำลอง แล้วเปลี่ยนการวางแนวหน้าจอ 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
- ออกจากเกมหรือออกจากเกมที่เล่นอยู่
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
- ย้ายฟิลด์ข้อมูล
word,scoreและwordListตรวจสอบว่าwordและscoreไม่ใช่private
อย่าย้ายตัวแปรการเชื่อมโยงGameFragmentBindingเนื่องจากมีข้อมูลอ้างอิงถึงมุมมอง ตัวแปรนี้ใช้เพื่อขยายเลย์เอาต์ ตั้งค่าเครื่องมือฟังการคลิก และแสดงข้อมูลบนหน้าจอ ซึ่งเป็นหน้าที่ของ Fragment - ย้ายวิธีการ
resetList()และnextWord()วิธีการเหล่านี้จะกำหนดคำที่จะแสดงบนหน้าจอ - จากภายในเมธอด
onCreateView()ให้ย้ายการเรียกเมธอดไปยังresetList()และnextWord()ไปยังบล็อกinitของGameViewModel
เมธอดเหล่านี้ต้องอยู่ในบล็อกinitเนื่องจากคุณควรรีเซ็ตรายการคำเมื่อสร้างViewModelไม่ใช่ทุกครั้งที่สร้าง Fragment คุณสามารถลบคำสั่งบันทึกในบล็อกinitของGameFragmentได้
onSkip() และ onCorrect() ตัวแฮนเดิลการคลิกใน GameFragment มีโค้ดสำหรับการประมวลผลข้อมูลและอัปเดต UI โค้ดสำหรับอัปเดต UI ควรอยู่ใน Fragment แต่ต้องย้ายโค้ดสำหรับการประมวลผลข้อมูลไปยัง ViewModel
ตอนนี้ให้ใส่วิธีการที่เหมือนกันในทั้ง 2 ที่
- คัดลอกเมธอด
onSkip()และonCorrect()จากGameFragmentไปยังGameViewModel - ใน
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
- ใน
GameFragmentให้อัปเดตวิธีการonSkip()และonCorrect()นำโค้ดออกเพื่ออัปเดตคะแนน แล้วเรียกใช้เมธอดonSkip()และonCorrect()ที่เกี่ยวข้องในviewModelแทน - เนื่องจากคุณย้ายเมธอด
nextWord()ไปยังViewModelแล้ว ตอนนี้ Fragment ของเกมจึงเข้าถึงเมธอดดังกล่าวไม่ได้
ในGameFragmentในเมธอดonSkip()และonCorrect()ให้แทนที่การเรียกใช้nextWord()ด้วยupdateScoreText()และupdateWordText()ซึ่งวิธีเหล่านี้จะแสดงข้อมูลบนหน้าจอ
private fun onSkip() {
viewModel.onSkip()
updateWordText()
updateScoreText()
}
private fun onCorrect() {
viewModel.onCorrect()
updateScoreText()
updateWordText()
}- ใน
GameFragmentให้อัปเดตตัวแปรscoreและwordเพื่อใช้ตัวแปรGameViewModelเนื่องจากตอนนี้ตัวแปรเหล่านี้อยู่ในGameViewModel
private fun updateWordText() {
binding.wordText.text = viewModel.word
}
private fun updateScoreText() {
binding.scoreText.text = viewModel.score.toString()
}- ใน
GameViewModelภายในเมธอดnextWord()ให้นำการเรียกเมธอดupdateWordText()และupdateScoreText()ออก ตอนนี้ระบบจะเรียกใช้เมธอดเหล่านี้จากGameFragment - สร้างแอปและตรวจสอบว่าไม่มีข้อผิดพลาด หากมีข้อผิดพลาด ให้ล้างและสร้างโปรเจ็กต์ใหม่
- เรียกใช้แอปและเล่นเกมผ่านคำบางคำ ขณะอยู่ในหน้าจอเกม ให้หมุนอุปกรณ์ โปรดทราบว่าระบบจะเก็บคะแนนปัจจุบันและคำปัจจุบันไว้หลังจากเปลี่ยนการวางแนว
เก่งมาก ตอนนี้ระบบจะจัดเก็บข้อมูลทั้งหมดของแอปไว้ใน ViewModel เพื่อให้ข้อมูลยังคงอยู่ระหว่างการเปลี่ยนแปลงการกำหนดค่า
ในงานนี้ คุณจะติดตั้งใช้งาน Listener การคลิกสำหรับปุ่มจบเกม
- ใน
GameFragmentให้เพิ่มเมธอดที่ชื่อonEndGame()ระบบจะเรียกใช้เมธอดonEndGame()เมื่อผู้ใช้แตะปุ่มจบเกม
private fun onEndGame() {
}- ใน
GameFragmentภายในเมธอดonCreateView()ให้ค้นหารหัสที่ตั้งค่าเครื่องมือตรวจหาการคลิกสำหรับปุ่มรับทราบและข้าม ตั้งค่าเครื่องมือฟังการคลิกสำหรับปุ่มจบเกมใต้ 2 บรรทัดนี้ ใช้ตัวแปรการเชื่อมโยงbindingภายในเครื่องมือฟังการคลิก ให้เรียกใช้เมธอดonEndGame()
binding.endGameButton.setOnClickListener { onEndGame() }- ใน
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)
}- ในเมธอด
onEndGame()ให้เรียกใช้เมธอดgameFinished()
private fun onEndGame() {
gameFinished()
}- เรียกใช้แอป เล่นเกม และวนดูคำบางคำ แตะปุ่มจบเกม โปรดสังเกตว่าแอปจะไปยังหน้าจอคะแนน แต่จะไม่แสดงคะแนนสุดท้าย คุณแก้ไขปัญหานี้ได้ในงานถัดไป
|
|
เมื่อผู้ใช้จบเกม ScoreFragment จะไม่แสดงคะแนน คุณต้องการให้ ViewModel เก็บคะแนนเพื่อแสดงโดย ScoreFragment คุณจะส่งค่าคะแนนระหว่างการเริ่มต้น ViewModel โดยใช้รูปแบบเมธอด Factory
รูปแบบเมธอดของโรงงานคือรูปแบบการออกแบบเชิงสร้างสรรค์ที่ใช้เมธอดของโรงงานเพื่อสร้างออบเจ็กต์ เมธอดของ Factory คือเมธอดที่แสดงผลอินสแตนซ์ของคลาสเดียวกัน
ในงานนี้ คุณจะได้สร้าง ViewModel ที่มีตัวสร้างพารามิเตอร์สำหรับ Fragment คะแนน และเมธอด Factory เพื่อสร้างอินสแตนซ์ของ ViewModel
- สร้างคลาส Kotlin ใหม่ชื่อ
ScoreViewModelในscoreโดยชั้นเรียนนี้จะเป็นViewModelสำหรับส่วนคะแนน - ขยายคลาส
ScoreViewModelจากViewModel.เพิ่มพารามิเตอร์ตัวสร้างสำหรับคะแนนสุดท้าย เพิ่มบล็อกinitที่มีคำสั่งบันทึก - ใน
ScoreViewModelคลาส ให้เพิ่มตัวแปรชื่อscoreเพื่อบันทึกคะแนนสุดท้าย
class ScoreViewModel(finalScore: Int) : ViewModel() {
// The final score
var score = finalScore
init {
Log.i("ScoreViewModel", "Final score is $finalScore")
}
}- สร้างคลาส Kotlin อีกคลาสหนึ่งชื่อ
ScoreViewModelFactoryในแพ็กเกจscoreคลาสนี้จะรับผิดชอบในการสร้างออบเจ็กต์ScoreViewModel - ขยายเวลาชั้นเรียน
ScoreViewModelFactoryจากViewModelProvider.Factoryเพิ่มพารามิเตอร์ตัวสร้างสำหรับคะแนนสุดท้าย
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}- ใน
ScoreViewModelFactoryAndroid 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")
}- ใน
ScoreFragmentให้สร้างตัวแปรคลาสสำหรับScoreViewModelและScoreViewModelFactory
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory- ใน
ScoreFragmentภายในonCreateView()หลังจากเริ่มต้นตัวแปรbindingแล้ว ให้เริ่มต้นviewModelFactoryใช้ScoreViewModelFactoryส่งคะแนนสุดท้ายจากชุดอาร์กิวเมนต์เป็นพารามิเตอร์ของตัวสร้างไปยังScoreViewModelFactory()
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)- ใน
onCreateView() หลังจากเริ่มต้นviewModelFactoryแล้ว ให้เริ่มต้นออบเจ็กต์viewModelเรียกใช้เมธอดViewModelProviders.of()ส่งบริบทของ Fragment คะแนนที่เชื่อมโยงและviewModelFactoryซึ่งจะสร้างออบเจ็กต์ScoreViewModelโดยใช้วิธีการ Factory ที่กำหนดไว้ในคลาสviewModelFactory.
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ScoreViewModel::class.java)- ใน
onCreateView()เมธอด หลังจากเริ่มต้นviewModelแล้ว ให้ตั้งค่าข้อความของscoreTextวิวเป็นคะแนนสุดท้ายที่กำหนดไว้ในScoreViewModel
binding.scoreText.text = viewModel.score.toString()- เรียกใช้แอปและเล่นเกม เลื่อนดูคำบางคำหรือทั้งหมด แล้วแตะจบเกม โปรดสังเกตว่าตอนนี้ส่วนคะแนนจะแสดงคะแนนสุดท้าย

- ไม่บังคับ: ตรวจสอบ
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 คือ | ตัวอย่างของ |
ไม่มีข้อมูลที่จะแสดงใน UI | มีข้อมูลที่ตัวควบคุม UI แสดงใน UI |
มีโค้ดสําหรับแสดงข้อมูลและโค้ดเหตุการณ์ของผู้ใช้ เช่น ตัวตรวจจับการคลิก | มีโค้ดสำหรับการประมวลผลข้อมูล |
ทำลายและสร้างใหม่ทุกครั้งที่มีการเปลี่ยนแปลงการกำหนดค่า | จะถูกทำลายเมื่อตัวควบคุม UI ที่เชื่อมโยงหายไปอย่างถาวรเท่านั้น สำหรับกิจกรรม เมื่อกิจกรรมเสร็จสิ้น หรือสำหรับ Fragment เมื่อ Fragment ถูกยกเลิกการเชื่อมต่อ |
มีมุมมอง | ไม่ควรมีการอ้างอิงถึงกิจกรรม Fragment หรือ View เนื่องจากองค์ประกอบเหล่านี้จะไม่คงอยู่เมื่อมีการเปลี่ยนแปลงการกำหนดค่า แต่ |
มีการอ้างอิงถึง | ไม่มีการอ้างอิงถึงตัวควบคุม UI ที่เชื่อมโยง |
หลักสูตร Udacity:
เอกสารประกอบสำหรับนักพัฒนาแอป Android
- ภาพรวม ViewModel
- การจัดการวงจรด้วยคอมโพเนนต์ที่รับรู้ถึงวงจร
- คู่มือสถาปัตยกรรมแอป
ViewModelProviderViewModelProvider.Factory
อื่นๆ:
- รูปแบบสถาปัตยกรรม MVVM (Model-View-ViewModel)
- หลักการออกแบบการแยกความกังวล (SoC)
- รูปแบบเมธอดของ Factory
ส่วนนี้แสดงรายการการบ้านที่เป็นไปได้สำหรับนักเรียน/นักศึกษาที่กำลังทำ Codelab นี้เป็นส่วนหนึ่งของหลักสูตรที่สอนโดยผู้สอน ผู้สอนมีหน้าที่ดำเนินการต่อไปนี้
- มอบหมายการบ้านหากจำเป็น
- สื่อสารกับนักเรียนเกี่ยวกับวิธีส่งงานที่ได้รับมอบหมาย
- ให้คะแนนงานการบ้าน
ผู้สอนสามารถใช้คำแนะนำเหล่านี้ได้มากน้อยตามที่ต้องการ และควรมีอิสระในการมอบหมายการบ้านอื่นๆ ที่เห็นว่าเหมาะสม
หากคุณกำลังทำ Codelab นี้ด้วยตนเอง โปรดใช้แบบฝึกหัดเหล่านี้เพื่อทดสอบความรู้ของคุณ
ตอบคำถามต่อไปนี้
คำถามที่ 1
คุณควรบันทึกข้อมูลแอปไว้ในคลาสใดเพื่อหลีกเลี่ยงการสูญหายของข้อมูลระหว่างการเปลี่ยนแปลงการกำหนดค่าอุปกรณ์
ViewModelLiveDataFragmentActivity
คำถามที่ 2
ViewModel ไม่ควรมีการอ้างอิงถึง Fragment, Activity หรือ View จริงหรือเท็จ
- จริง
- เท็จ
คำถามที่ 3
ViewModel จะถูกทำลายเมื่อใด
- เมื่อตัวควบคุม UI ที่เชื่อมโยงถูกทำลายและสร้างขึ้นใหม่ในระหว่างการเปลี่ยนการวางแนวอุปกรณ์
- ในการเปลี่ยนการวางแนว
- เมื่อตัวควบคุม UI ที่เชื่อมโยงเสร็จสิ้น (หากเป็นกิจกรรม) หรือยกเลิกการเชื่อมต่อ (หากเป็น Fragment)
- เมื่อผู้ใช้กดปุ่มย้อนกลับ
คำถามที่ 4
อินเทอร์เฟซ ViewModelFactory มีไว้เพื่ออะไร
- การสร้างอินสแตนซ์ของออบเจ็กต์
ViewModel - การเก็บรักษาข้อมูลระหว่างการเปลี่ยนแปลงการวางแนว
- รีเฟรชข้อมูลที่แสดงบนหน้าจอ
- รับการแจ้งเตือนเมื่อมีการเปลี่ยนแปลงข้อมูลแอป
เริ่มบทเรียนถัดไป:
ดูลิงก์ไปยัง Codelab อื่นๆ ในหลักสูตรนี้ได้ที่หน้า Landing Page ของ Codelab หลักพื้นฐานของ Android Kotlin




