‫Android Kotlin Fundamentals 05.1: ViewModel and ViewModelFactory

ה-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: מתחילים

  1. מורידים את קוד ההתחלה של GuessTheWord ופותחים את הפרויקט ב-Android Studio.
  2. מריצים את האפליקציה במכשיר עם Android או באמולטור.
  3. מקישים על הלחצנים. שימו לב שהמילה הבאה מוצגת בלחיצה על הלחצן דילוג והניקוד יורד באחת, ובלחיצה על הלחצן הבנתי המילה הבאה מוצגת והניקוד עולה באחת. הלחצן סיום המשחק לא מיושם, ולכן לא קורה כלום כשמקישים עליו.

שלב 2: בדיקת הקוד

  1. ב-Android Studio, בודקים את הקוד כדי להבין איך האפליקציה פועלת.
  2. חשוב לבדוק את הקבצים שמתוארים בהמשך, כי הם חשובים במיוחד.

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.

  1. מריצים את קוד ההתחלה ומשחקים במשחק כמה מילים, ומקישים על די או על הבנתי אחרי כל מילה.
  2. במסך המשחק מוצגת עכשיו מילה והניקוד הנוכחי. משנים את כיוון המסך על ידי סיבוב המכשיר או האמולטור. שימו לב שהניקוד הנוכחי יאבד.
  3. מריצים את המשחק עם עוד כמה מילים. כשהמסך של המשחק מוצג עם ניקוד מסוים, סוגרים את האפליקציה ופותחים אותה מחדש. שימו לב שהמשחק מופעל מחדש מההתחלה, כי מצב האפליקציה לא נשמר.
  4. משחקים במשחק עד שמגיעים לכמה מילים, ואז מקישים על הלחצן סיום המשחק. שימו לב שלא קורה כלום.

בעיות באפליקציה:

  • אפליקציית המתחילים לא שומרת את מצב האפליקציה ולא משחזרת אותו במהלך שינויים בהגדרות, למשל כשמשנים את כיוון המכשיר או כשהאפליקציה נסגרת ומופעלת מחדש.
    אפשר לפתור את הבעיה הזו באמצעות הקריאה החוזרת 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

  1. פותחים את הקובץ build.gradle(module:app). בתוך הבלוק dependencies, מוסיפים את יחסי התלות של Gradle עבור ViewModel.

    אם משתמשים בגרסה האחרונה של הספרייה, קומפילציית אפליקציית הפתרון אמורה להתבצע כמצופה. אם לא, צריך לפתור את הבעיה או לחזור לגרסה שמוצגת למטה.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. בתיקייה screens/game/ של החבילה, יוצרים מחלקה חדשה של Kotlin בשם GameViewModel.
  2. גורמים לכיתה GameViewModel להרחיב את הכיתה המופשטת ViewModel.
  3. כדי להבין טוב יותר איך ViewModel מודע למחזור החיים, מוסיפים בלוק init עם הצהרת log.
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

שלב 2: מחליפים את השיטה onCleared() ומוסיפים רישום ביומן

האובייקט ViewModel נהרס כשהקטע המשויך מנותק, או כשהפעילות מסתיימת. רגע לפני שהאובייקט ViewModel מושמד, מתבצעת קריאה חוזרת (callback) ל-onCleared() כדי לנקות את המשאבים.

  1. במחלקה GameViewModel, מחליפים את השיטה onCleared().
  2. מוסיפים הצהרת יומן בתוך onCleared() כדי לעקוב אחרי מחזור החיים של GameViewModel.
override fun onCleared() {
   super.onCleared()
   Log.i("GameViewModel", "GameViewModel destroyed!")
}

שלב 3: משייכים את GameViewModel לקטע המשחק

צריך לשייך ViewModel לבקר ממשק המשתמש. כדי לשייך בין השניים, יוצרים הפניה אל ViewModel בתוך בקר ממשק המשתמש.

בשלב הזה יוצרים הפניה אל GameViewModel בתוך בקר ממשק המשתמש המתאים, שהוא GameFragment.

  1. בכיתה 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:

  1. במחלקה GameFragment, מאתחלים את המשתנה viewModel. מציבים את הקוד הזה בתוך onCreateView(), אחרי ההגדרה של משתנה הקישור. משתמשים בשיטה ViewModelProviders.of() ומעבירים את ההקשר המשויך GameFragment ואת המחלקה GameViewModel.
  2. מעל האתחול של האובייקט ViewModel, מוסיפים הצהרת יומן כדי לרשום ביומן את הקריאה ל-method‏ ViewModelProviders.of().
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
  1. מריצים את האפליקציה. ב-Android Studio, פותחים את החלונית Logcat ומסננים לפי Game. מקישים על לחצן ההפעלה במכשיר או באמולטור. מסך המשחק נפתח.

    כפי שמוצג ב-Logcat, השיטה onCreateView() של GameFragment קוראת לשיטה ViewModelProviders.of() כדי ליצור את GameViewModel. הצהרות הרישום ביומן שהוספתם ל-GameFragment ול-GameViewModel מופיעות ב-Logcat.

  1. מפעילים את הגדרת הסיבוב האוטומטי במכשיר או באמולטור ומשנים את כיוון המסך כמה פעמים. ה-GameFragment מושמד ונוצר מחדש בכל פעם, ולכן הפונקציה ViewModelProviders.of() מופעלת בכל פעם. אבל האובייקט GameViewModel נוצר רק פעם אחת, והוא לא נוצר מחדש או נמחק בכל קריאה.
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
  1. יוצאים מהמשחק או עוברים אל מחוץ לקטע המשחק. האובייקט GameFragment מושמד. גם GameViewModel המשויך מושמד, וההתקשרות חזרה onCleared() מופעלת.
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel destroyed!

ה-ViewModel שורד שינויים בהגדרות, ולכן הוא מקום טוב לנתונים שצריכים לשרוד שינויים בהגדרות:

  • מכניסים את הנתונים שרוצים להציג על המסך ואת הקוד לעיבוד הנתונים ב-ViewModel.
  • המחלקות ViewModel לא יכולות להכיל הפניות לקטעים, לפעילויות או לתצוגות, כי פעילויות, קטעים ותצוגות לא שורדים שינויים בהגדרות.

לשם השוואה, כך מטופלים נתוני ממשק המשתמש של GameFragment באפליקציית המתחילים לפני שמוסיפים את ViewModel, ואחרי שמוסיפים את ViewModel:

  • לפני שמוסיפים את ViewModel:
    כשהאפליקציה עוברת שינוי בהגדרה, כמו סיבוב המסך, קטע המשחק נמחק ונוצר מחדש. הנתונים אבדו.
  • אחרי שמוסיפים את ViewModel ומעבירים את נתוני ממשק המשתמש של קטע המשחק אל ViewModel:
    כל הנתונים שצריך להציג בקטע הם עכשיו ViewModel. כשהאפליקציה עוברת שינוי בהגדרות, ה-ViewModel נשמר והנתונים נשארים.

במשימה הזו תעבירו את נתוני ממשק המשתמש של האפליקציה אל המחלקה GameViewModel, יחד עם השיטות לעיבוד הנתונים. הפעולה הזו מבטיחה שהנתונים יישמרו במהלך שינויים בהגדרות.

שלב 1: מעבירים את שדות הנתונים ואת עיבוד הנתונים אל ViewModel

העברת השדות והשיטות הבאים של נתונים מ-GameFragment אל GameViewModel:

  1. מעבירים את שדות הנתונים word,‏ score ו-wordList. חשוב לוודא ש-word ו-score לא שווים ל-private.

    אל תעבירו את משתנה הקישור, GameFragmentBinding, כי הוא מכיל הפניות לתצוגות. המשתנה הזה משמש להרחבת הפריסה, להגדרת מאזיני הקליקים ולהצגת הנתונים במסך – אלה האחריות של ה-fragment.
  2. מעבירים את השיטות resetList() ו-nextWord(). השיטות האלה קובעות איזו מילה תוצג על המסך.
  3. בתוך השיטה onCreateView(), מעבירים את קריאות השיטה אל resetList() ואל nextWord() אל הבלוק init של GameViewModel.

    השיטות האלה צריכות להיות בבלוק init, כי צריך לאפס את רשימת המילים כשיוצרים את ViewModel, ולא בכל פעם שיוצרים את המקטע. אפשר למחוק את הצהרת היומן בבלוק init של GameFragment.

ה-handlers של הקליקים onSkip() ו-onCorrect() ב-GameFragment מכילים קוד לעיבוד הנתונים ולעדכון ממשק המשתמש. הקוד לעדכון ממשק המשתמש צריך להישאר ב-fragment, אבל הקוד לעיבוד הנתונים צריך לעבור אל ViewModel.

בשלב הזה, צריך להוסיף את אותן שיטות בשני המקומות:

  1. מעתיקים את השיטות onSkip() ו-onCorrect() מה-GameFragment אל ה-GameViewModel.
  2. ב-GameViewModel, מוודאים שהשיטות onSkip() ו-onCorrect() לא private, כי תהיה הפניה לשיטות האלה מה-fragment.

הנה הקוד לכיתה GameViewModel אחרי רפקטורינג:

class GameViewModel : ViewModel() {
   // The current word
   var word = ""
   // The current score
   var score = 0
   // The list of words - the front of the list is the next word to guess
   private lateinit var wordList: MutableList<String>

   /**
    * Resets the list of words and randomizes the order
    */
   private fun resetList() {
       wordList = mutableListOf(
               "queen",
               "hospital",
               "basketball",
               "cat",
               "change",
               "snail",
               "soup",
               "calendar",
               "sad",
               "desk",
               "guitar",
               "home",
               "railway",
               "zebra",
               "jelly",
               "car",
               "crow",
               "trade",
               "bag",
               "roll",
               "bubble"
       )
       wordList.shuffle()
   }

   init {
       resetList()
       nextWord()
       Log.i("GameViewModel", "GameViewModel created!")
   }
   /**
    * Moves to the next word in the list
    */
   private fun nextWord() {
       if (!wordList.isEmpty()) {
           //Select and remove a word from the list
           word = wordList.removeAt(0)
       }
       updateWordText()
       updateScoreText()
   }
 /** Methods for buttons presses **/
   fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }

   override fun onCleared() {
       super.onCleared()
       Log.i("GameViewModel", "GameViewModel destroyed!")
   }
}

הנה הקוד של הכיתה GameFragment אחרי רפקטורינג:

/**
* Fragment where the game is played
*/
class GameFragment : Fragment() {


   private lateinit var binding: GameFragmentBinding


   private lateinit var viewModel: GameViewModel


   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {

       // Inflate view and obtain an instance of the binding class
       binding = DataBindingUtil.inflate(
               inflater,
               R.layout.game_fragment,
               container,
               false
       )

       Log.i("GameFragment", "Called ViewModelProviders.of")
       viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)

       binding.correctButton.setOnClickListener { onCorrect() }
       binding.skipButton.setOnClickListener { onSkip() }
       updateScoreText()
       updateWordText()
       return binding.root

   }


   /** Methods for button click handlers **/

   private fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   private fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }


   /** Methods for updating the UI **/

   private fun updateWordText() {
       binding.wordText.text = word
   }

   private fun updateScoreText() {
       binding.scoreText.text = score.toString()
   }
}

שלב 2: מעדכנים את ההפניות ל-click handlers ולשדות נתונים ב-GameFragment

  1. ב-GameFragment, מעדכנים את אמצעי התשלום onSkip() וonCorrect(). מסירים את הקוד לעדכון הניקוד ובמקום זאת קוראים לשיטות המתאימות onSkip() ו- onCorrect() ב-viewModel.
  2. העברת את השיטה nextWord() אל ViewModel, ולכן אין יותר גישה אליה מהקטע של המשחק.

    ב-GameFragment, בשיטות onSkip() ו-onCorrect(), מחליפים את הקריאה ל-nextWord() ב-updateScoreText() וב-updateWordText(). השיטות האלה מציגות את הנתונים במסך.
private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}
  1. ב-GameFragment, מעדכנים את המשתנים score ו-word כך שישתמשו במשתנים GameViewModel, כי המשתנים האלה נמצאים עכשיו ב-GameViewModel.
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}
  1. ב-GameViewModel, בתוך השיטה nextWord(), מסירים את הקריאות לשיטות updateWordText() ו-updateScoreText(). השיטות האלה נקראות עכשיו מ-GameFragment.
  2. מבצעים Build לאפליקציה ומוודאים שאין שגיאות. אם יש שגיאות, צריך לנקות את הפרויקט ולבנות אותו מחדש.
  3. מפעילים את האפליקציה ומשחקים במשחק באמצעות כמה מילים. במסך המשחק, מסובבים את המכשיר. שימו לב שהניקוד הנוכחי והמילה הנוכחית נשמרים אחרי שינוי הכיוון.

מעולה! עכשיו כל הנתונים של האפליקציה מאוחסנים ב-ViewModel, ולכן הם נשמרים במהלך שינויים בהגדרות.

במשימה הזו, מטמיעים את מאזין הקליקים של הלחצן End Game (סיום המשחק).

  1. ב-GameFragment, מוסיפים שיטה בשם onEndGame(). השיטה onEndGame() תופעל כשהמשתמש יקיש על הלחצן סיום המשחק.
private fun onEndGame() {
   }
  1. ב-GameFragment, בתוך השיטה onCreateView(), מאתרים את הקוד שמגדיר את מאזיני הקליקים ללחצנים Got It (הבנתי) ו-Skip (דילוג). מתחת לשתי השורות האלה, מגדירים מאזין ללחיצות על הלחצן End Game. משתמשים במשתנה הקישור, binding. בתוך מאזין הקליקים, קוראים לשיטת onEndGame().
binding.endGameButton.setOnClickListener { onEndGame() }
  1. ב-GameFragment, מוסיפים שיטה בשם gameFinished() כדי לנווט באפליקציה למסך התוצאות. מעבירים את הניקוד כארגומנט באמצעות Safe Args.
/**
* Called when the game is finished
*/
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score
   NavHostFragment.findNavController(this).navigate(action)
}
  1. ב-method‏ onEndGame(), מפעילים את ה-method‏ gameFinished().
private fun onEndGame() {
   gameFinished()
}
  1. מפעילים את האפליקציה, משחקים במשחק ועוברים בין כמה מילים. מקישים על הלחצן סיום המשחק . שימו לב שהאפליקציה עוברת למסך הניקוד, אבל הניקוד הסופי לא מוצג. תפתרו את הבעיה הזו במשימה הבאה.

כשהמשתמש מסיים את המשחק, הציון לא מוצג ב-ScoreFragment. רוצים שViewModel יכיל את הניקוד שיוצג על ידי ScoreFragment. תעבירו את ערך הניקוד במהלך האתחול של ViewModel באמצעות דפוס שיטת היצירה.

תבנית factory method היא תבנית עיצוב ליצירת אובייקטים שמשתמשת בשיטות ליצירת אובייקטים. שיטת factory היא שיטה שמחזירה מופע של אותה מחלקה.

במשימה הזו יוצרים ViewModel עם בנאי פרמטריזציה בשביל קטע הניקוד ושיטת factory ליצירת מופע של ViewModel.

  1. בקטע score package, יוצרים מחלקה חדשה של Kotlin בשם ScoreViewModel. הכיתה הזו תהיה ViewModel של קטע הציון.
  2. מרחיבים את המחלקה ScoreViewModel מ-ViewModel. מוסיפים פרמטר של בנאי לציון הסופי. מוסיפים בלוק init עם הצהרת יומן.
  3. במחלקת ScoreViewModel, מוסיפים משתנה בשם score כדי לשמור את הציון הסופי.
class ScoreViewModel(finalScore: Int) : ViewModel() {
   // The final score
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}
  1. בקטע score package, יוצרים עוד מחלקה של Kotlin בשם ScoreViewModelFactory. הכיתה הזו תהיה אחראית ליצירת מופע של האובייקט ScoreViewModel.
  2. הארכת השיעור ScoreViewModelFactory מ-ViewModelProvider.Factory. מוסיפים פרמטר של בנאי לציון הסופי.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  1. ב-ScoreViewModelFactory, ‏ Android Studio מציג שגיאה לגבי חבר מופשט שלא הוטמע. כדי לפתור את השגיאה, צריך לבטל את השיטה create(). בשיטה create(), מחזירים את האובייקט ScoreViewModel שנבנה לאחרונה.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
       return ScoreViewModel(finalScore) as T
   }
   throw IllegalArgumentException("Unknown ViewModel class")
}
  1. ב-ScoreFragment, יוצרים משתני כיתה ל-ScoreViewModel ול-ScoreViewModelFactory.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. ב-ScoreFragment, בתוך onCreateView(), אחרי אתחול המשתנה binding, מאתחלים את viewModelFactory. משתמשים ב-ScoreViewModelFactory. מעבירים את הניקוד הסופי מחבילת הארגומנטים כפרמטר של בנאי אל ScoreViewModelFactory().
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. ב-onCreateView(), אחרי אתחול viewModelFactory, מאתחלים את האובייקט viewModel. מפעילים את method‏ ViewModelProviders.of(), מעבירים את ההקשר של קטע הניקוד המשויך ואת viewModelFactory. ייווצר אובייקט ScoreViewModel באמצעות שיטת היצירה שהוגדרה במחלקה viewModelFactory.
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. בשיטה onCreateView(), אחרי שמפעילים את viewModel, מגדירים את הטקסט של התצוגה scoreText לציון הדירוג הסופי שמוגדר ב-ScoreViewModel.
binding.scoreText.text = viewModel.score.toString()
  1. מפעילים את האפליקציה ומשחקים במשחק. מעיינים בחלק מהמילים או בכולן ומקישים על סיום המשחק. שימו לב שקטע הניקוד מציג עכשיו את הניקוד הסופי.

  1. אופציונלי: בודקים את היומנים ScoreViewModel ב-Logcat על ידי סינון לפי ScoreViewModel. ערך הניקוד צריך להיות מוצג.
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15

במשימה הזו הטמעתם את ScoreFragment כדי להשתמש ב-ViewModel. למדתם גם איך ליצור בנאי עם פרמטרים עבור ViewModel באמצעות הממשק ViewModelFactory.

מעולה! שיניתם את הארכיטקטורה של האפליקציה כדי להשתמש באחד מרכיבי הארכיטקטורה של Android, ‏ ViewModel. פתרתם את הבעיה במחזור החיים של האפליקציה, ועכשיו הנתונים של המשחק נשמרים גם אחרי שינויים בהגדרות. למדתם גם איך ליצור בנאי עם פרמטרים כדי ליצור ViewModel באמצעות הממשק ViewModelFactory.

פרויקט Android Studio: ‏ GuessTheWord

  • בהנחיות לגבי ארכיטקטורת אפליקציות ל-Android מומלץ להפריד בין מחלקות עם תחומי אחריות שונים.
  • בקר ממשק משתמש הוא מחלקה שמבוססת על ממשק משתמש, כמו Activity או Fragment. רכיבי בקרה של ממשק המשתמש צריכים להכיל רק לוגיקה שמטפלת באינטראקציות עם ממשק המשתמש ומערכת ההפעלה. הם לא צריכים להכיל נתונים שיוצגו בממשק המשתמש. מכניסים את הנתונים ל-ViewModel.
  • הכיתה ViewModel מאחסנת ומנהלת נתונים שקשורים לממשק המשתמש. הסיווג ViewModel מאפשר לנתונים לשרוד שינויים בהגדרות, כמו סיבוב המסך.
  • ViewModel הוא אחד מרכיבי הארכיטקטורה המומלצים של Android.
  • ViewModelProvider.Factory הוא ממשק שאפשר להשתמש בו כדי ליצור אובייקט ViewModel.

בטבלה שלמטה מוצגת השוואה בין אמצעי בקרה בממשק המשתמש לבין מופעי ViewModel שמכילים את הנתונים שלהם:

UI controller

ViewModel

דוגמה לבקר ממשק משתמש היא ScoreFragment שיצרתם ב-codelab הזה.

דוגמה ל-ViewModel היא ScoreViewModel שיצרתם ב-codelab הזה.

לא מכיל נתונים להצגה בממשק המשתמש.

מכיל נתונים שמוצגים בממשק המשתמש על ידי בקר ממשק המשתמש.

מכיל קוד להצגת נתונים וקוד של אירועים שקשורים למשתמש, כמו click listeners (מאזינים לקליקים).

מכיל קוד לעיבוד נתונים.

הם נהרסים ונוצרים מחדש בכל שינוי בהגדרות.

האובייקט מושמד רק כשהבקר של ממשק המשתמש המשויך נעלם באופן סופי – בפעילות, כשהפעילות מסתיימת, או בקטע, כשהקטע מנותק.

כולל צפיות.

לעולם לא צריך להכיל הפניות לפעילויות, לקטעים או לתצוגות, כי הם לא שורדים שינויים בהגדרות, אבל ViewModel כן שורד.

מכיל הפניה אל ViewModel המשויך.

לא מכיל הפניה לבקר ממשק המשתמש המשויך.

קורס ב-Udacity:

מסמכי תיעוד למפתחי Android:

אחר:

בקטע הזה מפורטות אפשרויות למשימות ביתיות לתלמידים שעובדים על ה-Codelab הזה כחלק מקורס בהנחיית מדריך. המורה צריך:

  • אם צריך, מקצים שיעורי בית.
  • להסביר לתלמידים איך להגיש מטלות.
  • בודקים את שיעורי הבית.

אנשי ההוראה יכולים להשתמש בהצעות האלה כמה שרוצים, ומומלץ להם להקצות כל שיעורי בית אחרים שהם חושבים שמתאימים.

אם אתם עובדים על ה-codelab הזה לבד, אתם יכולים להשתמש במשימות האלה כדי לבדוק את הידע שלכם.

עונים על השאלות הבאות

שאלה 1

כדי למנוע אובדן נתונים במהלך שינוי בהגדרת המכשיר, באיזה סוג צריך לשמור את נתוני האפליקציה?

  • ViewModel
  • LiveData
  • Fragment
  • Activity

שאלה 2

מחלקת ViewModel אף פעם לא צריכה להכיל הפניות ל-fragments, לפעילויות או לתצוגות. נכון או לא נכון?

  • True
  • לא נכון

שאלה 3

מתי ViewModel נהרס?

  • כשהבקר של ממשק המשתמש המשויך נהרס ונוצר מחדש במהלך שינוי של כיוון המכשיר.
  • בשינוי הכיוון.
  • כשהבקר של ממשק המשתמש המשויך מסתיים (אם מדובר בפעילות) או מנותק (אם מדובר בקטע).
  • כשהמשתמש לוחץ על הלחצן 'הקודם'.

שאלה 4

למה משמש הממשק של ViewModelFactory?

  • יצירת מופע של אובייקט ViewModel.
  • שמירת הנתונים במהלך שינויים בכיוון המסך.
  • רענון של הנתונים שמוצגים במסך.
  • קבלת התראות כשנתוני האפליקציה משתנים.

עוברים לשיעור הבא: 5.2: LiveData and LiveData observers

קישורים ל-codelabs אחרים בקורס הזה מופיעים בדף הנחיתה של ה-codelabs בנושא יסודות Android Kotlin.