Android Kotlin Fundamentals 05.1: ViewModel ו-ViewModelfact

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

מסך – כותרת

מסך המשחק

מסך הניקוד

מבוא

במעבדה הזו, תוכלו ללמוד על אחד מרכיבי הארכיטקטורה של Android ViewModel:

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

דברים שחשוב לדעת

  • איך ליצור אפליקציות בסיסיות ל-Android ב-Kotlin.
  • איך משתמשים בתרשים הניווט להטמעת ניווט באפליקציה.
  • איך להוסיף קוד לניווט בין יעדי האפליקציה והעברת הנתונים בין יעדי הניווט.
  • כיצד פועלים מחזורי החיים של מחזורי החיים והמקטעים.
  • איך להוסיף פרטי רישום לאפליקציה ולקרוא יומנים באמצעות Logcat ב-Android Studio.

מה תלמדו

הפעולות שתבצעו:

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

במעבדות הקוד של שיעור 5, אתם מפתחים את האפליקציה GuessTheWord, שמתחילה בקוד למתחילים. GuessTheWord הוא משחק בסגנון שחקנים ששני שחקנים. השחקן משתף פעולה כדי להשיג את הניקוד הגבוה ביותר האפשרי.

השחקן הראשון בוחן את המילים באפליקציה ומתאים כל אחת מהן לשחקן אחר, ומבטיח שלא להציג את המילה לשחקן השני. השחקן השני מנסה לנחש את המילה.

כדי לשחק במשחק, השחקן הראשון פותח את האפליקציה במכשיר וצופה במילה, למשל "רצועה&לגיטרה; כפי שמוצג בצילום המסך שבהמשך.

השחקן הראשון מנגן את המילה, והיזהר לא לומר את המילה עצמה.

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

במשימה זו אתם מורידים ומפעילים את האפליקציה למתחילים ובודקים את הקוד.

שלב 1: מתחילים

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

שלב 2: הדרכה מפורטת על קוד

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

CurrentActivity.kt

הקובץ הזה מכיל רק קוד ברירת מחדל שנוצר על ידי תבנית.

res/layout/main_activity.xml

הקובץ הזה מכיל את הפריסה הראשית של האפליקציה. השדה NavHostFragment מארח את שאר קטעי הקוד כשהמשתמש מנווטים באפליקציה.

מקטעים של ממשק משתמש

הקוד למתחילים כולל שלושה מקטעים בשלושה חבילות שונות במסגרת החבילה com.example.android.guesstheword.screens:

  • title/TitleFragment למסך הכותרת
  • game/GameFragment למסך המשחק
  • score/ScoreFragment למסך הניקוד

Screen/title/TitleFragment.kt

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

Screen/game/GameFragment.kt

זהו הקטע העיקרי שבו מתבצעת רוב המשחק.

  • משתנים מוגדרים למילה הנוכחית ולציון הנוכחי.
  • ה-wordList שהוגדרו בשיטה resetList() היא רשימה לדוגמה של מילים שבהן יש להשתמש במשחק.
  • השיטה onSkip() היא הגורם המטפל בקליקים עבור הלחצן דילוג. היא מקטינה את הציון ב-1 ולאחר מכן מציגה את המילה הבאה בשיטה nextWord().
  • השיטה של onCorrect() היא ה-handler של הקליקים על לחצן Got It (הבנתי). שיטה זו מיושמת באופן דומה לשיטה onSkip(). ההבדל היחיד הוא שהשיטה הזו מוסיפה 1 לציון במקום לניכוי.

Screen/score/ניקודFragment.kt

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

res/Navigation/main_Navigation.xml

תרשים הניווט מראה איך מחברים את הקטעים דרך ניווט:

  • מקטע הטקסט, המשתמש יכול לנווט למקטע המשחק.
  • מקטע המשחק, המשתמש יכול לנווט למקטע הניקוד.
  • מקטע הניקוד, המשתמש יכול לנווט למקטע המשחק.

במשימה הזו תמצאו בעיות עם האפליקציה GuessTheWord Starter.

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

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

  • האפליקציה למתחילים לא שומרת ומשחזרת את מצב האפליקציה במהלך שינויי הגדרות, למשל כאשר הכיוון של המכשיר משתנה או כאשר האפליקציה נכבית ומופעלת מחדש.
    ניתן לפתור את הבעיה באמצעות הקריאה החוזרת (call) של onSaveInstanceState(). עם זאת, כשמשתמשים בשיטה onSaveInstanceState(), צריך לכתוב קוד נוסף כדי לשמור את המצב בחבילה, וליישם את הלוגיקה כדי לאחזר את המצב הזה. בנוסף, כמות הנתונים שאפשר לאחסן היא מינימלית.
  • מסך המשחק לא מנווט למסך הניקוד כאשר המשתמש מקיש על הלחצן סיום המשחק.

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

ארכיטקטורת אפליקציות

ארכיטקטורת אפליקציות היא דרך לעצב את האפליקציות' הכיתות והקשרים ביניהן, כך שהקוד מאורגן, מניב ביצועים טובים בתרחישים מסוימים וקל לעבוד איתו. בקבוצה של ארבעה מעבדות הקוד האלה, השיפורים שתבצעו באפליקציית GuessTheWord פועלים בהתאם להנחיות לארכיטקטורה של אפליקציות ל-Android, ואתם משתמשים ברכיבי ארכיטקטורה של Android. הארכיטקטורה של אפליקציית Android דומה לארכיטקטורה הMVVM (מודל-view-viewmodel).

אפליקציית GuessTheWord מבוססת על העיקרון של עיצוב החששות ומחולקת לכיתות, וכל כיתה מטפלת בבעיה נפרדת. במעבדה הראשונה הזו של השיעור, הכיתות שאיתם אתה עובד הן בקר ממשק משתמש, ViewModel וViewModelFactory.

בקר ממשק משתמש

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

בקוד למתחילים, GuessTheWord הם שלושת קטעי הקוד: GameFragment, ScoreFragment, ו-TitleFragment. בהתאם ל&מירכאות;העיקרון של העיצוב והעיצוב, GameFragment אחראי רק לציור רכיבי משחקים על המסך ולדעת מתי המשתמש מקיש על הלחצנים, ולא יותר. כשהמשתמש מקיש על לחצן, המידע הזה מועבר אל GameViewModel.

מודל תצוגה מפורטת

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

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

תצוגת דגם

ViewModelFactory יוצר אובייקט ViewModel עם, או עם פרמטרים של מבנה.

במעבדות מאוחרות יותר תוכלו ללמוד על רכיבי ארכיטקטורה אחרים של 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: משייכים את המשחק GameView לקטע הקוד של המשחק

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

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

  1. בכיתה GameFragment, מוסיפים שדה מסוג GameViewModel ברמה העליונה כמשתנה כיתה.
private lateinit var viewModel: GameViewModel

שלב 4: אתחול ה-ViewModel

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

איך ViewModelProvider פועל:

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

מפעילים את ViewModel באמצעות השיטה ViewModelProviders.of() כדי ליצור ViewModelProvider:

  1. בכיתה GameFragment, מפעילים את המשתנה viewModel. צריך להציב את הקוד הזה בתוך onCreateView(), אחרי שמגדירים את המשתנה הרלוונטי. משתמשים בשיטה ViewModelProviders.of() ומעבירים בהקשר של GameFragment ובסיווג GameViewModel.
  2. מעל לאתחול של האובייקט ViewModel, יש להוסיף הצהרת יומן כדי לתעד את הקריאה לשיטה 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 מופיעים ברישומים.

  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 כי הוא מכיל הפניות לתצוגות המפורטות. משתנה זה משמש לצורך ניפוח הפריסה, הגדרת פונקציות ה-click listener והצגת הנתונים על המסך - בקטע האחריות.
  2. העברה של השיטות resetList() ו-nextWord(). השיטות האלה קובעות איזו מילה תוצג על המסך.
  3. מתוך השיטה onCreateView(), יש להעביר את הקריאות לשיטה resetList() אל nextWord() ולבלוק init של GameViewModel.

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

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

בינתיים, יש להשתמש בשיטות זהות בשני המקומות:

  1. מעתיקים את השיטות onSkip() ו-onCorrect() מGameFragment אל GameViewModel.
  2. ב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: מעדכנים את ההפניות לרכיבי handler של קליקים ולשדות של נתונים ב-GameFragment

  1. בGameFragment, יש לעדכן את השיטות onSkip() ו-onCorrect(). עליך להסיר את הקוד כדי לעדכן את הציון, ולהשתמש במקום זאת בשיטות המתאימות onSkip() ו-onCorrect() בתאריך viewModel.
  2. מכיוון שהעברת את השיטה nextWord() ל-ViewModel, השיטות האלה מציגות את הנתונים במסך.
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. בונים את האפליקציה ומוודאים שאין שגיאות. אם נמצאו שגיאות, יש לנקות את הפרויקט ולבנות אותו מחדש.
  3. מפעילים את האפליקציה ומשחקים במשחק באמצעות כמה מילים. כשמסובבים את המסך, צריך לסובב את המכשיר. שימו לב שהציון הנוכחי והמילה הנוכחית נשמרים אחרי שינוי הכיוון.

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

במשימה הזו מטמיעים את מאזין הקליק על הלחצן סיום המשחק.

  1. ב-GameFragment, יש להוסיף שיטה שנקראת onEndGame(). תתבצע קריאה לשיטת onEndGame() כשהמשתמש מקיש על הלחצן סיום המשחק.
private fun onEndGame() {
   }
  1. בGameFragment, בתוך השיטה onCreateView(), מחפשים את הקוד שמגדיר את פונקציות הקליק עבור הלחצנים Got It ו-דילוג. מתחת לשתי השורות האלה, מגדירים לחצן להאזנה בלחיצה על הלחצן End Game (סיום המשחק). משתמשים במשתנה המחייב, binding. בתוך הפונקציה event listener, יש להתקשר לשיטה onEndGame().
binding.endGameButton.setOnClickListener { onEndGame() }
  1. ב-GameFragment, מוסיפים שיטה שנקראת gameFinished() כדי לנווט באפליקציה למסך הניקוד. אפשר להעביר את הציון כארגומנט באמצעות ארוחות בטוחות.
/**
* 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. בשיטה onEndGame(), יש להתקשר לשיטה gameFinished().
private fun onEndGame() {
   gameFinished()
}
  1. מפעילים את האפליקציה, משחקים במשחק ועוברים על כמה מילים. מקישים על הלחצן סיום המשחק. שימו לב שהאפליקציה מנווטת למסך הניקוד, אבל הציון הסופי לא מוצג. ניתן לפתור את הבעיה במשימה הבאה.

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

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

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

  1. במסגרת החבילה score, יש ליצור כיתה חדשה מ-Kotlin שנקראת ScoreViewModel. הכיתה הזו תהיה ViewModel עבור קטע הקוד של הציונים.
  2. הארכת המחלקה ScoreViewModel מ-ViewModel. הוספת פרמטר constructor לציון הסופי. הוספה של בלוק 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, עליך ליצור כיתה נוספת מ-Kotlin שנקראת ScoreViewModelFactory. הכיתה הזו תשמש ליצירת אובייקט ScoreViewModel.
  2. הארכה של הכיתה ב-ScoreViewModelFactory מ-ViewModelProvider.Factory. מוסיפים פרמטר constructor לציון הסופי.
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. קוראים לשיטה 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 שמכילים נתונים עבורם:

בקר ממשק משתמש

מודל צפייה

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

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

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

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

מכיל קוד להצגת נתונים, וקוד אירוע-משתמש כגון פונקציות מסוג listener של קליקים.

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

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

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

מכיל תצוגות.

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

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

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

קורס אוניברסיטה:

התיעוד של מפתח Android:

אחר:

בקטע הזה מפורטות מטלות שיעורי בית אפשריות לתלמידים שעובדים עם קוד Lab הזה, במסגרת קורס בהדרכת מורה. למורה יש אפשרות לבצע את הפעולות הבאות:

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

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

אם אתם עובדים בעצמכם על שיעור הקוד הזה, אתם מוזמנים להשתמש במטלות שיעורי הבית האלה כדי לבחון את הידע שלכם.

מענה על השאלות האלה

שאלה 1

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

  • ViewModel
  • LiveData
  • Fragment
  • Activity

שאלה 2

ViewModel לא יכול לכלול אף הפניה אל מקטעים, פעילויות או צפיות. נכון או לא נכון?

  • True
  • לא נכון

שאלה 3

מתי ViewModel נחרב?

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

שאלה 4

למה מיועד הממשק ViewModelFactory?

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

התחלת השיעור הבא: 5.2: LiveData וצופים בשידור חי

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