‫Android Kotlin Fundamentals 07.1: RecyclerView fundamentals

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

מבוא

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

מה שכדאי לדעת

חשוב שתכירו את:

  • בניית ממשק משתמש (UI) בסיסי באמצעות פעילות, רכיבים ותצוגות.
  • ניווט בין פרגמנטים ושימוש ב-safeArgs כדי להעביר נתונים בין פרגמנטים.
  • שימוש במודלים של תצוגה, במפעלים של מודלים של תצוגה, בטרנספורמציות וב-LiveData ובאובייקטים שלהם.
  • יצירת מסד נתונים Room, יצירת DAO והגדרת ישויות.
  • שימוש ב-coroutines למשימות שקשורות למסד נתונים ולמשימות אחרות לטווח ארוך.

מה תלמדו

  • איך משתמשים ב-RecyclerView עם Adapter ו-ViewHolder כדי להציג רשימת פריטים.

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

  • משנים את האפליקציה TrackMySleepQuality מהשיעור הקודם כך שתשתמש ב-RecyclerView כדי להציג נתונים של איכות השינה.

ב-codelab הזה תבנו את החלק RecyclerView באפליקציה שעוקבת אחרי איכות השינה. האפליקציה משתמשת במסד נתונים של Room כדי לאחסן את נתוני השינה לאורך זמן.

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

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

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

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

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

כדי לתמוך בכל תרחישי השימוש האלה, מערכת Android מספקת את הווידג'ט RecyclerView.

היתרון הגדול ביותר של RecyclerView הוא היעילות הרבה שלו ברשימות גדולות:

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

בסדרת התמונות הבאה אפשר לראות שתצוגה אחת התמלאה בנתונים, ABC. אחרי שהתצוגה הזו יוצאת מהמסך, RecyclerView משתמש מחדש בתצוגה לנתונים חדשים, XYZ.

דפוס המתאם

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

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

הטמעה של RecyclerView

כדי להציג את הנתונים בRecyclerView, צריך את החלקים הבאים:

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

במשימה הזו מוסיפים RecyclerView לקובץ הפריסה ומגדירים Adapter כדי לחשוף את נתוני השינה ל-RecyclerView.

שלב 1: מוסיפים RecyclerView עם LayoutManager

בשלב הזה, מחליפים את ScrollView ב-RecyclerView בקובץ fragment_sleep_tracker.xml.

  1. מורידים את האפליקציה RecyclerViewFundamentals-Starter מ-GitHub.
  2. מבצעים Build ומריצים את האפליקציה. שימו לב איך הנתונים מוצגים כטקסט פשוט.
  3. פותחים את קובץ הפריסה fragment_sleep_tracker.xml בכרטיסייה Design ב-Android Studio.
  4. בחלונית Component Tree, מוחקים את ScrollView. הפעולה הזו תמחק גם את TextView שנמצא בתוך ScrollView.
  5. בחלונית Palette, גוללים ברשימת סוגי הרכיבים בצד ימין עד שמגיעים אל Containers ובוחרים אותה.
  6. גוררים RecyclerView מהחלונית Palette לחלונית Component Tree. ממקמים את RecyclerView בתוך ConstraintLayout.

  1. אם נפתח חלון דו-שיח עם שאלה אם רוצים להוסיף יחסי תלות, לוחצים על אישור כדי לאפשר ל-Android Studio להוסיף את יחסי התלות recyclerview לקובץ Gradle. יכול להיות שיעברו כמה שניות עד שהאפליקציה תסתנכרן.

  1. פותחים את קובץ המודול build.gradle, גוללים לסוף ורושמים את התלות החדשה, שדומה לקוד שבהמשך:
implementation 'androidx.recyclerview:recyclerview:1.0.0'
  1. מעבר חזרה אל fragment_sleep_tracker.xml.
  2. בכרטיסייה Text (טקסט), מחפשים את הקוד RecyclerView שמופיע בהמשך:
<androidx.recyclerview.widget.RecyclerView
   android:layout_width="match_parent"
   android:layout_height="match_parent" />
  1. אני רוצה לתת לRecyclerView id של sleep_list.
android:id="@+id/sleep_list"
  1. ממקמים את RecyclerView כך שימלא את החלק הנותר של המסך בתוך ConstraintLayout. כדי לעשות את זה, מגבילים את החלק העליון של RecyclerView ללחצן התחלה, את החלק התחתון ללחצן ניקוי, וכל צד לאלמנט האב. מגדירים את הרוחב והגובה של הפריסה ל-0dp בכלי לעריכת פריסות או ב-XML, באמצעות הקוד הבא:
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toTopOf="@+id/clear_button"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/stop_button"
  1. מוסיפים מנהל פריסה ל-XML של RecyclerView. לכל RecyclerView צריך להיות מנהל פריסה שמגדיר איך למקם פריטים ברשימה. מערכת Android מספקת LinearLayoutManager, שמוצגים בה כברירת מחדל הפריטים ברשימה אנכית של שורות ברוחב מלא.
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
  1. עוברים לכרטיסייה עיצוב ורואים שהאילוצים שנוספו גרמו להרחבת RecyclerView כדי למלא את השטח הזמין.

שלב 2: יוצרים את פריסת הפריט ברשימה ואת מחזיק תצוגת הטקסט

התג RecyclerView הוא רק מאגר. בשלב הזה יוצרים את הפריסה והתשתית של הפריטים שיוצגו בתוך RecyclerView.

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

  1. יוצרים קובץ פריסה בשם text_item_view.xml. לא משנה מה משמש כרכיב הבסיסי, כי אתם תחליפו את קוד התבנית.
  2. ב-text_item_view.xml, מוחקים את כל הקוד שמופיע.
  3. מוסיפים TextView עם ריווח פנימי של 16dp בהתחלה ובסוף, וגודל טקסט של 24sp. הרוחב יתאים לרוחב של רכיב האב, והגובה יתאים לגובה של התוכן. מכיוון שהתצוגה הזו מוצגת בתוך RecyclerView, לא צריך למקם את התצוגה בתוך ViewGroup.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:textSize="24sp"
    android:paddingStart="16dp"
    android:paddingEnd="16dp"
    android:layout_width="match_parent"       
    android:layout_height="wrap_content" />
  1. פתיחת Util.kt. גוללים לסוף ומוסיפים את ההגדרה שמופיעה בהמשך, שיוצרת את המחלקה TextItemViewHolder. מציבים את הקוד בתחתית הקובץ, אחרי הסוגר המסולסל האחרון. הקוד מוכנס ל-Util.kt כי מחזיק התצוגה הזה הוא זמני, ואתם מחליפים אותו בהמשך.
class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
  1. אם תתבקשו, מייבאים את android.widget.TextView ואת androidx.recyclerview.widget.RecyclerView.

שלב 3: יצירת SleepNightAdapter

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

  1. בsleeptracker package, יוצרים מחלקה חדשה של Kotlin בשם SleepNightAdapter.
  2. מפעילים את האפשרות SleepNightAdapter הארכת השיעור RecyclerView.Adapter. המחלקות נקראות SleepNightAdapter כי הן מתאימות אובייקט SleepNight למשהו שאפשר להשתמש בו ב-RecyclerView. המתאם צריך לדעת באיזה מחזיק תצוגה להשתמש, ולכן מעבירים את TextItemViewHolder. מייבאים את הרכיבים הנדרשים כשמוצגת בקשה, ואז מוצגת שגיאה כי יש שיטות חובה שצריך להטמיע.
class SleepNightAdapter: RecyclerView.Adapter<TextItemViewHolder>() {}
  1. ברמה העליונה של SleepNightAdapter, יוצרים משתנה listOf SleepNight כדי לאחסן את הנתונים.
var data =  listOf<SleepNight>()
  1. ב-SleepNightAdapter, מחליפים את getItemCount() כדי להחזיר את הגודל של רשימת הלילות של השינה ב-data. ה-RecyclerView צריך לדעת כמה פריטים יש למתאם כדי להציג אותם, והוא עושה זאת על ידי קריאה ל-getItemCount().
override fun getItemCount() = data.size
  1. ב-SleepNightAdapter, מחליפים את הפונקציה onBindViewHolder(), כמו שמוצג בהמשך. ‫

    הפונקציה onBindViewHolder()מופעלת על ידי RecyclerView כדי להציג את הנתונים של פריט רשימה אחד במיקום שצוין. לכן, השיטה onBindViewHolder() מקבלת שני ארגומנטים: מחזיק תצוגה ומיקום הנתונים לקשירה. באפליקציה הזו, המחזיק הוא TextItemViewHolder והמיקום הוא המיקום ברשימה.
override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {
}
  1. בתוך onBindViewHolder(), יוצרים משתנה לפריט אחד במיקום נתון בנתונים.
 val item = data[position]
  1. ל-ViewHolder שיצרתם יש מאפיין שנקרא textView. בתוך onBindViewHolder(), מגדירים את text של textView למספר איכות השינה. הקוד הזה מציג רק רשימה של מספרים, אבל הדוגמה הפשוטה הזו מאפשרת לראות איך המתאם מעביר את הנתונים למחזיק התצוגה ולמסך.
holder.textView.text = item.sleepQuality.toString()
  1. ב-SleepNightAdapter, מחליפים את onCreateViewHolder() ומטמיעים אותו. הפונקציה הזו מופעלת כש-RecyclerView צריך מחזיק תצוגה כדי לייצג פריט. ‫

    הפונקציה הזו מקבלת שני פרמטרים ומחזירה ViewHolder. הפרמטר parent, שהוא קבוצת התצוגה שמכילה את מחזיק התצוגה, הוא תמיד RecyclerView. הפרמטר viewType משמש כשיש כמה תצוגות באותו RecyclerView. לדוגמה, אם מציבים רשימה של תצוגות טקסט, תמונה וסרטון באותו RecyclerView, הפונקציה onCreateViewHolder() צריכה לדעת באיזה סוג תצוגה להשתמש.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextItemViewHolder {
}
  1. ב-onCreateViewHolder(), יוצרים מופע של LayoutInflater.

    הכלי layout inflater יודע איך ליצור תצוגות מפריסות XML. השדה context מכיל מידע על האופן שבו צריך להגדיל את התצוגה. במתאם של תצוגת recycler, תמיד מעבירים את ההקשר של קבוצת התצוגה parent, שהיא RecyclerView.
val layoutInflater = LayoutInflater.from(parent.context)
  1. ב-onCreateViewHolder(), יוצרים את view על ידי בקשה מ-layoutinflater להגדיל אותו.

    מעבירים את פריסת ה-XML לתצוגה ואת קבוצת התצוגה parent לתצוגה. הארגומנט השלישי, בוליאני, הוא attachToRoot. הארגומנט הזה צריך להיות false, כי RecyclerView מוסיף את הפריט הזה להיררכיית התצוגה כשמגיע הזמן.
val view = layoutInflater
       .inflate(R.layout.text_item_view, parent, false) as TextView
  1. בonCreateViewHolder(), החזרת TextItemViewHolder שבוצעה באמצעות view.
return TextItemViewHolder(view)
  1. המתאם צריך לעדכן את RecyclerView כשdata משתנה, כי RecyclerView לא יודע כלום על הנתונים. הוא יודע רק על מחזיקי התצוגה שהמתאם מעביר אליו.

    כדי להודיע ל-RecyclerView מתי הנתונים שהוא מציג השתנו, מוסיפים פונקציית setter מותאמת אישית למשתנה data שנמצא בחלק העליון של המחלקה SleepNightAdapter. בפונקציית ההגדרה, נותנים למאפיין data ערך חדש, ואז קוראים לפונקציה notifyDataSetChanged() כדי להפעיל את הציור מחדש של הרשימה עם הנתונים החדשים.
var data =  listOf<SleepNight>()
   set(value) {
       field = value
       notifyDataSetChanged()
   }

שלב 4: מעדכנים את RecyclerView לגבי ה-Adapter

ה-RecyclerView צריך לדעת על המתאם שבו הוא משתמש כדי לקבל מחזיקי תצוגה.

  1. פתיחת SleepTrackerFragment.kt.
  2. ב-onCreateview(), יוצרים מתאם. ממקמים את הקוד הזה אחרי יצירת המודל ViewModel ולפני ההצהרה return.
val adapter = SleepNightAdapter()
  1. משייכים את adapter אל RecyclerView.
binding.sleepList.adapter = adapter
  1. מנקים את הפרויקט ובונים אותו מחדש כדי לעדכן את האובייקט binding.

    אם עדיין מופיעות שגיאות שקשורות ל-binding.sleepList או ל-binding.FragmentSleepTrackerBinding, צריך לבטל את התוקף של מטמונים ולהפעיל מחדש. (בוחרים באפשרות File > Invalidate Caches / Restart).

    אם מריצים את האפליקציה עכשיו, לא יופיעו שגיאות, אבל לא יוצגו נתונים כשתקישו על Start ואז על Stop.

שלב 5: העברת נתונים למתאם

עד עכשיו יש לכם מתאם ודרך להעביר נתונים מהמתאם אל RecyclerView. עכשיו צריך להעביר נתונים מהמערכת ViewModel למתאם.

  1. פתיחת SleepTrackerViewModel.
  2. מחפשים את המשתנה nights שבו מאוחסנים כל נתוני הלילות של השינה, שהם הנתונים שיוצגו. המשתנה nights מוגדר על ידי קריאה של getAllNights() במסד הנתונים.
  3. מסירים את private מ-nights, כי תיצור משתנה מסוג observer שצריך לגשת למשתנה הזה. ההצהרה שלכם צריכה להיראות כך:
val nights = database.getAllNights()
  1. בחבילה database, פותחים את SleepDatabaseDao.
  2. מחפשים את הפונקציה getAllNights(). שימו לב שהפונקציה הזו מחזירה רשימה של ערכי SleepNight בתור LiveData. המשמעות היא שהמשתנה nights מכיל את הערך LiveData שמתעדכן על ידי Room, ואפשר לעקוב אחרי nights כדי לדעת מתי הוא משתנה.
  3. פתיחת SleepTrackerFragment.
  4. ב-onCreateView(), מתחת ליצירה של adapter, יוצרים observer במשתנה nights.

    אם מספקים את viewLifecycleOwner של הפראגמנט כבעלים של מחזור החיים, אפשר לוודא שהאובייקט הזה של Observer פעיל רק כש-RecyclerView מוצג במסך.
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   })
  1. בתוך האובייקט observer, בכל פעם שמתקבל ערך שאינו null (עבור nights), צריך להקצות את הערך ל-data של המתאם. זהו הקוד המלא של האובייקט observer והגדרת הנתונים:
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   it?.let {
       adapter.data = it
   }
})
  1. כותבים ומריצים את הקוד.

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

שלב 6: בודקים איך מחזרים את מחזיקי התצוגה

RecyclerView recycles view holders, which means that it reuses them. כשמגללים תצוגה אל מחוץ למסך, RecyclerView משתמש מחדש בתצוגה עבור התצוגה שעומדת להיכנס למסך.

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

לדוגמה, אפשר להגדיר את צבע הטקסט לאדום ב-view holders שמכילים דירוגי איכות שקטנים מ-1 או שווים ל-1 ומייצגים שינה לא טובה.

  1. בכיתה SleepNightAdapter, מוסיפים את הקוד הבא בסוף onBindViewHolder().
if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
}
  1. מפעילים את האפליקציה.
  2. אם מוסיפים נתונים של שינה באיכות נמוכה, המספר יהיה אדום.
  3. מוסיפים דירוגים גבוהים לאיכות השינה עד שמופיע על המסך מספר אדום גבוה.

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

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

    כששני התנאים מוגדרים באופן מפורש, מחזיק התצוגה ישתמש בצבע הטקסט הנכון לכל פריט.
if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
} else {
   // reset
   holder.textView.setTextColor(Color.BLACK) // black
}
  1. מריצים את האפליקציה, והמספרים צריכים להיות בצבע הנכון.

מעולה! עכשיו יש לכם RecyclerView בסיסי עם כל הפונקציות.

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

התג הפשוט ViewHolder שהוספתם ל-Util.kt פשוט עוטף את TextView ב-TextItemViewHolder.

class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)

אז למה RecyclerView לא משתמש ב-TextView ישירות? שורה אחת של קוד מספקת הרבה פונקציונליות. ViewHolder מתאר תצוגת פריט ומטא-נתונים לגבי המיקום שלו בתוך RecyclerView. ‫RecyclerView מסתמך על הפונקציונליות הזו כדי למקם את התצוגה בצורה נכונה בזמן הגלילה ברשימה, וכדי לבצע פעולות מעניינות כמו הנפשת תצוגות כשמוסיפים או מסירים פריטים ב-Adapter.

אם RecyclerView צריך לגשת לתצוגות המפורטות שמאוחסנות ב-ViewHolder, הוא יכול לעשות זאת באמצעות המאפיין itemView של מחזיק התצוגה המפורטת. ‫RecyclerView משתמש ב-itemView כשהוא קושר פריט לתצוגה במסך, כשהוא מצייר קישוטים מסביב לתצוגה כמו גבול, וכשהוא מטמיע נגישות.

שלב 1: יוצרים את פריסת הפריטים

בשלב הזה יוצרים את קובץ הפריסה של פריט אחד. הפריסה מורכבת מ-ConstraintLayout עם ImageView לאיכות השינה, מ-TextView למשך השינה ומ-TextView לאיכות כטקסט. מכיוון שכבר יצרת פריסות בעבר, אפשר להעתיק ולהדביק את קוד ה-XML שסופק.

  1. יוצרים קובץ חדש של משאב פריסה ונותנים לו את השם list_item_sleep_night.
  2. מחליפים את כל הקוד בקובץ בקוד שלמטה. לאחר מכן, כדאי להכיר את הפריסה שיצרתם.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="wrap_content">

   <ImageView
       android:id="@+id/quality_image"
       android:layout_width="@dimen/icon_size"
       android:layout_height="60dp"
       android:layout_marginStart="16dp"
       android:layout_marginTop="8dp"
       android:layout_marginBottom="8dp"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       tools:srcCompat="@drawable/ic_sleep_5" />

   <TextView
       android:id="@+id/sleep_length"
       android:layout_width="0dp"
       android:layout_height="20dp"
       android:layout_marginStart="8dp"
       android:layout_marginTop="8dp"
       android:layout_marginEnd="16dp"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toEndOf="@+id/quality_image"
       app:layout_constraintTop_toTopOf="@+id/quality_image"
       tools:text="Wednesday" />

   <TextView
       android:id="@+id/quality_string"
       android:layout_width="0dp"
       android:layout_height="20dp"
       android:layout_marginTop="8dp"
       app:layout_constraintEnd_toEndOf="@+id/sleep_length"
       app:layout_constraintHorizontal_bias="0.0"
       app:layout_constraintStart_toStartOf="@+id/sleep_length"
       app:layout_constraintTop_toBottomOf="@+id/sleep_length"
       tools:text="Excellent!!!" />
</androidx.constraintlayout.widget.ConstraintLayout>
  1. עוברים לכרטיסייה Design (עיצוב) ב-Android Studio. בתצוגת העיצוב, הפריסה נראית כמו צילום המסך שמופיע בצד ימין למטה. בתצוגת התוכנית, הוא נראה כמו צילום המסך בצד שמאל.

שלב 2: יצירת ViewHolder

  1. פתיחת SleepNightAdapter.kt.
  2. יוצרים כיתה בתוך SleepNightAdapter שנקראת ViewHolder וגורמים לה להרחיב את RecyclerView.ViewHolder.
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){}
  1. בתוך ViewHolder, מקבלים הפניות לתצוגות המפורטות. צריך הפניה לתצוגות שה-ViewHolder הזה יעדכן. בכל פעם שמבצעים קישור של ViewHolder, צריך לגשת לתמונה ולשני תצוגות הטקסט. (בהמשך תמירו את הקוד הזה לשימוש בקשירת נתונים).
val sleepLength: TextView = itemView.findViewById(R.id.sleep_length)
val quality: TextView = itemView.findViewById(R.id.quality_string)
val qualityImage: ImageView = itemView.findViewById(R.id.quality_image)

שלב 3: שימוש ב-ViewHolder ב-SleepNightAdapter

  1. בהגדרה של SleepNightAdapter, במקום TextItemViewHolder, משתמשים ב-SleepNightAdapter.ViewHolder שיצרתם עכשיו.
class SleepNightAdapter: RecyclerView.Adapter<SleepNightAdapter.ViewHolder>() {

עדכון onCreateViewHolder():

  1. משנים את החתימה של onCreateViewHolder() כדי להחזיר את ViewHolder.
  2. משנים את ה-layout inflator כך שישתמש במשאב הפריסה הנכון, list_item_sleep_night.
  3. מסירים את ההעברה אל TextView.
  4. במקום להחזיר TextItemViewHolder, מחזירים ViewHolder.

    הנה פונקציית onCreateViewHolder() המעודכנת:
    override fun onCreateViewHolder(
            parent: ViewGroup, viewType: Int): ViewHolder {
        val layoutInflater = 
            LayoutInflater.from(parent.context)
        val view = layoutInflater
                .inflate(R.layout.list_item_sleep_night, 
                         parent, false)
        return ViewHolder(view)
    }

עדכון onBindViewHolder():

  1. משנים את החתימה של onBindViewHolder() כך שהפרמטר holder יהיה ViewHolder במקום TextItemViewHolder.
  2. בתוך onBindViewHolder(), מוחקים את כל הקוד, מלבד ההגדרה של item.
  3. מגדירים val res שמכיל הפניה אל resources של התצוגה הזו.
val res = holder.itemView.context.resources
  1. מגדירים את הטקסט של תצוגת הטקסט sleepLength למשך הזמן. מעתיקים את הקוד שבהמשך, שקורא לפונקציית עיצוב שמסופקת עם קוד ההתחלה.
holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
  1. הפעולה הזו מחזירה שגיאה, כי צריך להגדיר את convertDurationToFormatted(). פותחים את Util.kt ומבטלים את ההערה של הקוד והייבוא שמשויך אליו. (בוחרים באפשרות קוד > הוספת הערות בשורות).
  2. חוזרים אל onBindViewHolder() ומשתמשים ב-convertNumericQualityToString() כדי להגדיר את האיכות.
holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
  1. יכול להיות שתצטרכו לייבא את הפונקציות האלה באופן ידני.
import com.example.android.trackmysleepquality.convertDurationToFormatted
import com.example.android.trackmysleepquality.convertNumericQualityToString
  1. הגדרת הסמל הנכון לאיכות. סמל ic_sleep_active חדש מסופק לכם בקוד ההתחלתי.
holder.qualityImage.setImageResource(when (item.sleepQuality) {
   0 -> R.drawable.ic_sleep_0
   1 -> R.drawable.ic_sleep_1
   2 -> R.drawable.ic_sleep_2
   3 -> R.drawable.ic_sleep_3
   4 -> R.drawable.ic_sleep_4
   5 -> R.drawable.ic_sleep_5
   else -> R.drawable.ic_sleep_active
})
  1. הנה הפונקציה המעודכנת onBindViewHolder(), שקובעת את כל הנתונים של ViewHolder:
   override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = data[position]
        val res = holder.itemView.context.resources
        holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
        holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
        holder.qualityImage.setImageResource(when (item.sleepQuality) {
            0 -> R.drawable.ic_sleep_0
            1 -> R.drawable.ic_sleep_1
            2 -> R.drawable.ic_sleep_2
            3 -> R.drawable.ic_sleep_3
            4 -> R.drawable.ic_sleep_4
            5 -> R.drawable.ic_sleep_5
            else -> R.drawable.ic_sleep_active
        })
    }
  1. מריצים את האפליקציה. המסך אמור להיראות כמו בצילום המסך שלמטה, עם סמל איכות השינה, וגם טקסט של משך השינה ואיכות השינה.

השלמתם את RecyclerView! למדתם איך להטמיע Adapter ו-ViewHolder, ואיך לשלב ביניהם כדי להציג רשימה עם RecyclerView Adapter.

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

באפליקציה שנמצאת בייצור, יכול להיות שיהיו כמה מחזיקי תצוגות, מתאמים מורכבים יותר וכמה מפתחים שמבצעים שינויים. כדאי לבנות את הקוד כך שכל מה שקשור ל-view holder יהיה רק ב-view holder.

שלב 1: שינוי מבנה הקוד של onBindViewHolder()

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

  1. ב-SleepNightAdapter, ב-onBindViewHolder(), בוחרים הכול חוץ מההצהרה כדי להצהיר על המשתנה item.
  2. לוחצים לחיצה ימנית ובוחרים באפשרות Refactor > Extract > Function (שינוי מבנה > חילוץ > פונקציה).
  3. נותנים לפונקציה את השם bind ומאשרים את הפרמטרים המוצעים. לוחצים על אישור.

    הפונקציה bind() ממוקמת מתחת ל-onBindViewHolder().
    private fun bind(holder: ViewHolder, item: SleepNight) {
        val res = holder.itemView.context.resources
        holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
        holder.quality.text = convertNumericQualityToString(item.sleepQuality, res)
        holder.qualityImage.setImageResource(when (item.sleepQuality) {
            0 -> R.drawable.ic_sleep_0
            1 -> R.drawable.ic_sleep_1
            2 -> R.drawable.ic_sleep_2
            3 -> R.drawable.ic_sleep_3
            4 -> R.drawable.ic_sleep_4
            5 -> R.drawable.ic_sleep_5
            else -> R.drawable.ic_sleep_active
        })
    }
  1. מציבים את הסמן על המילה holder של הפרמטר holder של bind(). מקישים על Alt+Enter (או על Option+Enter ב-Mac) כדי לפתוח את תפריט הכוונות. בוחרים באפשרות המרת הפרמטר למקבל כדי להמיר אותו לפונקציית הרחבה עם החתימה הבאה:
private fun ViewHolder.bind(item: SleepNight) {...}
  1. גוזרים את הפונקציה bind() ומדביקים אותה ב-ViewHolder.
  2. הופכים את bind() לגלוי לכולם.
  3. אם צריך, מייבאים את bind() למתאם.
  4. עכשיו שהיא נמצאת ב-ViewHolder, אפשר להסיר את החלק ViewHolder מהחתימה. זה הקוד הסופי של הפונקציה bind() במחלקה ViewHolder.
fun bind(item: SleepNight) {
   val res = itemView.context.resources
   sleepLength.text = convertDurationToFormatted(
           item.startTimeMilli, item.endTimeMilli, res)
   quality.text = convertNumericQualityToString(
           item.sleepQuality, res)
   qualityImage.setImageResource(when (item.sleepQuality) {
       0 -> R.drawable.ic_sleep_0
       1 -> R.drawable.ic_sleep_1
       2 -> R.drawable.ic_sleep_2
       3 -> R.drawable.ic_sleep_3
       4 -> R.drawable.ic_sleep_4
       5 -> R.drawable.ic_sleep_5
       else -> R.drawable.ic_sleep_active
   })
}

שלב 2: שינוי המבנה של onCreateViewHolder

השיטה onCreateViewHolder() במתאם מרחיבה כרגע את התצוגה ממשאב הפריסה של ViewHolder. עם זאת, האינפלציה לא קשורה למתאם, אלא לViewHolder. האינפלציה צריכה להתרחש בViewHolder.

  1. ב-onCreateViewHolder(), בוחרים את כל הקוד בגוף הפונקציה.
  2. לוחצים לחיצה ימנית ובוחרים באפשרות Refactor > Extract > Function (שינוי מבנה > חילוץ > פונקציה).
  3. נותנים לפונקציה את השם from ומאשרים את הפרמטרים המוצעים. לוחצים על אישור.
  4. מציבים את הסמן על שם הפונקציה from. מקישים על Alt+Enter (או על Option+Enter ב-Mac) כדי לפתוח את תפריט הכוונות.
  5. בוחרים באפשרות העברה לאובייקט משני. הפונקציה from() צריכה להיות באובייקט נלווה כדי שאפשר יהיה להפעיל אותה במחלקה ViewHolder, ולא להפעיל אותה במופע ViewHolder.
  6. מעבירים את האובייקט companion אל המחלקה ViewHolder.
  7. הופכים את from() לגלוי לכולם.
  8. ב-onCreateViewHolder(), משנים את ההצהרה return כך שתחזיר את התוצאה של הקריאה ל-from() במחלקה ViewHolder.

    השיטות onCreateViewHolder() ו-from() אחרי השלמתן צריכות להיראות כמו הקוד שבהמשך, והקוד צריך להיבנות ולפעול ללא שגיאות.
    override fun onCreateViewHolder(parent: ViewGroup, viewType: 
Int): ViewHolder {
        return ViewHolder.from(parent)
    }
companion object {
   fun from(parent: ViewGroup): ViewHolder {
       val layoutInflater = LayoutInflater.from(parent.context)
       val view = layoutInflater
               .inflate(R.layout.list_item_sleep_night, parent, false)
       return ViewHolder(view)
   }
}
  1. משנים את החתימה של המחלקה ViewHolder כך שהבונה יהיה פרטי. השיטה from() מחזירה עכשיו מופע חדש של ViewHolder, ולכן אין יותר סיבה להפעיל את בנאי המחלקה של ViewHolder.
class ViewHolder private constructor(itemView: View) : RecyclerView.ViewHolder(itemView){
  1. מריצים את האפליקציה. האפליקציה אמורה להיבנות ולרוץ כמו קודם, וזהו התוצאה הרצויה אחרי שינוי המבנה.

פרויקט Android Studio: ‏ RecyclerViewFundamentals

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

כדי להציג את הנתונים בRecyclerView, צריך את החלקים הבאים:

  • RecyclerView
    כדי ליצור מופע של RecyclerView, מגדירים רכיב <RecyclerView> בקובץ הפריסה.
  • LayoutManager
    RecyclerView משתמש ב-LayoutManager כדי לארגן את הפריסה של הפריטים ב-RecyclerView, למשל כדי לפרוס אותם ברשת או ברשימה לינארית.

    ב-<RecyclerView> בקובץ הפריסה, מגדירים את מאפיין app:layoutManager למנהל הפריסה (למשל LinearLayoutManager או GridLayoutManager).

    אפשר גם להגדיר את LayoutManager ל-RecyclerView באופן פרוגרמטי. (הטכניקה הזו מוסברת ב-codelab בהמשך).
  • פריסה של כל פריט
    יוצרים פריסה של פריט נתונים אחד בקובץ פריסה בפורמט XML.
  • מתאם
    יוצרים מתאם שמכין את הנתונים ומגדיר איך הם יוצגו בViewHolder. משייכים את המתאם אל RecyclerView. ‫

    כשמפעילים את RecyclerView, הוא משתמש במתאם כדי להבין איך להציג את הנתונים במסך.

    כדי להשתמש במתאם, צריך להטמיע את השיטות הבאות:
    getItemCount() כדי להחזיר את מספר הפריטים.
    onCreateViewHolder() כדי להחזיר את ViewHolder של פריט ברשימה.
    onBindViewHolder() כדי להתאים את הנתונים לתצוגות של פריט ברשימה.

  • ViewHolder
    ViewHolder מכיל את פרטי התצוגה של פריט אחד מפריסת הפריט.
  • השיטה onBindViewHolder() במתאם מתאימה את הנתונים לתצוגות. תמיד מבטלים את השיטה הזו. בדרך כלל, onBindViewHolder() מרחיב את הפריסה של פריט ומציב את הנתונים בתצוגות בפריסה.
  • מכיוון ש-RecyclerView לא יודע דבר על הנתונים, Adapter צריך לעדכן את RecyclerView כשהנתונים האלה משתנים. משתמשים ב-notifyDataSetChanged()כדי להודיע ל-Adapter שהנתונים השתנו.

קורס ב-Udacity:

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

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

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

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

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

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

שאלה 1

איך מוצגים הפריטים ב-RecyclerView? יש לבחור בכל האפשרויות הרלוונטיות.

‫▢ הצגת הפריטים בתצוגת רשימה או בתצוגת רשת.

‫▢ גלילה אנכית או אופקית.

‫▢ גלילה באלכסון במכשירים גדולים יותר, כמו טאבלטים.

‫▢ מאפשר פריסות בהתאמה אישית כשפריסה של רשימה או רשת לא מספיקה לתרחיש השימוש.

שאלה 2

מה היתרונות של השימוש ב-RecyclerView? יש לבחור בכל האפשרויות הרלוונטיות.

‫▢ הצגה יעילה של רשימות גדולות.

‫▢ עדכון אוטומטי של הנתונים.

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

‫▢ שימוש חוזר בתצוגה שגוללת מחוץ למסך כדי להציג את הפריט הבא שגולל במסך.

שאלה 3

מהן כמה מהסיבות לשימוש במתאמים? יש לבחור בכל האפשרויות הרלוונטיות.

▢ הפרדה בין נושאים מקלה על שינוי קוד ובדיקתו.

‫▢ RecyclerView לא תלוי בנתונים שמוצגים.

‫▢ שכבות עיבוד הנתונים לא צריכות להתייחס לאופן שבו הנתונים יוצגו.

‫▢ האפליקציה תפעל מהר יותר.

שאלה 4

אילו מהמשפטים הבאים נכונים לגבי ViewHolder? יש לבחור בכל האפשרויות הרלוונטיות.

‫▢ הפריסה ViewHolder מוגדרת בקובצי פריסה של XML.

‫▢ יש ViewHolder אחד לכל יחידת נתונים בקבוצת הנתונים.

▢ אפשר להוסיף יותר מ-ViewHolder אחד ב-RecyclerView.

‫▢ Adapter קושר נתונים ל-ViewHolder.

עוברים לשיעור הבא: 7.2: DiffUtil ואיגוד נתונים עם RecyclerView