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