‫Android Kotlin Fundamentals 08.1: קבלת נתונים מהאינטרנט

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

מבוא

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

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

מה שכדאי לדעת

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

מה תלמדו

  • מהו שירות אינטרנט REST.
  • שימוש בספריית Retrofit כדי להתחבר לשירות אינטרנט REST באינטרנט ולקבל תגובה.
  • שימוש בספריית Moshi כדי לנתח את תגובת ה-JSON לאובייקט נתונים.

מה עושים

  • משנים אפליקציית Starter כדי ליצור בקשת API לשירות אינטרנט ולטפל בתשובה.
  • מטמיעים שכבת רשת באפליקציה באמצעות ספריית Retrofit.
  • מנתחים את תגובת ה-JSON משירות האינטרנט לנתונים בזמן אמת באפליקציה באמצעות ספריית Moshi.
  • כדי לפשט את הקוד, אפשר להשתמש בתמיכה של Retrofit ב-coroutines.

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

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

.

הארכיטקטורה של אפליקציית MarsRealEstate כוללת שני מודולים עיקריים:

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

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

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

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

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

שלב 1: בחינת רכיבי Fragment וניווט

  1. מורידים את אפליקציית המתחילים MarsRealEstate ופותחים אותה ב-Android Studio.
  2. בדיקה של app/java/MainActivity.kt. האפליקציה משתמשת ב-fragments בשני המסכים, ולכן המשימה היחידה של הפעילות היא לטעון את הפריסה של הפעילות.
  3. בדיקה של app/res/layout/activity_main.xml. פריסת הפעילות היא המארח של שני הקטעים, שמוגדרים בקובץ הניווט. פריסת ה-Layout הזו יוצרת מופע של NavHostFragment ושל בקר הניווט המשויך שלו עם משאב nav_graph.
  4. פתיחת app/res/navigation/nav_graph.xml. כאן אפשר לראות את קשר הניווט בין שני הפרגמנטים. גרף הניווט StartDestination מצביע על overviewFragment, ולכן קטע הסקירה הכללית מופעל כשמפעילים את האפליקציה.

שלב 2: בודקים את קובצי המקור של Kotlinואת קישור הנתונים

  1. בחלונית Project, מרחיבים את app > java. שימו לב שאפליקציית MarsRealEstate כוללת שלוש תיקיות חבילה: detail,‏ network ו-overview. הם תואמים לשלושת הרכיבים העיקריים של האפליקציה: קטעי הסקירה הכללית והפרטים, והקוד של שכבת הרשת.
  2. פתיחת app/java/overview/OverviewFragment.kt. הפונקציה OverviewFragment מאתחלת את OverviewViewModel בצורה עצלה, כלומר OverviewViewModel נוצר בפעם הראשונה שמשתמשים בו.
  3. בודקים את השיטה onCreateView(). בשיטה הזו, הפריסה fragment_overview מתרחבת באמצעות קישור נתונים, הבעלים של מחזור החיים של הקישור מוגדר לעצמו (this), והמשתנה viewModel באובייקט binding מוגדר כפריסה. הגדרנו את הבעלים של מחזור החיים, ולכן כל LiveData שמשמש לקישור נתונים יעבור מעקב אוטומטי אחרי שינויים, וממשק המשתמש יעודכן בהתאם.
  4. פתיחת app/java/overview/OverviewViewModel. מכיוון שהתשובה היא LiveData והגדרנו את מחזור החיים של משתנה הקישור, כל שינוי בו יעדכן את ממשק המשתמש של האפליקציה.
  5. בודקים את הבלוק init. כשיוצרים את ViewModel, מתבצעת קריאה לשיטה getMarsRealEstateProperties().
  6. בודקים את השיטה getMarsRealEstateProperties(). באפליקציית המתחילים הזו, ה-method מכילה תשובה של placeholder. המטרה של ה-codelab הזה היא לעדכן את התגובה LiveData בתוך ViewModel באמצעות נתונים אמיתיים שמתקבלים מהאינטרנט.
  7. פתיחת app/res/layout/fragment_overview.xml. זה הפריסה של קטע הסקירה הכללית שאתם עובדים איתו ב-codelab הזה, והיא כוללת את קשירת הנתונים של מודל התצוגה. הפונקציה מייבאת את OverviewViewModel ואז קושרת את התשובה מ-ViewModel ל-TextView. ב-codelabs מאוחרים יותר, מחליפים את תצוגת הטקסט ברשת של תמונות ב-RecyclerView.
  8. קומפילציה והרצה של האפליקציה. בגרסה הנוכחית של האפליקציה מוצגת רק התגובה הראשונית – Set the Mars API Response here!‎ (צריך להגדיר כאן את התגובה של Mars API).

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

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

https://android-kotlin-fun-mars-server.appspot.com

אם תקלידו את כתובת ה-URL הבאה בדפדפן, תקבלו רשימה של כל הנכסים הזמינים למכירה במאדים!

https://android-kotlin-fun-mars-server.appspot.com/realestate

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

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

שלב 1: הוספה של יחסי תלות ב-Retrofit ל-Gradle

  1. פותחים את build.gradle (Module: app).
  2. בקטע dependencies, מוסיפים את השורות הבאות לספריות Retrofit:
implementation "com.squareup.retrofit2:retrofit:$version_retrofit"
implementation "com.squareup.retrofit2:converter-scalars:$version_retrofit"


שימו לב שמספרי הגרסאות מוגדרים בנפרד בקובץ Gradle של הפרויקט. התלות הראשונה היא בספרייה Retrofit 2 עצמה, והתלות השנייה היא בממיר הסקלרי של Retrofit. הכלי הזה מאפשר ל-Retrofit להחזיר את התוצאה בפורמט JSON כ-String. שתי הספריות פועלות יחד.

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

שלב 2: הטמעה של MarsApiService

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

המחלקות MarsApiService מחזיקות את שכבת הרשת של האפליקציה, כלומר זה ה-API ש-ViewModel ישתמש בו כדי לתקשר עם שירות האינטרנט. זוהי המחלקה שבה תטמיעו את Retrofit service API.

  1. פתיחת app/java/network/MarsApiService.kt. בשלב הזה, הקובץ מכיל רק דבר אחד: קבוע לכתובת ה-URL הבסיסית של שירות האינטרנט.
private const val BASE_URL = 
   "https://android-kotlin-fun-mars-server.appspot.com"
  1. מתחת לקבוע הזה, משתמשים ב-Retrofit builder כדי ליצור אובייקט Retrofit. מייבאים את retrofit2.Retrofit ואת retrofit2.converter.scalars.ScalarsConverterFactory כשמוצגת בקשה לעשות זאת.
private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())
   .baseUrl(BASE_URL)
   .build()

כדי ליצור API של שירותי אינטרנט באמצעות Retrofit, צריכים להיות זמינים לפחות שני דברים: ה-URI הבסיסי של שירות האינטרנט ומפעל המרות. ה-Converter אומר ל-Retrofit מה לעשות עם הנתונים שהוא מקבל חזרה משירות האינטרנט. במקרה הזה, רוצים ש-Retrofit יאחזר תגובת JSON משירות האינטרנט ויחזיר אותה כ-String. ל-Retrofit יש ScalarsConverter שתומך במחרוזות ובסוגים פרימיטיביים אחרים, לכן קוראים ל-addConverterFactory() בכלי הבנייה עם מופע של ScalarsConverterFactory. לבסוף, קוראים ל-build() כדי ליצור את אובייקט Retrofit.

  1. מגדירים ממשק שקובע איך Retrofit מתקשר עם שרת האינטרנט באמצעות בקשות HTTP, ממש מתחת לקריאה ל-Retrofit builder. מייבאים את retrofit2.http.GET ואת retrofit2.Call כשמוצגת בקשה לעשות זאת.
interface MarsApiService {
    @GET("realestate")
    fun getProperties():
            Call<String>
}

בשלב הזה המטרה היא לקבל את מחרוזת תגובת ה-JSON משירות האינטרנט, וצריך רק method אחד כדי לעשות את זה: getProperties(). כדי להגדיר ל-Retrofit מה השיטה הזו צריכה לעשות, משתמשים בהערה @GET ומציינים את הנתיב או את נקודת הקצה של שיטת שירות האינטרנט הזו. במקרה הזה, נקודת הקצה נקראת realestate. כשמפעילים את השיטה getProperties(), ספריית Retrofit מוסיפה את נקודת הקצה realestate לכתובת הבסיסית (שהגדרתם ב-Retrofit builder) ויוצרת אובייקט Call. האובייקט Call משמש להתחלת הבקשה.

  1. מתחת לממשק MarsApiService, מגדירים אובייקט ציבורי בשם MarsApi כדי לאתחל את שירות Retrofit.
object MarsApi {
    val retrofitService : MarsApiService by lazy { 
       retrofit.create(MarsApiService::class.java) }
}

השיטה create() של Retrofit יוצרת את שירות Retrofit עצמו באמצעות הממשק MarsApiService. הקריאה הזו יקרה, והאפליקציה צריכה רק מופע אחד של שירות Retrofit, לכן חושפים את השירות לשאר האפליקציה באמצעות אובייקט ציבורי בשם MarsApi, ומבצעים את האתחול של שירות Retrofit באופן עצלני. עכשיו, אחרי שסיימתם את כל ההגדרה, בכל פעם שהאפליקציה שלכם קוראת ל-MarsApi.retrofitService, היא תקבל אובייקט Retrofit יחיד שמיישם את MarsApiService.

שלב 3: קוראים לשירות האינטרנט ב-OverviewViewModel

  1. פתיחת app/java/overview/OverviewViewModel.kt. גוללים למטה אל השיטה getMarsRealEstateProperties().
private fun getMarsRealEstateProperties() {
   _response.value = "Set the Mars API Response here!"
}

ב-method הזה קוראים לשירות Retrofit ומטפלים במחרוזת ה-JSON שמוחזרת. בשלב הזה יש רק מחרוזת placeholder לתגובה.

  1. מוחקים את שורת ה-placeholder שמגדירה את התגובה ל-Set the Mars API Response here!‎ (הגדרת התגובה של Mars API כאן).
  2. בתוך getMarsRealEstateProperties(), מוסיפים את הקוד שמופיע למטה. מייבאים את retrofit2.Callback ואת com.example.android.marsrealestate.network.MarsApi כשמוצגת בקשה לעשות זאת. ‫

    השיטה MarsApi.retrofitService.getProperties() מחזירה אובייקט Call. אחר כך אפשר להתקשר אל enqueue() באובייקט הזה כדי להתחיל את בקשת הרשת בשרשור ברקע.
MarsApi.retrofitService.getProperties().enqueue( 
   object: Callback<String> {
})
  1. לוחצים על המילה object שמסומנת בקו תחתון אדום. בוחרים באפשרות קוד > הטמעת שיטות. בוחרים את האפשרויות onResponse() ו-onFailure() מהרשימה. ‫


    Android Studio מוסיף את הקוד עם הערות TODO בכל שיטה:
override fun onFailure(call: Call<String>, t: Throwable) {
       TODO("not implemented") 
}

override fun onResponse(call: Call<String>, 
   response: Response<String>) {
       TODO("not implemented") 
}
  1. ב-onFailure(), מוחקים את TODO ומגדירים את _response להודעת שגיאה, כמו שמוצג למטה. ‫_response היא מחרוזת LiveData שקובעת מה מוצג בתצוגת הטקסט. כל מצב צריך לעדכן את _response LiveData.

    הקריאה החוזרת onFailure() מופעלת אם התגובה של שירות האינטרנט נכשלת. בתגובה הזו, צריך להגדיר את הסטטוס _response כ-"Failure: " בצירוף ההודעה מהארגומנט Throwable.
override fun onFailure(call: Call<String>, t: Throwable) {
   _response.value = "Failure: " + t.message
}
  1. ב-onResponse(), מוחקים את TODO ומגדירים את _response לגוף התגובה. הקריאה החוזרת onResponse() מתבצעת כשהבקשה מצליחה ושירות האינטרנט מחזיר תגובה.
override fun onResponse(call: Call<String>, 
   response: Response<String>) {
      _response.value = response.body()
}

שלב 4: הגדרת הרשאת הגישה לאינטרנט

  1. קומפילציה והפעלה של אפליקציית MarsRealEstate. שימו לב שהאפליקציה נסגרת מיד עם שגיאה.
  2. לוחצים על הכרטיסייה Logcat ב-Android Studio ורושמים את השגיאה ביומן, שמתחילה בשורה כזו:
Process: com.example.android.marsrealestate, PID: 10646
java.lang.SecurityException: Permission denied (missing INTERNET permission?)

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

  1. פתיחת app/manifests/AndroidManifest.xml. מוסיפים את השורה הזו ממש לפני התג <application>:
<uses-permission android:name="android.permission.INTERNET" />
  1. קומפילציה והרצה של האפליקציה שוב. אם הכול פועל כמו שצריך בחיבור לאינטרנט, מוצג טקסט JSON שמכיל נתונים של נכס מאדים.
  2. מקישים על הלחצן הקודם במכשיר או באמולטור כדי לסגור את האפליקציה.
  3. מעבירים את המכשיר או האמולטור למצב טיסה, ואז פותחים מחדש את האפליקציה מהתפריט 'אחרונים' או מפעילים מחדש את האפליקציה מ-Android Studio.


  1. משביתים שוב את מצב הטיסה.

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

במשימה הזו משתמשים בספריית Moshi עם Retrofit כדי לנתח את תגובת ה-JSON משירות האינטרנט לאובייקטים שימושיים של Mars Property Kotlin. אתם משנים את האפליקציה כך שבמקום להציג את ה-JSON הגולמי, האפליקציה תציג את מספר המאפיינים של מאדים שהוחזרו.

שלב 1: מוסיפים תלויות בספריית Moshi

  1. פותחים את build.gradle (Module: app).
  2. בקטע dependencies (יחסי תלות), מוסיפים את הקוד שמופיע למטה כדי לכלול את יחסי התלות של Moshi. בדומה ל-Retrofit, ‏ $version_moshi מוגדר בנפרד בקובץ Gradle ברמת הפרויקט. התלויות האלה מוסיפות תמיכה בספריית ה-JSON המרכזית של Moshi, ובתמיכה של Moshi ב-Kotlin.
implementation "com.squareup.moshi:moshi:$version_moshi"
implementation "com.squareup.moshi:moshi-kotlin:$version_moshi"
  1. מאתרים את השורה של ממיר הסקלר Retrofit בבלוק dependencies:
implementation "com.squareup.retrofit2:converter-scalars:$version_retrofit"
  1. משנים את השורה ל-converter-moshi:
implementation "com.squareup.retrofit2:converter-moshi:$version_retrofit"
  1. לוחצים על סנכרון עכשיו כדי לבנות מחדש את הפרויקט עם התלויות החדשות.

שלב 2: הטמעה של מחלקת הנתונים MarsProperty

דוגמה לרשומה בתגובת ה-JSON שמתקבלת משירות האינטרנט:

[{"price":450000,
"id":"424906",
"type":"rent",
"img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"},
...]

תגובת ה-JSON שמוצגת למעלה היא מערך, כפי שמצוין על ידי הסוגריים המרובעים. המערך מכיל אובייקטים של JSON, שמוקפים בסוגריים מסולסלים. כל אובייקט מכיל קבוצה של צמדי שם-ערך שמופרדים בנקודתיים. השמות מוקפים במירכאות. הערכים יכולים להיות מספרים או מחרוזות, ומחרוזות מוקפות במירכאות. לדוגמה, הערך של מאפיין price הוא 450,000$, והערך של מאפיין img_src הוא כתובת URL, שהיא המיקום של קובץ התמונה בשרת.

בדוגמה שלמעלה, אפשר לראות שלכל רשומה של מאפיין Mars יש את צמדי המפתח והערך הבאים ב-JSON:

  • price: המחיר של הנכס Mars, כמספר.
  • id: מזהה הנכס, כמחרוזת.
  • type: "rent" או "buy".
  • img_src: כתובת ה-URL של התמונה כמחרוזת.

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

  1. פתיחת app/java/network/MarsProperty.kt.
  2. מחליפים את ההגדרה הקיימת של המחלקה MarsProperty בקוד הבא:
data class MarsProperty(
   val id: String, val img_src: String,
   val type: String,
   val price: Double
)

שימו לב שכל אחד מהמשתנים במחלקה MarsProperty תואם לשם מפתח באובייקט JSON. כדי להתאים את הסוגים ב-JSON, משתמשים באובייקטים String לכל הערכים חוץ מ-price, שהוא Double. אפשר להשתמש ב-Double כדי לייצג כל מספר JSON.

כשספריית Moshi מנתחת את ה-JSON, היא מתאימה את המפתחות לפי השם וממלאת את אובייקטי הנתונים בערכים המתאימים.

  1. מחליפים את השורה של המפתח img_src בשורה שמוצגת למטה. מייבאים את com.squareup.moshi.Json כשמתבקשים לעשות זאת.
@Json(name = "img_src") val imgSrcUrl: String,

לפעמים שמות המפתחות בתגובת JSON יכולים ליצור מאפייני Kotlin מבלבלים, או לא להתאים לסגנון התכנות שלכם. לדוגמה, בקובץ JSON המפתח img_src משתמש בקו תחתון, בעוד שבמאפייני Kotlin נהוג להשתמש באותיות רישיות וקטנות (camel case).

כדי להשתמש בשמות משתנים במחלקת הנתונים ששונים משמות המפתחות בתגובת ה-JSON, משתמשים בהערת ה-@Json. בדוגמה הזו, השם של המשתנה במחלקת הנתונים הוא imgSrcUrl. המשתנה ממופה למאפיין JSON‏ img_src באמצעות @Json(name = "img_src").

שלב 3: מעדכנים את MarsApiService ואת OverviewViewModel

אחרי שיוצרים את מחלקת הנתונים MarsProperty, אפשר לעדכן את ה-API של הרשת ואת ViewModel כך שיכללו את נתוני Moshi.

  1. פתיחת network/MarsApiService.kt. יכול להיות שיוצגו שגיאות של מחלקה חסרה עבור ScalarsConverterFactory. הסיבה לכך היא שינוי התלות ב-Retrofit שביצעתם בשלב 1. חשוב לתקן את השגיאות האלה בהקדם.
  2. בחלק העליון של הקובץ, ממש לפני Retrofit builder, מוסיפים את הקוד הבא כדי ליצור את מופע Moshi. מייבאים את com.squareup.moshi.Moshi ואת com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory כשמוצגת בקשה לעשות זאת.
private val moshi = Moshi.Builder()
   .add(KotlinJsonAdapterFactory())
   .build()

בדומה למה שעשיתם עם Retrofit, כאן אתם יוצרים אובייקט moshi באמצעות Moshi builder. כדי שההערות של Moshi יפעלו בצורה תקינה עם Kotlin, מוסיפים את KotlinJsonAdapterFactory ואז מפעילים את build().

  1. משנים את Retrofit builder כך שישתמש ב-MoshiConverterFactory במקום ב-ScalarConverterFactory, ומעבירים את המופע moshi שיצרתם. מייבאים את retrofit2.converter.moshi.MoshiConverterFactory כשמתבקשים.
private val retrofit = Retrofit.Builder()
   .addConverterFactory(MoshiConverterFactory.create(moshi))
   .baseUrl(BASE_URL)
   .build()
  1. מומלץ למחוק גם את הייבוא של ScalarConverterFactory.

קוד למחיקה:

import retrofit2.converter.scalars.ScalarsConverterFactory
  1. מעדכנים את הממשק MarsApiService כך ש-Retrofit תחזיר רשימה של אובייקטים מסוג MarsProperty, במקום להחזיר Call<String>.
interface MarsApiService {
   @GET("realestate")
   fun getProperties():
      Call<List<MarsProperty>>
}
  1. פתיחת OverviewViewModel.kt. גוללים למטה אל השיחה עם getProperties().enqueue() בשיטה getMarsRealEstateProperties().
  2. משנים את הארגומנט ל-enqueue() מ-Callback<String> ל-Callback<List<MarsProperty>>. מייבאים את com.example.android.marsrealestate.network.MarsProperty כשמתבקשים.
MarsApi.retrofitService.getProperties().enqueue( 
   object: Callback<List<MarsProperty>> {
  1. ב-onFailure(), משנים את הארגומנט מ-Call<String> ל-Call<List<MarsProperty>>:
override fun onFailure(call: Call<List<MarsProperty>>, t: Throwable) {
  1. מבצעים את אותו שינוי בשני הארגומנטים של onResponse():
override fun onResponse(call: Call<List<MarsProperty>>, 
   response: Response<List<MarsProperty>>) {
  1. בגוף של onResponse(), מחליפים את ההקצאה הקיימת ל-_response.value בהקצאה שמוצגת למטה. מכיוון ש-response.body() הוא עכשיו רשימה של אובייקטים מסוג MarsProperty, גודל הרשימה הזו הוא מספר המאפיינים שנותחו. הודעת התגובה הזו מדפיסה את מספר הנכסים:
_response.value = 
   "Success: ${response.body()?.size} Mars properties retrieved"
  1. מוודאים שמצב טיסה מושבת. מבצעים קומפילציה ומריצים את האפליקציה. הפעם ההודעה אמורה להציג את מספר הנכסים שמוחזרים משירות האינטרנט:

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

במשימה הזו תמירו את שירות הרשת ואת ViewModel לשימוש בקורוטינות.

שלב 1: מוסיפים תלות בקורוטינות

  1. פותחים את build.gradle (Module: app).
  2. בקטע dependencies, מוסיפים תמיכה בספריות הליבה של Kotlin coroutine ובספריית Retrofit coroutine:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version_kotlin_coroutines"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version_kotlin_coroutines"

implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$version_retrofit_coroutines_adapter"
  1. לוחצים על סנכרון עכשיו כדי לבנות מחדש את הפרויקט עם התלויות החדשות.

שלב 2: מעדכנים את MarsApiService ואת OverviewViewModel

  1. ב-MarsApiService.kt, מעדכנים את Retrofit builder כדי להשתמש ב-CoroutineCallAdapterFactory. הכלי המלא לבניית תנאים נראה עכשיו כך:
private val retrofit = Retrofit.Builder()
        .addConverterFactory(MoshiConverterFactory.create(moshi))
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .baseUrl(BASE_URL)
        .build()

מתאמי קריאות מוסיפים ל-Retrofit את היכולת ליצור ממשקי API שמחזירים משהו אחר מלבד המחלקה Call שמוגדרת כברירת מחדל. במקרה הזה, CoroutineCallAdapterFactory מאפשר לנו להחליף את האובייקט Call שמוחזר על ידי getProperties() באובייקט Deferred.

  1. בשיטה getProperties(), משנים את Call<List<MarsProperty>> ל-Deferred<List<MarsProperty>>. מייבאים את kotlinx.coroutines.Deferred כשמתבקשים. getProperties()השיטה המלאה נראית כך:
@GET("realestate")
fun getProperties():
   Deferred<List<MarsProperty>>

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

  1. פתיחת OverviewViewModel.kt. לפני הבלוק init, מוסיפים עבודת קורוטינה:
private var viewModelJob = Job()
  1. יוצרים היקף קורוטינה למשימה החדשה באמצעות ה-dispatcher הראשי:
private val coroutineScope = CoroutineScope(
   viewModelJob + Dispatchers.Main )

ה-dispatcher‏ Dispatchers.Main משתמש ב-UI thread לעבודה שלו. מכיוון ש-Retrofit מבצע את כל העבודה שלו בשרשור ברקע, אין סיבה להשתמש בשרשור אחר עבור ההיקף. כך תוכלו לעדכן בקלות את הערך של MutableLiveData כשתקבלו תוצאה.

  1. מוחקים את כל הקוד בתוך getMarsRealEstateProperties(). במקום הקריאה ל-enqueue() והקריאות החוזרות onFailure() ו-onResponse(), נשתמש כאן בקורוטינות.
  2. בתוך getMarsRealEstateProperties(), מפעילים את הקורוטינה:
coroutineScope.launch { 

}


כדי להשתמש באובייקט Deferred שמוחזר על ידי Retrofit עבור משימת הרשת, צריך להיות בתוך קורוטינה, ולכן כאן מפעילים את הקורוטינה שיצרתם. הקוד עדיין מופעל ב-thread הראשי, אבל עכשיו אתם מאפשרים ל-coroutines לנהל את הריצה המקבילית.

  1. בתוך בלוק ההפעלה, קוראים ל-getProperties() באובייקט retrofitService:
var getPropertiesDeferred = MarsApi.retrofitService.getProperties()

התקשרות אל getProperties() מהשירות MarsApi יוצרת ומתחילה את השיחה ברשת בשרשור ברקע, ומחזירה את האובייקט Deferred למשימה הזו.

  1. בתוך בלוק ההפעלה, מוסיפים בלוק try/catch כדי לטפל בחריגים:
try {

} catch (e: Exception) {
  
}
  1. בתוך הבלוק try {}, קוראים ל-await() באובייקט Deferred:
var listResult = getPropertiesDeferred.await()

הפעלת הפונקציה await() באובייקט Deferred מחזירה את התוצאה מהקריאה לרשת כשהערך מוכן. השיטה await() היא לא חוסמת, ולכן שירות Mars API מאחזר את הנתונים מהרשת בלי לחסום את השרשור הנוכחי – וזה חשוב כי אנחנו בהיקף של שרשור ממשק המשתמש. אחרי שהמשימה מסתיימת, הקוד ממשיך לפעול מהמקום שבו הוא הפסיק. הפעולה הזו מתבצעת בתוך try {} כדי שתוכלו לזהות חריגים.

  1. בנוסף, בתוך הבלוק try {}, אחרי השיטה await(), מעדכנים את הודעת התגובה לתגובה מוצלחת:
_response.value = 
   "Success: ${listResult.size} Mars properties retrieved"
  1. בתוך הבלוק catch {}, מטפלים בתגובה של הכשל:
_response.value = "Failure: ${e.message}"


השיטה המלאה getMarsRealEstateProperties() נראית עכשיו כך:

private fun getMarsRealEstateProperties() {
   coroutineScope.launch {
       var getPropertiesDeferred = 
          MarsApi.retrofitService.getProperties()
       try {          
           _response.value = 
              "Success: ${listResult.size} Mars properties retrieved"
       } catch (e: Exception) {
           _response.value = "Failure: ${e.message}"
       }
   }
}
  1. בחלק התחתון של הכיתה, מוסיפים onCleared() קריאה חוזרת עם הקוד הזה:
override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}

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

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

פרויקט Android Studio: ‏ MarsRealEstateNetwork

שירותי אינטרנט של REST

  • שירות אינטרנט הוא שירות באינטרנט שמאפשר לאפליקציה לשלוח בקשות ולקבל נתונים.
  • שירותי אינטרנט נפוצים משתמשים בארכיטקטורת REST. שירותי אינטרנט שמציעים ארכיטקטורת REST נקראים שירותים RESTful. שירותי אינטרנט מסוג RESTful מבוססים על רכיבים ופרוטוקולים סטנדרטיים של אינטרנט.
  • אתם שולחים בקשה לשירות אינטרנט REST בצורה סטנדרטית, באמצעות URI.
  • כדי להשתמש בשירות אינטרנט, אפליקציה צריכה ליצור חיבור לרשת ולתקשר עם השירות. לאחר מכן, האפליקציה צריכה לקבל ולנתח את נתוני התשובות לפורמט שהאפליקציה יכולה להשתמש בו.
  • הספרייה Retrofit היא ספריית לקוח שמאפשרת לאפליקציה לשלוח בקשות לשירות אינטרנט REST.
  • אפשר להשתמש בממירים כדי להגדיר ל-Retrofit מה לעשות עם הנתונים שהוא שולח לשירות האינטרנט ומקבל משירות האינטרנט. לדוגמה, הממיר ScalarsConverter מתייחס לנתונים של שירות האינטרנט כאל String או כאל פרימיטיב אחר.
  • כדי לאפשר לאפליקציה להתחבר לאינטרנט, מוסיפים את ההרשאה "android.permission.INTERNET" בקובץ המניפסט של Android.

ניתוח JSON

  • התגובה משירות אינטרנט מפורמטת לרוב ב-JSON, פורמט נפוץ להחלפת נתונים שמייצג נתונים מובְנים.
  • אובייקט JSON הוא אוסף של צמדי מפתח/ערך. האוסף הזה נקרא לפעמים מילון, מפת גיבוב או מערך משויך.
  • אוסף של אובייקטים בפורמט JSON הוא מערך JSON. אתם מקבלים מערך JSON כתגובה משירות אינטרנט.
  • המפתחות בצמד מפתח/ערך מוקפים במירכאות. הערכים יכולים להיות מספרים או מחרוזות. גם מחרוזות מוקפות במירכאות.
  • הספרייה Moshi היא כלי לניתוח JSON ב-Android שממיר מחרוזת JSON לאובייקטים של Kotlin. ל-Retrofit יש ממיר שפועל עם Moshi.
  • ‫Moshi מתאים את המפתחות בתגובת JSON למאפיינים באובייקט נתונים עם אותו שם.
  • כדי להשתמש בשם מאפיין אחר למפתח, מוסיפים למאפיין את ההערה @Json ואת שם מפתח ה-JSON.

Retrofit וקורוטינות

  • מתאמי קריאות מאפשרים ל-Retrofit ליצור ממשקי API שמחזירים משהו אחר מלבד מחלקת ברירת המחדל Call. משתמשים במחלקה CoroutineCallAdapterFactory כדי להחליף את Call בקורוטינה Deferred.
  • משתמשים בשיטה await() באובייקט Deferred כדי לגרום לקוד של הקורוטינה להמתין בלי לחסום עד שהערך יהיה מוכן, ואז הערך מוחזר.

קורס ב-Udacity:

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

תיעוד של Kotlin:

אחר:

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

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

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

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

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

שאלה 1

מהם שני הדברים העיקריים שנדרשים ל-Retrofit כדי ליצור API של שירותי אינטרנט?

‫▢ ה-URI הבסיסי של שירות האינטרנט, ושאילתת GET.

‫▢ URI הבסיסי של שירות האינטרנט, ומפעל המרות.

‫▢ חיבור לרשת לשירות האינטרנט ואסימון הרשאה.

‫▢ מפעל המרה ומנתח לתגובה.

שאלה 2

מה המטרה של ספריית Moshi?

‫▢ כדי לקבל נתונים משירות אינטרנט.

‫▢ כדי ליצור אינטראקציה עם Retrofit כדי לשלוח בקשה לשירות אינטרנט.

‫▢ כדי לנתח תגובת JSON משירות אינטרנט לאובייקטים של נתונים ב-Kotlin.

‫▢ כדי לשנות את השם של אובייקטים ב-Kotlin כך שיתאימו למפתחות בתגובת ה-JSON.

שאלה 3

למה משמשים מתאמי קריאות של Retrofit?

‫▢ הם מאפשרים ל-Retrofit להשתמש ב-coroutines.

‫▢ הם מתאימים את התגובה של שירות האינטרנט לאובייקטים של נתונים ב-Kotlin.

▢ הם משנים קריאה ל-Retrofit לקריאה לשירות אינטרנט.

‫▢ הם מוסיפים את האפשרות להחזיר משהו שאינו מחלקת ברירת המחדל Call ב-Retrofit.

עוברים לשיעור הבא: 8.2 טעינה והצגה של תמונות מהאינטרנט

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