‫Android Kotlin Fundamentals 08.2: טעינה והצגה של תמונות מהאינטרנט

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

מבוא

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

מה שכדאי לדעת

  • איך יוצרים קטעים ומשתמשים בהם.
  • איך משתמשים ברכיבי ארכיטקטורה, כולל מודלים של תצוגה, מפעלים של מודלים של תצוגה, טרנספורמציות ו-LiveData.
  • איך לאחזר JSON משירות אינטרנט מסוג REST ולנתח את הנתונים לאובייקטים של Kotlin באמצעות הספריות Retrofit ו-Moshi.
  • איך יוצרים פריסת רשת באמצעות התג RecyclerView.
  • איך פועלים Adapter,‏ ViewHolder ו-DiffUtil

מה תלמדו

  • איך משתמשים בספריית Glide כדי לטעון ולהציג תמונה מכתובת URL באינטרנט.
  • איך משתמשים ב-RecyclerView ובמתאם רשת כדי להציג רשת של תמונות.
  • איך לטפל בשגיאות פוטנציאליות במהלך ההורדה וההצגה של התמונות.

מה עושים

  • משנים את האפליקציה MarsRealEstate כדי לקבל את כתובת ה-URL של התמונה מנתוני הנכס במאדים, ומשתמשים ב-Glide כדי לטעון ולהציג את התמונה.
  • הוספת אנימציה של טעינה וסמל שגיאה לאפליקציה.
  • משתמשים ב-RecyclerView כדי להציג רשת של תמונות של נכסים במאדים.
  • מוסיפים סטטוס וטיפול בשגיאות ל-RecyclerView.

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

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

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

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

בעיקרון, כדי להשתמש ב-Glide צריך שני דברים:

  • כתובת ה-URL של התמונה שרוצים לטעון ולהציג.
  • אובייקט ImageView להצגת התמונה.

במשימה הזו תלמדו איך להשתמש ב-Glide כדי להציג תמונה אחת משירות האינטרנט של חברת הנדל"ן. אתם מציגים את התמונה שמייצגת את הנכס הראשון של מארס ברשימת הנכסים ששירות האינטרנט מחזיר. צילומי המסך שלפני ואחרי:

שלב 1: הוספת תלות ב-Glide

  1. פותחים את אפליקציית MarsRealEstate מה-codelab האחרון. (אם האפליקציה לא מותקנת במכשיר, אפשר להוריד את MarsRealEstateNetwork מכאן).
  2. מריצים את האפליקציה כדי לראות מה היא עושה. (מוצגים פרטי טקסט של נכס שזמין באופן היפותטי במאדים).
  3. פותחים את build.gradle (Module: app).
  4. בקטע dependencies, מוסיפים את השורה הזו לספריית Glide:
implementation "com.github.bumptech.glide:glide:$version_glide"


שימו לב שמספר הגרסה כבר מוגדר בנפרד בקובץ הפרויקט של Gradle.

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

שלב 2: עדכון מודל התצוגה

אחר כך מעדכנים את המחלקה OverviewViewModel כך שתכלול נתונים בזמן אמת עבור נכס יחיד של Mars.

  1. פתיחת overview/OverviewViewModel.kt. מתחת ל-LiveData של _response, מוסיפים נתונים חיים פנימיים (ניתנים לשינוי) וחיצוניים (לא ניתנים לשינוי) לאובייקט MarsProperty יחיד.

    כשמתבקשים, מייבאים את הכיתה MarsProperty (com.example.android.marsrealestate.network.MarsProperty).
private val _property = MutableLiveData<MarsProperty>()

val property: LiveData<MarsProperty>
   get() = _property
  1. בשיטה getMarsRealEstateProperties(), מחפשים את השורה בתוך הבלוק try/catch {} שבה מוגדר _response.value למספר הנכסים. מוסיפים את הבדיקה שמוצגת למטה. אם אובייקטים של MarsProperty זמינים, הבדיקה הזו מגדירה את הערך של _property LiveData לנכס הראשון ב-listResult.
if (listResult.size > 0) {   
    _property.value = listResult[0]
}

בלוק try/catch {} המלא נראה עכשיו כך:

try {
   var listResult = getPropertiesDeferred.await()
   _response.value = "Success: ${listResult.size} Mars properties retrieved"
   if (listResult.size > 0) {      
       _property.value = listResult[0]
   }
 } catch (e: Exception) {
    _response.value = "Failure: ${e.message}"
 }
  1. פותחים את הקובץ res/layout/fragment_overview.xml. באמצעות האלמנט <TextView>, משנים את android:text כך שיקשר לרכיב imgSrcUrl של property LiveData:
android:text="@{viewModel.property.imgSrcUrl}"
  1. מריצים את האפליקציה. ב-TextView מוצגת רק כתובת ה-URL של התמונה בנכס הראשון של מאדים. עד עכשיו הגדרתם רק את מודל התצוגה ואת הנתונים הפעילים של כתובת ה-URL הזו.

שלב 3: יוצרים מתאם של Binding וקוראים ל-Glide

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

  1. פתיחת BindingAdapters.kt. הקובץ הזה יכיל את מתאמי הקישור שבהם משתמשים באפליקציה.
  2. יוצרים פונקציה bindImage() שמקבלת ImageView ו-String כפרמטרים. מוסיפים הערה לפונקציה עם @BindingAdapter. ההערה @BindingAdapter מציינת ל-Data Binding שרוצים שהמתאם הזה יופעל כשפריט XML כולל את המאפיין imageUrl.

    מייבאים את androidx.databinding.BindingAdapter ואת android.widget.ImageView כשמתבקשים.
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {

}
  1. בתוך הפונקציה bindImage(), מוסיפים בלוק let {} לארגומנט imgUrl:
imgUrl?.let { 
}
  1. בתוך הבלוק let {}, מוסיפים את השורה שמוצגת למטה כדי להמיר את מחרוזת כתובת ה-URL (מ-XML) לאובייקט Uri. מייבאים את androidx.core.net.toUri כשמתבקשים.

    רוצים שאובייקט Uri הסופי ישתמש בסכימת HTTPS, כי השרת שממנו שולפים את התמונות דורש את הסכימה הזו. כדי להשתמש בסכמת HTTPS, מוסיפים buildUpon.scheme("https") ל-toUri builder. השיטה toUri() היא פונקציית הרחבה של Kotlin מהספרייה המרכזית של Android KTX, ולכן נראה שהיא חלק מהמחלקה String.
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
  1. עדיין בתוך let {}, קוראים ל-Glide.with() כדי לטעון את התמונה מהאובייקט Uri אל ImageView. מייבאים את com.bumptech.glide.Glide כשמתבקשים לעשות זאת.
Glide.with(imgView.context)
       .load(imgUri)
       .into(imgView)

שלב 4: עדכון הפריסה והקטעים

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

  1. פתיחת res/layout/gridview_item.xml. זהו קובץ משאבי הפריסה שתשתמשו בו לכל פריט ב-RecyclerView בהמשך ה-codelab. אתם משתמשים בו באופן זמני כדי להציג רק את התמונה הבודדת.
  2. מעל האלמנט <ImageView>, מוסיפים אלמנט <data> לקישור הנתונים ומקשרים אותו למחלקה OverviewViewModel:
<data>
   <variable
       name="viewModel"
       type="com.example.android.marsrealestate.overview.OverviewViewModel" />
</data>
  1. כדי להשתמש במתאם החדש של קישור לטעינת תמונות, מוסיפים מאפיין app:imageUrl לאלמנט ImageView:
app:imageUrl="@{viewModel.property.imgSrcUrl}"
  1. פתיחת overview/OverviewFragment.kt. בשיטה onCreateView(), מוסיפים הערה לשורה שמנפחת את המחלקה FragmentOverviewBinding ומקצה אותה למשתנה של הקישור. זה רק זמני, ותוכלו לחזור אליו מאוחר יותר.
//val binding = FragmentOverviewBinding.inflate(inflater)
  1. במקום זאת, מוסיפים שורה כדי להגדיל את הכיתה GridViewItemBinding. מייבאים את com.example.android.marsrealestate. databinding.GridViewItemBinding כשמתבקשים.
val binding = GridViewItemBinding.inflate(inflater)
  1. מפעילים את האפליקציה. עכשיו אמורה להופיע תמונה של התמונה מהפריט הראשון MarsProperty ברשימת התוצאות.

שלב 5: הוספת תמונות פשוטות של טעינה ושגיאה

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

  1. פותחים את res/drawable/ic_broken_image.xml ולוחצים על הכרטיסייה תצוגה מקדימה בצד שמאל. בתמונת השגיאה, אתם משתמשים בסמל של תמונה שבורה שזמין בספריית הסמלים המובנית. הפריט הגרפי הווקטורי הזה שניתן לשרטוט משתמש במאפיין android:tint כדי לצבוע את הסמל באפור.

  1. פתיחת res/drawable/loading_animation.xml. הפריט הגרפי הזה הוא אנימציה שמוגדרת באמצעות התג <animate-rotate>. האנימציה מסובבת תמונה שניתנת לציור, loading_img.xml, סביב נקודת המרכז. (לא רואים את האנימציה בתצוגה המקדימה).

  1. חוזרים לקובץ BindingAdapters.kt. בשיטה bindImage(), מעדכנים את הקריאה ל-Glide.with() כדי לקרוא לפונקציה apply() בין load() ל-into(). מייבאים את com.bumptech.glide.request.RequestOptions כשמתבקשים.

    הקוד הזה מגדיר את תמונת הטעינה של ה-placeholder לשימוש בזמן הטעינה (ה-drawable‏ loading_animation). הקוד גם מגדיר תמונה לשימוש אם טעינת התמונה נכשלת (ה-drawable‏ broken_image). השיטה המלאה bindImage() נראית עכשיו כך:
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
    imgUrl?.let {
        val imgUri = 
           imgUrl.toUri().buildUpon().scheme("https").build()
        Glide.with(imgView.context)
                .load(imgUri)
                .apply(RequestOptions()
                        .placeholder(R.drawable.loading_animation)
                        .error(R.drawable.ic_broken_image))
                .into(imgView)
    }
}
  1. מריצים את האפליקציה. בהתאם למהירות החיבור לרשת, יכול להיות שתראו לזמן קצר את תמונת הטעינה בזמן שהמערכת מורידה ומציגה את תמונת הנכס. אבל עדיין לא תראו את סמל התמונה השבורה, גם אם תכבו את הרשת – תתקנו את זה בחלק האחרון של ה-codelab.

האפליקציה שלכם טוענת עכשיו את פרטי הנכס מהאינטרנט. באמצעות הנתונים מפריט הרשימה הראשון MarsProperty, יצרתם מאפיין LiveData במודל התצוגה, והשתמשתם בכתובת ה-URL של התמונה מנתוני המאפיין הזה כדי לאכלס את ImageView. אבל המטרה היא שהאפליקציה תציג רשת של תמונות, ולכן צריך להשתמש ב-RecyclerView עם GridLayoutManager.

שלב 1: עדכון מודל התצוגה

בשלב הזה, למודל התצוגה יש _property LiveData שמכיל אובייקט MarsProperty אחד – הראשון ברשימת התגובות משירות האינטרנט. בשלב הזה, משנים את LiveData כך שיכיל את כל רשימת האובייקטים של MarsProperty.

  1. פתיחת overview/OverviewViewModel.kt.
  2. משנים את המשתנה הפרטי _property ל-_properties. משנים את הסוג לרשימה של אובייקטים מסוג MarsProperty.
private val _properties = MutableLiveData<List<MarsProperty>>()
  1. מחליפים את הנתונים החיצוניים property ב-properties. מוסיפים את הרשימה גם לסוג LiveData כאן:
 val properties: LiveData<List<MarsProperty>>
        get() = _properties
  1. גוללים למטה אל השיטה getMarsRealEstateProperties(). בתוך הבלוק try {}, מחליפים את כל הבדיקה שהוספתם במשימה הקודמת בשורה שמופיעה למטה. מכיוון שהמשתנה listResult מכיל רשימה של אובייקטים מסוג MarsProperty, אפשר פשוט להקצות אותו ל-_properties.value במקום לבדוק אם התקבלה תגובה מוצלחת.
_properties.value = listResult

הבלוק try/catch כולו נראה עכשיו כך:

try {
   var listResult = getPropertiesDeferred.await()
   _response.value = "Success: ${listResult.size} Mars properties retrieved"
   _properties.value = listResult
} catch (e: Exception) {
   _response.value = "Failure: ${e.message}"
}

שלב 2: עדכון הפריסות והקטעים

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

  1. פתיחת res/layout/gridview_item.xml. משנים את קישור הנתונים מ-OverviewViewModel ל-MarsProperty ומשנים את שם המשתנה ל-"property".
<variable
   name="property"
   type="com.example.android.marsrealestate.network.MarsProperty" />
  1. ב-<ImageView>, משנים את המאפיין app:imageUrl כך שיפנה לכתובת ה-URL של התמונה באובייקט MarsProperty:
app:imageUrl="@{property.imgSrcUrl}"
  1. פתיחת overview/OverviewFragment.kt. ב-onCreateview(), מבטלים את ההערה בשורה שמנפחת את FragmentOverviewBinding. מוחקים את השורה שמנפחת את GridViewBinding או הופכים אותה לתגובה. השינויים האלה מבטלים את השינויים הזמניים שביצעתם במשימה האחרונה.
val binding = FragmentOverviewBinding.inflate(inflater)
 // val binding = GridViewItemBinding.inflate(inflater)
  1. פתיחת res/layout/fragment_overview.xml. מוחקים את כל הרכיב <TextView>.
  2. במקום זאת, מוסיפים את רכיב <RecyclerView> הזה, שמשתמש בפריסת grid_view_item וב-GridLayoutManager לפריט בודד:
<androidx.recyclerview.widget.RecyclerView
            android:id="@+id/photos_grid"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:padding="6dp"
            android:clipToPadding="false"
            app:layoutManager=
               "androidx.recyclerview.widget.GridLayoutManager"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:spanCount="2"
            tools:itemCount="16"
            tools:listitem="@layout/grid_view_item" />

שלב 3: הוספת המתאם של רשת התמונות

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

  1. פתיחת overview/PhotoGridAdapter.kt.
  2. יוצרים את המחלקה PhotoGridAdapter עם פרמטרי הבנאי שמוצגים למטה. המחלקות  מרחיבות את המחלקות , והבנאי שלהן צריך את סוג פריט הרשימה, את מחזיק התצוגה ואת ההטמעה של .PhotoGridAdapterListAdapterDiffUtil.ItemCallback

    מייבאים את הכיתות androidx.recyclerview.widget.ListAdapter ו-com.example.android.marsrealestate.network.MarsProperty כשמתבקשים לעשות זאת. בשלבים הבאים, מטמיעים את החלקים החסרים האחרים של ה-constructor הזה שגורמים לשגיאות.
class PhotoGridAdapter : ListAdapter<MarsProperty,
        PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {

}
  1. לוחצים במקום כלשהו בכיתה PhotoGridAdapter ומקישים על Control+i כדי להטמיע את השיטות ListAdapter, שהן onCreateViewHolder() ו-onBindViewHolder().
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoGridAdapter.MarsPropertyViewHolder {
   TODO("not implemented") 
}

override fun onBindViewHolder(holder: PhotoGridAdapter.MarsPropertyViewHolder, position: Int) {
   TODO("not implemented") 
}
  1. בסוף ההגדרה של המחלקה PhotoGridAdapter, אחרי השיטות שהוספתם, מוסיפים הגדרה של אובייקט נלווה בשם DiffCallback, כמו בדוגמה שלמטה.

    מייבאים את androidx.recyclerview.widget.DiffUtil כשמתבקשים. ‫

    האובייקט DiffCallback מרחיב את DiffUtil.ItemCallback עם סוג האובייקט שרוצים להשוות – MarsProperty.
companion object DiffCallback : DiffUtil.ItemCallback<MarsProperty>() {
}
  1. מקישים על Control+i כדי להטמיע את שיטות ההשוואה של האובייקט הזה, שהן areItemsTheSame() ו-areContentsTheSame().
override fun areItemsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
   TODO("not implemented") 
}

override fun areContentsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
   TODO("not implemented") }
  1. בשביל שיטת areItemsTheSame(), מסירים את TODO. משתמשים באופרטור השוואה של Kotlin (===), שמחזיר true אם הפניות לאובייקט של oldItem ו-newItem זהות.
override fun areItemsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem === newItem
}
  1. במאפיין areContentsTheSame(), משתמשים באופרטור השוויון הרגיל רק על המזהה של oldItem ושל newItem.
override fun areContentsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem.id == newItem.id
}
  1. עדיין בתוך המחלקה PhotoGridAdapter, מתחת לאובייקט הנלווה, מוסיפים הגדרה של מחלקה פנימית בשם MarsPropertyViewHolder, שמרחיבה את RecyclerView.ViewHolder.

    מייבאים את androidx.recyclerview.widget.RecyclerView ואת com.example.android.marsrealestate.databinding.GridViewItemBinding כשמתבקשים.

    צריך את המשתנה GridViewItemBinding כדי לקשור את MarsProperty לפריסה, לכן מעבירים את המשתנה אל MarsPropertyViewHolder. מכיוון שהמחלקה הבסיסית ViewHolder דורשת תצוגה בבונה שלה, מעבירים לה את תצוגת השורש של הקישור.
class MarsPropertyViewHolder(private var binding: 
                   GridViewItemBinding):
       RecyclerView.ViewHolder(binding.root) {

}
  1. ב-MarsPropertyViewHolder, יוצרים שיטת bind() שמקבלת אובייקט MarsProperty כארגומנט ומגדירה את binding.property לאובייקט הזה. מתקשרים אל executePendingBindings() אחרי שמגדירים את המאפיין, וכך העדכון מתבצע באופן מיידי.
fun bind(marsProperty: MarsProperty) {
   binding.property = marsProperty
   binding.executePendingBindings()
}
  1. ב-onCreateViewHolder(), מסירים את ה-TODO ומוסיפים את השורה שמוצגת למטה. מייבאים את android.view.LayoutInflater כשמתבקשים. ‫

    השיטה onCreateViewHolder() צריכה להחזיר MarsPropertyViewHolder חדש, שנוצר על ידי ניפוח GridViewItemBinding ושימוש ב-LayoutInflater מההקשר של ViewGroup האב.
   return MarsPropertyViewHolder(GridViewItemBinding.inflate(
      LayoutInflater.from(parent.context)))
  1. בשיטה onBindViewHolder(), מסירים את ה-TODO ומוסיפים את השורות שמוצגות למטה. כאן קוראים ל-getItem() כדי לקבל את האובייקט MarsProperty שמשויך למיקום הנוכחי RecyclerView, ואז מעבירים את הנכס הזה לשיטה bind() ב-MarsPropertyViewHolder.
val marsProperty = getItem(position)
holder.bind(marsProperty)

שלב 4: מוסיפים את מתאם הקישור ומחברים את החלקים

לבסוף, משתמשים ב-BindingAdapter כדי לאתחל את PhotoGridAdapter עם רשימת האובייקטים MarsProperty. שימוש ב-BindingAdapter כדי להגדיר את נתוני RecyclerView גורם לכך שהנתונים יקושרו באופן אוטומטי ל-LiveData של רשימת אובייקטים מסוג MarsProperty. לאחר מכן, מתאם הקישור נקרא באופן אוטומטי כשמתבצעים שינויים ברשימה MarsProperty.

  1. פתיחת BindingAdapters.kt.
  2. בסוף הקובץ, מוסיפים שיטת bindRecyclerView() שמקבלת RecyclerView ורשימה של אובייקטים מסוג MarsProperty כארגומנטים. מוסיפים הערה לשיטה עם @BindingAdapter.

    מייבאים את androidx.recyclerview.widget.RecyclerView ואת com.example.android.marsrealestate.network.MarsProperty כשמתבקשים.
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, 
    data: List<MarsProperty>?) {
}
  1. בתוך הפונקציה bindRecyclerView(), מבצעים המרה של recyclerView.adapter ל-PhotoGridAdapter ומפעילים את adapter.submitList() עם הנתונים. ההודעה הזו מציינת ל-RecyclerView מתי רשימה חדשה זמינה.

מייבאים את com.example.android.marsrealestate.overview.PhotoGridAdapter כשמתבקשים.

val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
  1. פתיחת res/layout/fragment_overview.xml. מוסיפים את המאפיין app:listData לרכיב RecyclerView ומגדירים אותו לערך viewmodel.properties באמצעות קישור נתונים.
app:listData="@{viewModel.properties}"
  1. פתיחת overview/OverviewFragment.kt. ב-onCreateView(), ממש לפני הקריאה ל-setHasOptionsMenu(), מאתחלים את המתאם RecyclerView ב-binding.photosGrid לאובייקט PhotoGridAdapter חדש.
binding.photosGrid.adapter = PhotoGridAdapter()
  1. מריצים את האפליקציה. אמורה להופיע רשת של תמונות של MarsProperty. כשגוללים כדי לראות תמונות חדשות, האפליקציה מציגה את סמל התקדמות הטעינה לפני שהתמונה עצמה מוצגת. אם מפעילים את מצב הטיסה, תמונות שעדיין לא נטענו מופיעות כסמלים של תמונות פגומות.

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

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

שלב 1: הוספת סטטוס למודל התצוגה

כדי להתחיל, יוצרים LiveData במודל התצוגה כדי לייצג את הסטטוס של בקשת האינטרנט. יש שלושה מצבים שצריך לקחת בחשבון: טעינה, הצלחה וכישלון. מצב הטעינה מתרחש בזמן שאתם ממתינים לנתונים בשיחה אל await().

  1. פתיחת overview/OverviewViewModel.kt. בחלק העליון של הקובץ (אחרי הייבוא, לפני הגדרת המחלקה), מוסיפים enum כדי לייצג את כל הסטטוסים הזמינים:
enum class MarsApiStatus { LOADING, ERROR, DONE }
  1. משנים את השם של ההגדרות של הנתונים הפעילים הפנימיים והחיצוניים של _response בכל מקום בכיתה OverviewViewModel ל-_status. מכיוון שהוספת תמיכה ב-_properties LiveData מוקדם יותר ב-codelab הזה, התשובה המלאה של שירות האינטרנט לא הייתה בשימוש. כדי לעקוב אחרי הסטטוס הנוכחי, צריך LiveData כאן, ולכן אפשר פשוט לשנות את השם של המשתנים הקיימים.

בנוסף, צריך לשנות את הסוגים מ-String ל-MarsApiStatus.

private val _status = MutableLiveData<MarsApiStatus>()

val status: LiveData<MarsApiStatus>
   get() = _status
  1. גוללים למטה אל אמצעי התשלום getMarsRealEstateProperties() ומעדכנים את _response ל-_status גם שם. משנים את המחרוזת "Success" למצב MarsApiStatus.DONE ואת המחרוזת "Failure" למצב MarsApiStatus.ERROR.
  2. מוסיפים סטטוס MarsApiStatus.LOADING בראש הבלוק try {}, לפני השיחה אל await(). זה הסטטוס הראשוני בזמן שהקורוטינה פועלת וממתינים לנתונים. בלוק try/catch {} המלא נראה עכשיו כך:
try {
    _status.value = MarsApiStatus.LOADING
   var listResult = getPropertiesDeferred.await()
   _status.value = MarsApiStatus.DONE
   _properties.value = listResult
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
}
  1. אחרי מצב השגיאה בבלוק catch {}, מגדירים את _properties LiveData לרשימה ריקה. הפעולה הזו תנקה את RecyclerView.
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
   _properties.value = ArrayList()
}

שלב 2: הוספת מתאם של Binding עבור ImageView של הסטטוס

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

  1. פתיחת BindingAdapters.kt. מוסיפים מתאם חדש של קישור נתונים בשם bindStatus() שמקבל את הערכים ImageView ו-MarsApiStatus כארגומנטים. מייבאים את com.example.android.marsrealestate.overview.MarsApiStatus כשמתבקשים.
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView, 
          status: MarsApiStatus?) {
}
  1. מוסיפים when {} בתוך השיטה bindStatus() כדי לעבור בין הסטטוסים השונים.
when (status) {

}
  1. בתוך when {}, מוסיפים מקרה למצב הטעינה (MarsApiStatus.LOADING). במצב הזה, מגדירים את ImageView כגלוי ומקצים לו את אנימציית הטעינה. זהו אותו ציור אנימציה שבו השתמשתם ב-Glide במשימה הקודמת. מייבאים את android.view.View כשמתבקשים.
when (status) {
   MarsApiStatus.LOADING -> {
      statusImageView.visibility = View.VISIBLE
      statusImageView.setImageResource(R.drawable.loading_animation)
   }
}
  1. מוסיפים תרחיש לסטטוס השגיאה, שהוא MarsApiStatus.ERROR. בדומה למה שעשיתם בשלב LOADING, מגדירים את הסטטוס ImageView כגלוי ומשתמשים מחדש ב-drawable של שגיאת החיבור.
MarsApiStatus.ERROR -> {
   statusImageView.visibility = View.VISIBLE
   statusImageView.setImageResource(R.drawable.ic_connection_error)
}
  1. מוסיפים תרחיש שימוש למצב 'בוצע', שהוא MarsApiStatus.DONE. התשובה תקינה, לכן צריך להשבית את ההגדרה 'חשיפה' של הסטטוס ImageView כדי להסתיר אותו.
MarsApiStatus.DONE -> {
   statusImageView.visibility = View.GONE
}

שלב 3: הוספת תצוגת התמונה של הסטטוס לפריסה

  1. פתיחת res/layout/fragment_overview.xml. מתחת לרכיב RecyclerView, בתוך ConstraintLayout, מוסיפים את ImageView שמופיע למטה.

    לרכיב ImageView יש את אותן מגבלות כמו לרכיב RecyclerView. עם זאת, הרוחב והגובה משתמשים ב-wrap_content כדי למרכז את התמונה ולא כדי למתוח אותה כך שתמלא את התצוגה. שימו לב גם למאפיין app:marsApiStatus, שכולל את קריאת התצוגה של BindingAdapter כשמאפיין הסטטוס במודל התצוגה משתנה.
<ImageView
   android:id="@+id/status_image"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:marsApiStatus="@{viewModel.status}" />
  1. מפעילים את מצב טיסה באמולטור או במכשיר כדי לדמות מצב שבו אין חיבור לרשת. קומפלו את האפליקציה והריצו אותה. תראו שתמונת השגיאה מופיעה:

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

פרויקט Android Studio: ‏ MarsRealEstateGrid

  • כדי לפשט את תהליך ניהול התמונות, אפשר להשתמש בספריית Glide כדי להוריד, לאחסן בזיכרון זמני, לפענח ולשמור במטמון תמונות באפליקציה.
  • כדי לטעון תמונה מהאינטרנט, Glide צריך שני דברים: כתובת ה-URL של התמונה ואובייקט ImageView שבו התמונה תוצג. כדי לציין את האפשרויות האלה, משתמשים בשיטות load() ו-into() עם Glide.
  • Binding adapters הן שיטות הרחבה שמוצבות בין תצוגה לבין הנתונים שמשויכים לתצוגה הזו. מתאמי Binding מספקים התנהגות מותאמת אישית כשהנתונים משתנים, למשל, כדי להפעיל את Glide לטעינת תמונה מכתובת URL לתוך ImageView.
  • מתאמי Binding הם שיטות הרחבה שמסומנות בהערה @BindingAdapter.
  • כדי להוסיף אפשרויות לבקשת Glide, משתמשים בשיטה apply(). לדוגמה, משתמשים ב-apply() עם placeholder() כדי לציין drawable לטעינה, וב-apply() עם error() כדי לציין drawable לשגיאה.
  • כדי ליצור רשת של תמונות, משתמשים ב-RecyclerView עם GridLayoutManager.
  • כדי לעדכן את רשימת המאפיינים כשהיא משתנה, משתמשים במתאם קישור בין RecyclerView לפריסה.

קורס ב-Udacity:

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

אחר:

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

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

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

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

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

שאלה 1

באיזו שיטה של Glide השתמשת כדי לציין את ImageView שיכיל את התמונה שנטענה?

into()

with()

imageview()

apply()

שאלה 2

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

‫▢ שימוש בשיטה into() עם drawable.

‫▢ משתמשים ב-RequestOptions() ומפעילים את השיטה placeholder() עם drawable.

‫▢ מקצים את המאפיין Glide.placeholder ל-drawable.

‫▢ משתמשים ב-RequestOptions() ומפעילים את השיטה loadingImage() עם drawable.

שאלה 3

איך מציינים ששיטה היא מתאם קישור?

‫▢ מפעילים את ה-method‏ setBindingAdapter() ב-LiveData.

‫▢ מעבירים את השיטה לקובץ Kotlin בשם BindingAdapters.kt.

‫▢ משתמשים במאפיין android:adapter בפריסת ה-XML.

‫▢ מוסיפים את ההערה @BindingAdapter לשיטה.

להתחלת השיעור הבא: 8.3 סינון ותצוגות מפורטות עם נתונים מהאינטרנט

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