ה-codelab הזה הוא חלק מהקורס Android Kotlin Fundamentals. כדי להפיק את המרב מהקורס הזה, מומלץ לעבוד על ה-codelabs לפי הסדר. כל ה-codelab של הקורס מפורטים בדף הנחיתה של ה-codelab בנושא יסודות Kotlin ל-Android.
מסך – כותרת | מסך המשחק | מסך הציון |
מבוא
ב-codelab הזה תלמדו על אחד מרכיבי הארכיטקטורה של Android, ViewModel
:
- משתמשים במחלקה
ViewModel
כדי לאחסן ולנהל נתונים שקשורים לממשק המשתמש באופן שמתחשב במחזור החיים. הסיווגViewModel
מאפשר לנתונים לשרוד שינויים בהגדרות המכשיר, כמו סיבוב המסך ושינויים בזמינות המקלדת. - משתמשים במחלקה
ViewModelFactory
כדי ליצור מופע של האובייקטViewModel
ולהחזיר אותו. האובייקט הזה שורד שינויים בהגדרות.
מה שכדאי לדעת
- איך ליצור אפליקציות בסיסיות ל-Android ב-Kotlin.
- איך משתמשים בתרשים הניווט כדי להטמיע ניווט באפליקציה.
- איך מוסיפים קוד כדי לנווט בין היעדים באפליקציה ולהעביר נתונים בין יעדי הניווט.
- איך מחזורי החיים של הפעילות והקטע פועלים.
- איך מוסיפים מידע לרישום ביומן לאפליקציה וקוראים את היומנים באמצעות Logcat ב-Android Studio.
מה תלמדו
- איך משתמשים בארכיטקטורת האפליקציה המומלצת ל-Android.
- איך להשתמש במחלקות
Lifecycle
,ViewModel
ו-ViewModelFactory
באפליקציה. - איך לשמור נתונים של ממשק משתמש כשמשנים את הגדרות המכשיר.
- מהו דפוס התכנון factory method ואיך משתמשים בו.
- איך יוצרים אובייקט
ViewModel
באמצעות הממשקViewModelProvider.Factory
.
הפעולות שתבצעו:
- מוסיפים
ViewModel
לאפליקציה, כדי לשמור את הנתונים של האפליקציה כך שהם לא יימחקו כשמשנים את ההגדרות. - משתמשים ב-
ViewModelFactory
ובדפוס העיצוב של שיטת המפעל כדי ליצור מופע של אובייקטViewModel
עם פרמטרים של בנאי.
בשיעור 5 של Codelab, מפתחים את האפליקציה GuessTheWord, החל מקוד התחלתי. GuessTheWord הוא משחק בסגנון ניחוש מילים לשני שחקנים, שבו השחקנים משתפים פעולה כדי להשיג את הניקוד הגבוה ביותר האפשרי.
השחקן הראשון מסתכל על המילים באפליקציה ומציג כל אחת מהן בתורו, תוך הקפדה לא להראות את המילה לשחקן השני. השחקן השני מנסה לנחש את המילה.
כדי לשחק במשחק, השחקן הראשון פותח את האפליקציה במכשיר ורואה מילה, למשל 'גיטרה', כמו שמוצג בצילום המסך שלמטה.
השחקן הראשון מדגים את המילה, ומשתדל לא לומר אותה.
- כשהשחקן השני מנחש את המילה נכון, השחקן הראשון לוחץ על הלחצן Got It (הבנתי), מה שמגדיל את הספירה באחד ומציג את המילה הבאה.
- אם השחקן השני לא מצליח לנחש את המילה, השחקן הראשון לוחץ על הכפתור דילוג, מה שמקטין את המספר באחד ומדלג למילה הבאה.
- כדי לסיים את המשחק, לוחצים על הלחצן סיום המשחק. (הפונקציונליות הזו לא מופיעה בקוד ההתחלתי של ה-codelab הראשון בסדרה).
במשימה הזו תורידו ותריצו את אפליקציית המתחילים ותבחנו את הקוד.
שלב 1: מתחילים
- מורידים את קוד ההתחלה של GuessTheWord ופותחים את הפרויקט ב-Android Studio.
- מריצים את האפליקציה במכשיר עם Android או באמולטור.
- מקישים על הלחצנים. שימו לב שהמילה הבאה מוצגת בלחיצה על הלחצן דילוג והניקוד יורד באחת, ובלחיצה על הלחצן הבנתי המילה הבאה מוצגת והניקוד עולה באחת. הלחצן סיום המשחק לא מיושם, ולכן לא קורה כלום כשמקישים עליו.
שלב 2: בדיקת הקוד
- ב-Android Studio, בודקים את הקוד כדי להבין איך האפליקציה פועלת.
- חשוב לבדוק את הקבצים שמתוארים בהמשך, כי הם חשובים במיוחד.
MainActivity.kt
הקובץ הזה מכיל רק קוד שנוצר כברירת מחדל על ידי תבנית.
res/layout/main_activity.xml
הקובץ הזה מכיל את הפריסה הראשית של האפליקציה. ה-NavHostFragment
מארח את שאר הרכיבים כשהמשתמש עובר בין חלקי האפליקציה.
רכיבי UI
קוד ההתחלה כולל שלושה קטעים בשלוש חבילות שונות מתחת לחבילה com.example.android.guesstheword.screens
:
-
title/TitleFragment
למסך השם -
game/GameFragment
במסך המשחק score/ScoreFragment
במסך התוצאה
screens/title/TitleFragment.kt
הקטע של הכותרת הוא המסך הראשון שמוצג כשמפעילים את האפליקציה. הוגדר handler ללחיצה על הלחצן Play (הפעלה), כדי לנווט למסך המשחק.
screens/game/GameFragment.kt
זהו הפרגמנט הראשי, שבו מתרחשת רוב הפעולה במשחק:
- המשתנים מוגדרים למילה הנוכחית ולציון הנוכחי.
- ההגדרה
wordList
בתוך השיטהresetList()
היא רשימת מילים לדוגמה לשימוש במשחק. - השיטה
onSkip()
היא handler של קליקים עבור הלחצן Skip (דילוג). הציון יופחת ב-1, ואז המילה הבאה תוצג באמצעות השיטהnextWord()
. - השיטה
onCorrect()
היא handler של קליקים עבור הלחצן Got It. השיטה הזו מיושמת באופן דומה לשיטהonSkip()
. ההבדל היחיד הוא שבשיטה הזו מוסיפים 1 לציון במקום להחסיר.
screens/score/ScoreFragment.kt
ScoreFragment
הוא המסך האחרון במשחק, ומוצג בו הניקוד הסופי של השחקן. ב-codelab הזה מוסיפים את ההטמעה כדי להציג את המסך הזה ואת הניקוד הסופי.
res/navigation/main_navigation.xml
בתרשים הניווט מוצג איך הפרגמנטים מקושרים באמצעות ניווט:
- מקטע הכותרת, המשתמש יכול לנווט לקטע המשחק.
- מקטע המשחק, המשתמש יכול לעבור לקטע הניקוד.
- מקטע הניקוד, המשתמש יכול לחזור לקטע המשחק.
במשימה הזו, תמצאו בעיות באפליקציית המתחילים GuessTheWord.
- מריצים את קוד ההתחלה ומשחקים במשחק כמה מילים, ומקישים על די או על הבנתי אחרי כל מילה.
- במסך המשחק מוצגת עכשיו מילה והניקוד הנוכחי. משנים את כיוון המסך על ידי סיבוב המכשיר או האמולטור. שימו לב שהניקוד הנוכחי יאבד.
- מריצים את המשחק עם עוד כמה מילים. כשהמסך של המשחק מוצג עם ניקוד מסוים, סוגרים את האפליקציה ופותחים אותה מחדש. שימו לב שהמשחק מופעל מחדש מההתחלה, כי מצב האפליקציה לא נשמר.
- משחקים במשחק עד שמגיעים לכמה מילים, ואז מקישים על הלחצן סיום המשחק. שימו לב שלא קורה כלום.
בעיות באפליקציה:
- אפליקציית המתחילים לא שומרת את מצב האפליקציה ולא משחזרת אותו במהלך שינויים בהגדרות, למשל כשמשנים את כיוון המכשיר או כשהאפליקציה נסגרת ומופעלת מחדש.
אפשר לפתור את הבעיה הזו באמצעות הקריאה החוזרתonSaveInstanceState()
. עם זאת, השימוש בשיטהonSaveInstanceState()
מחייב כתיבת קוד נוסף כדי לשמור את המצב בחבילה, והטמעה של הלוגיקה לאחזור המצב הזה. בנוסף, כמות הנתונים שאפשר לאחסן היא מינימלית. - כשמשתמש מקיש על הלחצן סיום המשחק, הוא לא מועבר ממסך המשחק למסך התוצאות.
כדי לפתור את הבעיות האלה, אפשר להשתמש ברכיבי ארכיטקטורת האפליקציה שמוסברים ב-codelab הזה.
ארכיטקטורת האפליקציה
ארכיטקטורת אפליקציה היא דרך לתכנן את המחלקות של האפליקציות ואת היחסים ביניהן, כך שהקוד יהיה מאורגן, יפעל בצורה טובה בתרחישים מסוימים ויהיה קל לעבוד איתו. בסדרה הזו של ארבעה codelab, השיפורים שתבצעו באפליקציית GuessTheWord יתבססו על ההנחיות של ארכיטקטורת אפליקציות ל-Android, ותשתמשו ב-Android Architecture Components. הארכיטקטורה של אפליקציות Android דומה לדפוס הארכיטקטוני MVVM (model-view-viewmodel).
אפליקציית GuessTheWord פועלת לפי העיקרון העיצובי של הפרדת נושאים ומחולקת למחלקות, כאשר כל מחלקה מטפלת בנושא נפרד. ב-Codelab הראשון הזה של השיעור, הכיתות שבהן תעבדו הן בקר ממשק משתמש, ViewModel
ו-ViewModelFactory
.
בקר ממשק משתמש
בקר ממשק משתמש הוא מחלקה שמבוססת על ממשק משתמש, כמו Activity
או Fragment
. בקר ממשק משתמש צריך להכיל רק לוגיקה שמטפלת באינטראקציות עם ממשק המשתמש ומערכת ההפעלה, כמו הצגת תצוגות וקליטת קלט משתמש. אל תכניסו לוגיקה של קבלת החלטות, כמו לוגיקה שקובעת את הטקסט שיוצג, לבקר של ממשק המשתמש.
בקוד ההתחלתי של GuessTheWord, הבקרים של ממשק המשתמש הם שלושת הפרגמנטים: GameFragment
, ScoreFragment,
ו-TitleFragment
. בהתאם לעיקרון העיצוב 'הפרדת דאגות', רכיב GameFragment
אחראי רק לציור של אלמנטים במשחק על המסך ולזיהוי מתי המשתמש מקיש על הלחצנים, ולא לשום דבר אחר. כשמשתמש מקיש על לחצן, המידע הזה מועבר אל GameViewModel
.
ViewModel
אובייקט ViewModel
מכיל נתונים שיוצגו בקטע או בפעילות שמשויכים ל-ViewModel
. ViewModel
יכול לבצע חישובים פשוטים ושינויים בנתונים כדי להכין אותם לתצוגה על ידי בקר ממשק המשתמש. בארכיטקטורה הזו, ViewModel
מבצע את קבלת ההחלטות.GameViewModel
מחזיק בנתונים כמו ערך הניקוד, רשימת המילים והמילה הנוכחית, כי אלה הנתונים שיוצגו במסך. בנוסף, GameViewModel
מכיל את הלוגיקה העסקית לביצוע חישובים פשוטים כדי לקבוע מה המצב הנוכחי של הנתונים.
ViewModelFactory
הפונקציה ViewModelFactory
יוצרת מופעים של אובייקטים מסוג ViewModel
, עם או בלי פרמטרים של בנאי.
ב-codelabs מאוחרים יותר, תלמדו על רכיבים אחרים של ארכיטקטורת Android שקשורים לבקרי ממשק משתמש ול-ViewModel
.
המחלקות ViewModel
מיועדות לאחסון ולניהול של נתונים שקשורים לממשק המשתמש. באפליקציה הזו, כל ViewModel
משויך לקטע אחד.
במשימה הזו מוסיפים את 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
נהרס כשהקטע המשויך מנותק, או כשהפעילות מסתיימת. רגע לפני שהאובייקט ViewModel
מושמד, מתבצעת קריאה חוזרת (callback) ל-onCleared()
כדי לנקות את המשאבים.
- במחלקה
GameViewModel
, מחליפים את השיטהonCleared()
. - מוסיפים הצהרת יומן בתוך
onCleared()
כדי לעקוב אחרי מחזור החיים שלGameViewModel
.
override fun onCleared() {
super.onCleared()
Log.i("GameViewModel", "GameViewModel destroyed!")
}
שלב 3: משייכים את GameViewModel לקטע המשחק
צריך לשייך ViewModel
לבקר ממשק המשתמש. כדי לשייך בין השניים, יוצרים הפניה אל ViewModel
בתוך בקר ממשק המשתמש.
בשלב הזה יוצרים הפניה אל GameViewModel
בתוך בקר ממשק המשתמש המתאים, שהוא GameFragment
.
- בכיתה
GameFragment
, מוסיפים שדה מהסוגGameViewModel
ברמה העליונה כמשתנה כיתתי.
private lateinit var viewModel: GameViewModel
שלב 4: מאתחלים את ViewModel
במהלך שינויים בהגדרות, כמו סיבוב המסך, נוצרים מחדש רכיבי בקרה של ממשק המשתמש, כמו fragments. עם זאת, ViewModel
מופעים ממשיכים להתקיים. אם יוצרים את מופע ViewModel
באמצעות המחלקה ViewModel
, נוצר אובייקט חדש בכל פעם שהקטע נוצר מחדש. במקום זאת, יוצרים את מופע ViewModel
באמצעות ViewModelProvider
.
איך ViewModelProvider
עובד:
- הפונקציה
ViewModelProvider
מחזירהViewModel
קיים אם יש כזה, או יוצרתViewModel
חדש אם הוא לא קיים. -
ViewModelProvider
יוצר מופע שלViewModel
בשיוך להיקף הנתון (פעילות או קטע). - ה-
ViewModel
שנוצר נשמר כל עוד ההיקף פעיל. לדוגמה, אם ההיקף הוא קטע, ה-ViewModel
נשמר עד שהקטע מנותק.
מפעילים את ViewModel
באמצעות ה-method ViewModelProviders.of()
כדי ליצור ViewModelProvider
:
- במחלקה
GameFragment
, מאתחלים את המשתנהviewModel
. מציבים את הקוד הזה בתוךonCreateView()
, אחרי ההגדרה של משתנה הקישור. משתמשים בשיטהViewModelProviders.of()
ומעבירים את ההקשר המשויךGameFragment
ואת המחלקהGameViewModel
. - מעל האתחול של האובייקט
ViewModel
, מוסיפים הצהרת יומן כדי לרשום ביומן את הקריאה ל-methodViewModelProviders.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.
- מפעילים את הגדרת הסיבוב האוטומטי במכשיר או באמולטור ומשנים את כיוון המסך כמה פעמים. ה-
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
לא יכולות להכיל הפניות לקטעים, לפעילויות או לתצוגות, כי פעילויות, קטעים ותצוגות לא שורדים שינויים בהגדרות.
לשם השוואה, כך מטופלים נתוני ממשק המשתמש של GameFragment
באפליקציית המתחילים לפני שמוסיפים את ViewModel
, ואחרי שמוסיפים את ViewModel
:
- לפני שמוסיפים את
ViewModel
:
כשהאפליקציה עוברת שינוי בהגדרה, כמו סיבוב המסך, קטע המשחק נמחק ונוצר מחדש. הנתונים אבדו. - אחרי שמוסיפים את
ViewModel
ומעבירים את נתוני ממשק המשתמש של קטע המשחק אלViewModel
:
כל הנתונים שצריך להציג בקטע הם עכשיוViewModel
. כשהאפליקציה עוברת שינוי בהגדרות, ה-ViewModel
נשמר והנתונים נשארים.
במשימה הזו תעבירו את נתוני ממשק המשתמש של האפליקציה אל המחלקה GameViewModel
, יחד עם השיטות לעיבוד הנתונים. הפעולה הזו מבטיחה שהנתונים יישמרו במהלך שינויים בהגדרות.
שלב 1: מעבירים את שדות הנתונים ואת עיבוד הנתונים אל ViewModel
העברת השדות והשיטות הבאים של נתונים מ-GameFragment
אל GameViewModel
:
- מעבירים את שדות הנתונים
word
,score
ו-wordList
. חשוב לוודא ש-word
ו-score
לא שווים ל-private
.
אל תעבירו את משתנה הקישור,GameFragmentBinding
, כי הוא מכיל הפניות לתצוגות. המשתנה הזה משמש להרחבת הפריסה, להגדרת מאזיני הקליקים ולהצגת הנתונים במסך – אלה האחריות של ה-fragment. - מעבירים את השיטות
resetList()
ו-nextWord()
. השיטות האלה קובעות איזו מילה תוצג על המסך. - בתוך השיטה
onCreateView()
, מעבירים את קריאות השיטה אלresetList()
ואלnextWord()
אל הבלוקinit
שלGameViewModel
.
השיטות האלה צריכות להיות בבלוקinit
, כי צריך לאפס את רשימת המילים כשיוצרים אתViewModel
, ולא בכל פעם שיוצרים את המקטע. אפשר למחוק את הצהרת היומן בבלוקinit
שלGameFragment
.
ה-handlers של הקליקים onSkip()
ו-onCorrect()
ב-GameFragment
מכילים קוד לעיבוד הנתונים ולעדכון ממשק המשתמש. הקוד לעדכון ממשק המשתמש צריך להישאר ב-fragment, אבל הקוד לעיבוד הנתונים צריך לעבור אל ViewModel
.
בשלב הזה, צריך להוסיף את אותן שיטות בשני המקומות:
- מעתיקים את השיטות
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: מעדכנים את ההפניות ל-click handlers ולשדות נתונים ב-GameFragment
- ב-
GameFragment
, מעדכנים את אמצעי התשלוםonSkip()
וonCorrect()
. מסירים את הקוד לעדכון הניקוד ובמקום זאת קוראים לשיטות המתאימותonSkip()
ו-onCorrect()
ב-viewModel
. - העברת את השיטה
nextWord()
אלViewModel
, ולכן אין יותר גישה אליה מהקטע של המשחק.
ב-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
. - מבצעים Build לאפליקציה ומוודאים שאין שגיאות. אם יש שגיאות, צריך לנקות את הפרויקט ולבנות אותו מחדש.
- מפעילים את האפליקציה ומשחקים במשחק באמצעות כמה מילים. במסך המשחק, מסובבים את המכשיר. שימו לב שהניקוד הנוכחי והמילה הנוכחית נשמרים אחרי שינוי הכיוון.
מעולה! עכשיו כל הנתונים של האפליקציה מאוחסנים ב-ViewModel
, ולכן הם נשמרים במהלך שינויים בהגדרות.
במשימה הזו, מטמיעים את מאזין הקליקים של הלחצן End Game (סיום המשחק).
- ב-
GameFragment
, מוסיפים שיטה בשםonEndGame()
. השיטהonEndGame()
תופעל כשהמשתמש יקיש על הלחצן סיום המשחק.
private fun onEndGame() {
}
- ב-
GameFragment
, בתוך השיטהonCreateView()
, מאתרים את הקוד שמגדיר את מאזיני הקליקים ללחצנים Got It (הבנתי) ו-Skip (דילוג). מתחת לשתי השורות האלה, מגדירים מאזין ללחיצות על הלחצן End Game. משתמשים במשתנה הקישור,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)
}
- ב-method
onEndGame()
, מפעילים את ה-methodgameFinished()
.
private fun onEndGame() {
gameFinished()
}
- מפעילים את האפליקציה, משחקים במשחק ועוברים בין כמה מילים. מקישים על הלחצן סיום המשחק . שימו לב שהאפליקציה עוברת למסך הניקוד, אבל הניקוד הסופי לא מוצג. תפתרו את הבעיה הזו במשימה הבאה.
כשהמשתמש מסיים את המשחק, הציון לא מוצג ב-ScoreFragment
. רוצים שViewModel
יכיל את הניקוד שיוצג על ידי ScoreFragment
. תעבירו את ערך הניקוד במהלך האתחול של ViewModel
באמצעות דפוס שיטת היצירה.
תבנית factory method היא תבנית עיצוב ליצירת אובייקטים שמשתמשת בשיטות ליצירת אובייקטים. שיטת factory היא שיטה שמחזירה מופע של אותה מחלקה.
במשימה הזו יוצרים ViewModel
עם בנאי פרמטריזציה בשביל קטע הניקוד ושיטת factory ליצירת מופע של ViewModel
.
- בקטע
score
package, יוצרים מחלקה חדשה של 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
package, יוצרים עוד מחלקה של 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
. מפעילים את methodViewModelProviders.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 מומלץ להפריד בין מחלקות עם תחומי אחריות שונים.
- בקר ממשק משתמש הוא מחלקה שמבוססת על ממשק משתמש, כמו
Activity
אוFragment
. רכיבי בקרה של ממשק המשתמש צריכים להכיל רק לוגיקה שמטפלת באינטראקציות עם ממשק המשתמש ומערכת ההפעלה. הם לא צריכים להכיל נתונים שיוצגו בממשק המשתמש. מכניסים את הנתונים ל-ViewModel
. - הכיתה
ViewModel
מאחסנת ומנהלת נתונים שקשורים לממשק המשתמש. הסיווגViewModel
מאפשר לנתונים לשרוד שינויים בהגדרות, כמו סיבוב המסך. -
ViewModel
הוא אחד מרכיבי הארכיטקטורה המומלצים של Android. -
ViewModelProvider.Factory
הוא ממשק שאפשר להשתמש בו כדי ליצור אובייקטViewModel
.
בטבלה שלמטה מוצגת השוואה בין אמצעי בקרה בממשק המשתמש לבין מופעי ViewModel
שמכילים את הנתונים שלהם:
UI controller | ViewModel |
דוגמה לבקר ממשק משתמש היא | דוגמה ל- |
לא מכיל נתונים להצגה בממשק המשתמש. | מכיל נתונים שמוצגים בממשק המשתמש על ידי בקר ממשק המשתמש. |
מכיל קוד להצגת נתונים וקוד של אירועים שקשורים למשתמש, כמו click listeners (מאזינים לקליקים). | מכיל קוד לעיבוד נתונים. |
הם נהרסים ונוצרים מחדש בכל שינוי בהגדרות. | האובייקט מושמד רק כשהבקר של ממשק המשתמש המשויך נעלם באופן סופי – בפעילות, כשהפעילות מסתיימת, או בקטע, כשהקטע מנותק. |
כולל צפיות. | לעולם לא צריך להכיל הפניות לפעילויות, לקטעים או לתצוגות, כי הם לא שורדים שינויים בהגדרות, אבל |
מכיל הפניה אל | לא מכיל הפניה לבקר ממשק המשתמש המשויך. |
קורס ב-Udacity:
מסמכי תיעוד למפתחי Android:
- סקירה כללית של ViewModel
- טיפול במחזורי חיים באמצעות רכיבים שמודעים למחזור החיים
- מדריך לארכיטקטורת אפליקציות
ViewModelProvider
ViewModelProvider.Factory
אחר:
- MVVM (model-view-viewmodel) architectural pattern.
- עקרון התכנון הפרדת בעיות (SoC)
- תבנית של שיטת יצירה
בקטע הזה מפורטות אפשרויות למשימות ביתיות לתלמידים שעובדים על ה-Codelab הזה כחלק מקורס בהנחיית מדריך. המורה צריך:
- אם צריך, מקצים שיעורי בית.
- להסביר לתלמידים איך להגיש מטלות.
- בודקים את שיעורי הבית.
אנשי ההוראה יכולים להשתמש בהצעות האלה כמה שרוצים, ומומלץ להם להקצות כל שיעורי בית אחרים שהם חושבים שמתאימים.
אם אתם עובדים על ה-codelab הזה לבד, אתם יכולים להשתמש במשימות האלה כדי לבדוק את הידע שלכם.
עונים על השאלות הבאות
שאלה 1
כדי למנוע אובדן נתונים במהלך שינוי בהגדרת המכשיר, באיזה סוג צריך לשמור את נתוני האפליקציה?
ViewModel
LiveData
Fragment
Activity
שאלה 2
מחלקת ViewModel
אף פעם לא צריכה להכיל הפניות ל-fragments, לפעילויות או לתצוגות. נכון או לא נכון?
- True
- לא נכון
שאלה 3
מתי ViewModel
נהרס?
- כשהבקר של ממשק המשתמש המשויך נהרס ונוצר מחדש במהלך שינוי של כיוון המכשיר.
- בשינוי הכיוון.
- כשהבקר של ממשק המשתמש המשויך מסתיים (אם מדובר בפעילות) או מנותק (אם מדובר בקטע).
- כשהמשתמש לוחץ על הלחצן 'הקודם'.
שאלה 4
למה משמש הממשק של ViewModelFactory
?
- יצירת מופע של אובייקט
ViewModel
. - שמירת הנתונים במהלך שינויים בכיוון המסך.
- רענון של הנתונים שמוצגים במסך.
- קבלת התראות כשנתוני האפליקציה משתנים.
עוברים לשיעור הבא:
קישורים ל-codelabs אחרים בקורס הזה מופיעים בדף הנחיתה של ה-codelabs בנושא יסודות Android Kotlin.