Codelab นี้เป็นส่วนหนึ่งของหลักสูตรพื้นฐานเกี่ยวกับ Kotlin ใน Android คุณจะได้รับประโยชน์สูงสุดจากหลักสูตรนี้ หากทํางานผ่าน Codelab ตามลําดับ Codelab ของหลักสูตรทั้งหมดจะแสดงอยู่ในหน้า Landing Page ของ Codelab ของ Android Kotlin Fundamentals
หน้าจอชื่อ | หน้าจอเกม | หน้าจอคะแนน |
บทนำ
ใน Codelab นี้ คุณได้เรียนรู้เกี่ยวกับหนึ่งในคอมโพเนนต์สถาปัตยกรรม Android ViewModel
- คุณจะใช้คลาส
ViewModel
เพื่อจัดเก็บและจัดการข้อมูลที่เกี่ยวข้องกับ UI โดยคํานึงถึงการใช้งานตลอดอายุการใช้งาน คลาสViewModel
ช่วยให้ข้อมูลผ่านการเปลี่ยนแปลงการกําหนดค่าอุปกรณ์ เช่น การหมุนหน้าจอและการเปลี่ยนแปลงความพร้อมใช้งานของแป้นพิมพ์ - คุณใช้คลาส
ViewModelFactory
เพื่อจําลองและแสดงผลออบเจ็กต์ViewModel
ที่รอดจากการเปลี่ยนแปลงการกําหนดค่า
สิ่งที่ควรทราบอยู่แล้ว
- วิธีสร้างแอป Android ขั้นพื้นฐานใน Kotlin
- วิธีใช้กราฟการนําทางเพื่อไปยังส่วนต่างๆ ของแอป
- วิธีเพิ่มรหัสเพื่อไปยังปลายทางต่างๆ ของแอปและส่งผ่านข้อมูลระหว่างปลายทางของการนําทาง
- วิธีการทํางานของกิจกรรมและวงจรส่วนย่อย
- วิธีเพิ่มข้อมูลการบันทึกลงในแอปและอ่านบันทึกโดยใช้ Logcat ใน Android Studio
สิ่งที่คุณจะได้เรียนรู้
- วิธีใช้สถาปัตยกรรมแอป Android ที่แนะนํา
- วิธีใช้ชั้นเรียน
Lifecycle
,ViewModel
และViewModelFactory
ในแอป - วิธีเก็บรักษาข้อมูล UI ผ่านการเปลี่ยนแปลงการกําหนดค่าอุปกรณ์
- รูปแบบการออกแบบวิธีการเป็นค่าเริ่มต้นคืออะไรและวิธีใช้
- วิธีสร้างออบเจ็กต์
ViewModel
โดยใช้อินเทอร์เฟซViewModelProvider.Factory
สิ่งที่คุณจะทํา
- เพิ่ม
ViewModel
ลงในแอปเพื่อบันทึกข้อมูลของแอปเพื่อให้การเปลี่ยนแปลงข้อมูลมีผล - ใช้
ViewModelFactory
และรูปแบบการออกแบบเมธอดเป็นค่าเริ่มต้น เพื่อจําแนกออบเจ็กต์ViewModel
ด้วยพารามิเตอร์เครื่องมือสร้าง
ใน Codelab บทที่ 5 คุณจะพัฒนาแอป GuessTheWord ได้โดยเริ่มต้นด้วยโค้ดเริ่มต้น GuessTheWord เป็นเกมสไตล์ชาร์ดแบบผู้เล่น 2 คน ซึ่งผู้เล่นจะทํางานร่วมกันเพื่อให้ได้คะแนนสูงสุดเท่าที่จะเป็นไปได้
ผู้เล่นคนแรกจะดูคําในแอปและกระทําทีละคํา และอย่าแสดงคํานั้นให้ผู้เล่นคนที่สองเห็น ผู้เล่นรายที่ 2 พยายามเดาคํานั้น
ในการเล่นเกม ผู้เล่นคนแรกจะเปิดแอปในอุปกรณ์และจะเห็นคํา เช่น "guitar," ดังที่แสดงในภาพหน้าจอด้านล่าง
ผู้เล่นคนแรกจะทําตามคําและระวังอย่าพูดคํานั้นจริงๆ
- เมื่อผู้เล่นคนที่ 2 เดาคําได้อย่างถูกต้อง ผู้เล่นคนแรกจะกดปุ่มรับทราบ ซึ่งจะเพิ่มจํานวนเป็น 1 และแสดงคําถัดไป
- หากผู้เล่นรายที่ 2 เดาคําไม่ได้ ผู้เล่นคนแรกจะกดปุ่มข้าม ซึ่งจะลดจํานวนลงและข้ามไปยังคําถัดไป
- หากต้องการจบเกม ให้กดปุ่มจบเกม (ฟังก์ชันนี้จะไม่อยู่ในโค้ดเริ่มต้นสําหรับ Codelab แรกในชุด)
ในงานนี้ คุณจะดาวน์โหลดและเรียกใช้แอปเริ่มต้นและตรวจสอบโค้ดได้
ขั้นตอนที่ 1: เริ่มต้นใช้งาน
- ดาวน์โหลดรหัสเริ่มต้น GuessTheWord แล้วเปิดโครงการใน Android Studio
- เรียกใช้แอปในอุปกรณ์ที่ใช้ Android หรือในโปรแกรมจําลอง
- แตะปุ่ม โปรดสังเกตว่าปุ่มข้ามจะแสดงคําถัดไปและลดคะแนนลง 1 รายการ และปุ่มรับทราบจะแสดงคําถัดไปและเพิ่มคะแนนเป็น 1 คะแนน ปุ่มสิ้นสุดเกมจะไม่มีผล ดังนั้นจะไม่มีการดําเนินการใดๆ เมื่อคุณแตะปุ่มนั้น
ขั้นตอนที่ 2: ดําเนินการแนะนําโค้ด
- ใน Android Studio สํารวจโค้ดเพื่อทําความเข้าใจวิธีการทํางานของแอป
- ตรวจสอบไฟล์ที่อธิบายไว้ด้านล่างซึ่งสําคัญอย่างยิ่ง
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 เงื่อนไขเริ่มต้น
- เรียกใช้โค้ดเริ่มต้นและเล่นเกมผ่านคํา 2-3 คํา แตะข้ามหรือรับทราบหลังคําแต่ละคํา
- หน้าจอเกมแสดงคําและคะแนนปัจจุบัน เปลี่ยนการวางแนวหน้าจอโดยการหมุนอุปกรณ์หรือโปรแกรมจําลอง โปรดสังเกตว่าคะแนนปัจจุบันจะหายไป
- เล่นเกมอีก 2-3 คํา เมื่อแสดงหน้าจอเกมพร้อมคะแนนบางส่วน ให้ปิดและเปิดแอปอีกครั้ง โปรดทราบว่าเกมจะรีสตาร์ทตั้งแต่ต้น เนื่องจากไม่มีการบันทึกสถานะแอป
- เล่นเกมผ่านคํา 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
- เปิดไฟล์
build.gradle(module:app)
ภายในบล็อกdependencies
ให้เพิ่มทรัพยากร Gradle สําหรับViewModel
หากคุณใช้ไลบรารี เวอร์ชันล่าสุด แอปโซลูชันควรรวบรวมตามที่คาดไว้ หากไม่ใช่ ให้ลองแก้ปัญหาหรือเปลี่ยนกลับเป็นเวอร์ชันที่แสดงด้านล่าง
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
- สร้างโฟลเดอร์ Kotlin ใหม่ที่ชื่อว่า
GameViewModel
ในโฟลเดอร์screens/game/
- กําหนดให้คลาส
GameViewModel
ขยายคลาสนามธรรมViewModel
- เพิ่มบล็อก
init
ด้วยคําสั่งlog
เพื่อช่วยให้คุณเข้าใจViewModel
ได้ดีขึ้น
class GameViewModel : ViewModel() {
init {
Log.i("GameViewModel", "GameViewModel created!")
}
}
ขั้นตอนที่ 2: ลบล้าง onCleared() และเพิ่มการบันทึก
ViewModel
จะถูกทําลายเมื่อแยกส่วนย่อยที่เกี่ยวข้องออกหรือเมื่อกิจกรรมเสร็จสิ้น ก่อนที่ ViewModel
จะถูกทําลาย ระบบเรียกโค้ดเรียกกลับ onCleared()
เพื่อล้างทรัพยากร
- ในชั้นเรียน
GameViewModel
ให้ลบล้างเมธอดonCleared()
- เพิ่มคําสั่งบันทึกภายใน
onCleared()
เพื่อติดตามวงจรของGameViewModel
override fun onCleared() {
super.onCleared()
Log.i("GameViewModel", "GameViewModel destroyed!")
}
ขั้นตอนที่ 3: เชื่อมโยง GameViewmodel กับส่วนย่อยของเกม
ต้องเชื่อมโยง ViewModel
กับตัวควบคุม UI หากต้องการเชื่อมโยงทั้ง 2 อย่าง ให้สร้างการอ้างอิงไปยัง ViewModel
ในตัวควบคุม UI
ในขั้นตอนนี้ คุณจะต้องสร้างการอ้างอิงของ GameViewModel
ภายในตัวควบคุม UI ที่เกี่ยวข้อง ซึ่งก็คือ GameFragment
- ในชั้นเรียน
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
:
- ในตัวแปร
GameFragment
ให้เริ่มต้นตัวแปร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
แตะปุ่มเล่นในอุปกรณ์หรือโปรแกรมจําลอง หน้าจอเกมจะเปิดขึ้น
ดังที่แสดงใน Logcat เมธอดonCreateView()
ของ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
ไม่ควรมีการอ้างอิงส่วนย่อย กิจกรรม หรือข้อมูลพร็อพเพอร์ตี้ เนื่องจากกิจกรรม ส่วนย่อย และมุมมองจะไม่มีผลต่อการเปลี่ยนแปลงการกําหนดค่า
เพื่อเป็นการเปรียบเทียบ นี่คือวิธีจัดการข้อมูล UI ของ GameFragment
ในแอปเริ่มต้นก่อนเพิ่ม ViewModel
และหลังจากเพิ่ม ViewModel
- ก่อนเพิ่ม
ViewModel
:
เมื่อแอปมีการเปลี่ยนแปลงการกําหนดค่า เช่น การหมุนหน้าจอ ระบบจะทําลายชิ้นส่วนเกมและสร้างใหม่ ข้อมูลจะหายไป - หลังจากเพิ่ม
ViewModel
และย้ายข้อมูล UI ของ Fragment ไปยังViewModel
:
ข้อมูลทั้งหมดที่ Fragment ต้องแสดงจะเป็นViewModel
เมื่อแอปผ่านการเปลี่ยนแปลงการกําหนดค่าแล้วViewModel
จะยังคงอยู่และเก็บรักษาข้อมูลไว้
ในงานนี้ คุณจะต้องย้ายข้อมูล UI ของแอปไปยังคลาส GameViewModel
และวิธีประมวลผลข้อมูล คุณต้องใช้วิธีนี้เพื่อรักษาข้อมูลระหว่างการเปลี่ยนแปลงการกําหนดค่า
ขั้นตอนที่ 1: ย้ายช่องข้อมูลและการประมวลผลข้อมูลไปยัง ViewModel
ย้ายช่องและข้อมูลต่อไปนี้จาก GameFragment
ไปที่ GameViewModel
:
- ย้ายช่องข้อมูล
word
,score
และwordList
ตรวจสอบว่าword
และscore
ไม่ใช่private
อย่าย้ายตัวแปรการเชื่อมโยงGameFragmentBinding
เนื่องจากมีการอ้างอิงไปยังข้อมูลพร็อพเพอร์ตี้ ตัวแปรนี้ใช้เพิ่มเลย์เอาต์ ตั้งค่า Listener การคลิกให้สูงเกินจริง และแสดงข้อมูลบนหน้าจอ ได้แก่ ความรับผิดชอบของส่วนย่อย - ย้ายเมธอด
resetList()
และnextWord()
วิธีการเหล่านี้จะเป็นตัวกําหนดคําที่จะแสดงบนหน้าจอ - จากภายในเมธอด
onCreateView()
ให้ย้ายเมธอดไปยังresetList()
และnextWord()
ไปที่บล็อกinit
ของGameViewModel
วิธีการเหล่านี้จะต้องอยู่ในบล็อกinit
เนื่องจากคุณควรรีเซ็ตรายการคําเมื่อสร้างViewModel
ไม่ใช่ทุกครั้งที่สร้างส่วนย่อย คุณลบคําสั่งบันทึกในบล็อกinit
ของGameFragment
ได้
เครื่องจัดการคลิก onSkip()
และ onCorrect()
ใน GameFragment
มีโค้ดสําหรับการประมวลผลข้อมูลและอัปเดต UI โค้ดสําหรับอัปเดต UI ควรอยู่ในส่วนย่อย แต่โค้ดสําหรับประมวลผลข้อมูลจะต้องย้ายไปอยู่ที่ ViewModel
สําหรับตอนนี้ ให้เพิ่มวิธีการที่เหมือนกันในทั้ง 2 ที่
- คัดลอกเมธอด
onSkip()
และonCorrect()
จากGameFragment
ไปยังGameViewModel
- ใน
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
- ใน
GameFragment
ให้อัปเดตเมธอดonSkip()
และonCorrect()
นําโค้ดออกเพื่ออัปเดตคะแนนและเรียกเมธอดonSkip()
และonCorrect()
ที่เกี่ยวข้องในviewModel
แทน - เนื่องจากคุณย้ายเมธอด
nextWord()
ไปที่ViewModel
แล้ว Fragment ของเกมจึงไม่สามารถเข้าถึงได้อีกต่อไป
ในGameFragment
ระบบจะใช้วิธีแทนที่nextWord()
ด้วยupdateScoreText()
และupdateWordText()
ในเมธอดonSkip()
และonCorrect()
วิธีการเหล่านี้จะแสดงข้อมูลบนหน้าจอ
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()
ให้ค้นหาโค้ดที่ตั้งค่า Listener การคลิกสําหรับปุ่มรับทราบและข้าม ใต้ 2 บรรทัดนี้ ให้ตั้งค่า Listener การคลิกสําหรับปุ่มสิ้นสุดเกม ใช้ตัวแปรการเชื่อมโยงbinding
ภายใน Listener การคลิก ให้เรียกใช้เมธอดonEndGame()
binding.endGameButton.setOnClickListener { onEndGame() }
- ใน
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)
}
- ในเมธอด
onEndGame()
ให้เรียกเมธอดgameFinished()
private fun onEndGame() {
gameFinished()
}
- เรียกใช้แอป เล่นเกม และเลื่อนดูคําบางคํา แตะปุ่มสิ้นสุดเกม โปรดสังเกตว่าแอปจะไปยังหน้าจอคะแนน แต่คะแนนสรุปจะไม่แสดง คุณแก้ไขนี้ได้ในงานถัดไป
เมื่อผู้ใช้จบเกม ScoreFragment
จะไม่แสดงคะแนน คุณต้องการให้ ViewModel
ระงับคะแนนให้แสดงโดย ScoreFragment
คุณจะส่งค่าคะแนนระหว่างการเริ่มต้นของ ViewModel
โดยใช้รูปแบบวิธีการเป็นค่าเริ่มต้น
รูปแบบวิธีการเริ่มต้นเป็นรูปแบบการออกแบบที่สร้างสรรค์ที่ใช้วิธีการเป็นค่าเริ่มต้นในการสร้างวัตถุ วิธีการเริ่มต้นคือเมธอดที่ส่งคืนอินสแตนซ์ของคลาสเดียวกัน
ในงานนี้ คุณจะได้สร้าง ViewModel
ที่มีเครื่องมือสร้างพารามิเตอร์สําหรับ Fragment คะแนนและวิธีการจากโรงงานเพื่อแสดงตัวอย่าง ViewModel
- ใต้แพ็กเกจ
score
ให้สร้างคลาส Kotlin ใหม่ที่ชื่อว่าScoreViewModel
คลาสนี้จะเป็น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")
}
}
- ใต้แพ็กเกจ
score
ให้สร้างชั้นเรียน Kotlin อีกชั้นเรียนหนึ่งที่ชื่อScoreViewModelFactory
คลาสนี้จะมีหน้าที่แทนที่ออบเจ็กต์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()
แล้วส่งบริบทส่วนย่อยของคะแนนที่เกี่ยวข้องและviewModelFactory
การดําเนินการนี้จะสร้างออบเจ็กต์ScoreViewModel
โดยใช้เมธอดของโรงงานที่กําหนดไว้ในคลาส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 | โมเดลการดู |
ตัวอย่างตัวควบคุม UI คือ | ตัวอย่างของ |
ไม่มีข้อมูลที่จะแสดงใน UI | มีข้อมูลที่ตัวควบคุม UI แสดงใน UI |
มีโค้ดสําหรับแสดงข้อมูล และโค้ดเหตุการณ์ผู้ใช้ เช่น Listener การคลิก | มีโค้ดสําหรับการประมวลผลข้อมูล |
ถูกทําลายและสร้างใหม่ในระหว่างการเปลี่ยนแปลงการกําหนดค่าทั้งหมด | จะถูกทําลายเมื่อตัวควบคุม UI ที่เชื่อมโยงหายไปอย่างถาวร ไม่ว่าจะเป็นกิจกรรม กิจกรรมเสร็จสิ้น หรือสําหรับส่วนย่อย เมื่อแยกส่วนย่อย |
มีข้อมูลพร็อพเพอร์ตี้ | ไม่ควรมีการอ้างอิงกิจกรรม ส่วนย่อย หรือมุมมอง เนื่องจากองค์ประกอบเหล่านี้ไม่เปลี่ยนแปลงการเปลี่ยนแปลง แต่ |
มีการอ้างอิง | ไม่มีการอ้างอิงถึงตัวควบคุม UI ที่เกี่ยวข้อง |
หลักสูตร Udacity:
เอกสารประกอบสําหรับนักพัฒนาซอฟต์แวร์ Android
- ภาพรวมของ ViewModel
- การจัดการวงจรการใช้งานด้วยคอมโพเนนต์การรับรู้วงจรการใช้งาน
- คําแนะนําเกี่ยวกับสถาปัตยกรรมแอป
ViewModelProvider
ViewModelProvider.Factory
อื่นๆ:
- รูปแบบสถาปัตยกรรม MVVM (model-view-viewmodel)
- หลักในการแยกข้อกังวล (SoC)
- รูปแบบวิธีการจากโรงงาน
ส่วนนี้จะอธิบายการบ้านและรายงานสําหรับนักเรียนที่ทํางานผ่าน Codelab นี้ซึ่งเป็นส่วนหนึ่งของหลักสูตรที่นําโดยผู้สอน สิ่งที่ผู้สอนต้องทํามีดังนี้
- มอบหมายการบ้านหากจําเป็น
- สื่อสารกับนักเรียนเกี่ยวกับวิธีส่งงานทําการบ้าน
- ตัดเกรดการบ้าน
ผู้สอนจะใช้คําแนะนําเหล่านี้เท่าใดก็ได้หรือตามที่ต้องการก็ได้ และสามารถกําหนดให้การบ้านอื่นๆ ที่ตนคิดว่าเหมาะสมได้
หากคุณใช้ Codelab ด้วยตัวเอง ก็ให้ใช้การบ้านเพื่อทดสอบความรู้ของคุณได้
ตอบคําถามเหล่านี้
คำถามที่ 1
คุณควรบันทึกข้อมูลแอปไว้ในคลาสใด เพื่อหลีกเลี่ยงการสูญเสียข้อมูลระหว่างการเปลี่ยนแปลงการกําหนดค่าอุปกรณ์
ViewModel
LiveData
Fragment
Activity
คำถามที่ 2
ViewModel
ไม่ควรมีการอ้างอิงส่วน กิจกรรม หรือการดู จริงหรือเท็จ
- จริง
- ไม่จริง
คำถามที่ 3
ViewModel
จะถูกทําลายเมื่อใด
- เมื่อมีการทําลายและสร้างตัวควบคุม UI ที่เกี่ยวข้องระหว่างการเปลี่ยนแปลงการวางแนวอุปกรณ์
- อยู่ในช่วงเปลี่ยนการวางแนว
- เมื่อตัวควบคุม UI ที่เชื่อมโยงเสร็จสมบูรณ์ (หากเป็นกิจกรรม) หรือถูกปลดออก (หากเป็นส่วนย่อย)
- เมื่อผู้ใช้กดปุ่มย้อนกลับ
คำถามที่ 4
อินเทอร์เฟซ ViewModelFactory
มีไว้เพื่ออะไร
- กําลังเรียกคืนออบเจ็กต์
ViewModel
- การเก็บรักษาข้อมูลระหว่างการเปลี่ยนการวางแนว
- การรีเฟรชข้อมูลที่แสดงบนหน้าจอ
- การรับการแจ้งเตือนเมื่อมีการเปลี่ยนแปลงข้อมูลแอป
เริ่มบทเรียนถัดไป:
สําหรับลิงก์ไปยัง Codelab อื่นๆ ในหลักสูตรนี้ โปรดดูหน้า Landing Page ของ Codelab ของ Android Kotlin Fundamentals