שימוש ב-Cotlin Coroutines באפליקציית Android

במעבדה הזו תוכלו ללמוד איך להשתמש ב-Kotlin Coroutines באפליקציה ל-Android – דרך חדשה לנהל שרשורי רקע פשוטים יותר כדי להפחית את הצורך בקריאות חוזרות (callbacks). קורסטים הם תכונה של Kotlin שממירה קריאה חוזרת (callback) אסינכרונית למשימות ארוכות, כגון גישה למסד נתונים או לרשת, לקוד רציף.

הנה קטע קוד שיספק לך מושג לגבי הפעולות שלך&#39.

// Async callbacks
networkRequest { result ->
   // Successful network request
   databaseSave(result) { rows ->
     // Result saved
   }
}

הקוד שמבוסס על קריאה חוזרת (callback) יומר לקוד רציף באמצעות פונקציות coroutine.

// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved

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

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

דרישות מוקדמות

  • היכרות עם רכיבי הארכיטקטורה ViewModel, LiveData, Repository ו-Room.
  • חוויה עם תחביר של Kotlin, כולל פונקציות של תוספים ומלבדות.
  • הבנה בסיסית של שימוש בשרשורים ב-Android, כולל השרשור הראשי, שרשורים ברקע וקריאות חוזרות (callbacks).

מה צריך לעשות

  • קוד שיחה כתוב באמצעות פונקציות corouts ומשיגים תוצאות.
  • יש להשתמש בפונקציות בהשעיה כדי להפוך את קוד האסינכרוני לרצף.
  • יש להשתמש ב-launch וב-runBlocking כדי לשלוט בביצועי הקוד.
  • בקישור הבא ניתן ללמוד איך להשתמש ב-suspendCoroutine כדי להמיר את ממשקי ה-API הקיימים לתרחישים.
  • שימוש בשגרות עם רכיבי ארכיטקטורה.
  • מידע על שיטות מומלצות לבדיקת שגרים.

מה תצטרך להכין

  • Android 3.5 (ייתכן ש-Codelab יפעל עם גרסאות אחרות, אך ייתכן שחלק מהרכיבים יהיו חסרים או ייראו שונים).

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

להורדת הקוד

כדי להוריד את כל הקוד של Lablab זה, צריך ללחוץ על הקישור הבא:

להורדת קובץ Zip

... או משכפלים את המאגר של GitHub משורת הפקודה באמצעות הפקודה הבאה:

$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git

שאלות נפוצות

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

  1. אם הורדת את קובץ ה-ZIP של kotlin-coroutines, עליך לחלץ את הקובץ.
  2. פותחים את הפרויקט coroutines-codelab ב-Android Studio.
  3. יש לבחור את מודול האפליקציה start.
  4. לוחצים על הלחצן exe.pngהפעלה, ובוחרים אמולטור או מחברים את מכשיר ה-Android, שתומך ב-Android Lollipop (ה-SDK המינימלי הנתמך הוא 21). המסך Kotlin Coroutine צריך להופיע:

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

האפליקציה הזו משתמשת ברכיבי ארכיטקטורה כדי להפריד את קוד ממשק המשתמש ב-MainActivity מהלוגיקה של האפליקציה ב-MainViewModel. מומלץ להקדיש כמה רגעים כדי להכיר את מבנה הפרויקט.

  1. בממשק המשתמש של MainActivity מוצג רישום של ממשק המשתמש, רישום האזנה לקליקים והצגת Snackbar. פעולה זו מעבירה את האירועים אל MainViewModel ומעדכנת את המסך על סמך LiveData באפליקציה MainViewModel.
  2. MainViewModel יטפל באירועים בonMainViewClicked וי לתקשר עם MainActivity באמצעות LiveData.
  3. האפליקציה Executors מגדירה את BACKGROUND, שיכול להפעיל שרשורים ברקע.
  4. TitleRepository מאחזר תוצאות מהרשת ושומר אותן במסד הנתונים.

הוספת פונקציות coroutines לפרויקט

כדי להשתמש בשגרות בקוט קוטין, עליך לכלול את הספרייה coroutines-core בקובץ build.gradle (Module: app) של הפרויקט. הפרויקטים של Codelab כבר עשו את זה בשבילך, לכן אין צורך לעשות זאת כדי להשלים את שיעור ה-Codelab.

'שגרות' ב-Android זמינות כספריית ליבה ותוספים ספציפיים ל-Android:

  • kotlinx-corountines-core – ממשק ראשי לשימוש בשגרות
  • kotlinx-coroutines-android – תמיכה בשרשור הראשי של Android בפונקציות coroutine

האפליקציה למתחילים כבר כוללת את התלות ב-build.gradle. בעת יצירת פרויקט אפליקציה חדש, צריך לפתוח את build.gradle (Module: app) ולהוסיף את יחסי התלות של הפרויקט לפרויקט הזה.

dependencies {
  ...
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}

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

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

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

דפוס הקריאה החוזרת

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

לפניכם דוגמה לדפוס של קריאה חוזרת (callback).

// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
    // The slow network request runs on another thread
    slowFetch { result ->
        // When the result is ready, this callback will get the result
        show(result)
    }
    // makeNetworkRequest() exits after calling slowFetch without waiting for the result
}

מכיוון שהקוד הזה מסומן בהערה @UiThread, הוא צריך לפעול מהר מספיק כדי לבצע אותו בשרשור הראשי. כלומר, העדכון צריך לחזור מהר מאוד, כך שעדכון המסך הבא לא מתעכב. עם זאת, מאחר שהשלמת הפעולה של slowFetch תימשך כמה שניות או אפילו דקות ספורות, השרשור הראשי לא יוכל להמתין לתוצאה. הקריאה החוזרת (callback) של show(result) מאפשרת ל-slowFetch לרוץ בשרשור רקע ולהחזיר את התוצאה כאשר היא מוכנה.

שימוש בפונקציות Corouts להסרת קריאה חוזרת (callback)

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

קוטריטים של Kotlin מאפשרים לכם להמיר קוד מבוסס קריאה חוזרת (callback) לקוד רציף. לרוב, קל יותר לקרוא את הקוד ברצף, ואפילו ניתן להשתמש בפיצ'רים של השפה, כמו חריגים.

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

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

לדוגמה: בקוד שבהמשך, הפונקציה makeNetworkRequest() והפונקציה slowFetch() הן פונקציות suspend.

// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
    // slowFetch is another suspend function so instead of 
    // blocking the main thread  makeNetworkRequest will `suspend` until the result is 
    // ready
    val result = slowFetch()
    // continue to execute after the result is ready
    show(result)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }

בדיוק כמו בגרסת השיחה החוזרת, makeNetworkRequest חייב לחזור מיד מהשרשור הראשי כי היא מסומנת ב-@UiThread. כלומר, בדרך כלל הוא לא יכול לקרוא לשיטות חסימה כמו slowFetch. כאן מילת המפתח suspend עובדת בצורה קסומה.

בהשוואה לקוד מבוסס על קריאה חוזרת (callback), קוד הקוטב'ן מקבל את אותה תוצאה של ביטול חסימת השרשור הנוכחי באמצעות פחות קוד. בגלל הסגנון הרציף שלה, קל ליצור רצף של כמה משימות ארוכות בלי ליצור כמה קריאות חוזרות (callbacks). לדוגמה, קוד שמאחזר תוצאה משתי נקודות קצה ברשת ושומר אותו במסד הנתונים יכול להיכתב כפונקציה בפונקציות corouts ללא קריאה חוזרת (callback). כך עושים זאת:

// Request data from network and save it to database with coroutines

// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
    // slowFetch and anotherFetch are suspend functions
    val slow = slowFetch()
    val another = anotherFetch()
    // save is a regular function and will block this thread
    database.save(slow, another)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }

בקטע הבא יצוינו קורסים לאפליקציה לדוגמה.

בתרגיל הזה יש לכתוב שגרה חדשה שמציגה הודעה לאחר עיכוב. כדי להתחיל, יש לוודא שהמודול start פתוח ב-Android Studio.

הסבר על CoroutineScope

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

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

שימוש ב-viewModelScope

ספריית lifecycle-viewmodel-ktxX ב-Android מוסיפה CoroutineScope ל-ViewModels, שהוגדרו להתחיל קורסים הקשורים לממשק המשתמש. כדי להשתמש בספרייה הזו, עליך לכלול אותה בקובץ build.gradle (Module: start) של הפרויקט שלך. השלב הזה כבר מתבצע בפרויקטים של Codelab.

dependencies {
  ...
  implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x"
}

הספרייה מוסיפה viewModelScope כפונקציית הרחבה של המחלקה ViewModel. ההיקף הזה מוגבל אל Dispatchers.Main, והוא יבוטל באופן אוטומטי לאחר ניקוי ה-ViewModel.

מעבר משרשורים לשגרות

ב-MainViewModel.kt מחפשים את השלמת הפעולה הבאה, לצד הקוד הבא:

PrimaryViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   BACKGROUND.submit {
       Thread.sleep(1_000)
       _taps.postValue("$tapCount taps")
   }
}

הקוד הזה משתמש ב-BACKGROUND ExecutorService (המוגדר ב-util/Executor.kt) כדי לפעול בשרשור ברקע. מאחר ש-sleep חוסם את השרשור הנוכחי, הוא יקפא את ממשק המשתמש אם הוא נקרא בשרשור הראשי. שנייה אחת לאחר שהמשתמש לוחץ על התצוגה הראשית, היא מבקשת מזנון.

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

PrimaryViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   Thread.sleep(1_000)
   _taps.postValue("$tapCount taps")
}

יש להחליף את updateTaps בקוד המבוסס על corouting, שמבצע את אותה הפעולה. יהיה עליך לייבא את launch ואת delay.

PrimaryViewModel.kt

/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
   // launch a coroutine in viewModelScope
   viewModelScope.launch {
       tapCount++
       // suspend this coroutine for one second
       delay(1_000)
       // resume in the main dispatcher
       // _snackbar.value can be called directly from main thread
       _taps.postValue("$tapCount taps")
   }
}

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

  1. viewModelScope.launch יתחיל שגרת המשך ב-viewModelScope. המשמעות היא שכאשר מבטלים את העבודה שהעברנו אל viewModelScope, כל המשימות שנעשה בהן עבודה או את ההיקף הזה יבוטלו. אם המשתמש עוזב את הפעילות לפני ש-delay חוזר, ההוראה הזו תבוטל באופן אוטומטי כשהקריאה ל-onCleared תתבצע עם השמדת ה-ViewModel.
  2. מאחר שלviewModelScope יש סדרן ברירת מחדל של Dispatchers.Main, הקורטין הזה יושק בשרשור הראשי. בהמשך נראה איך להשתמש בשרשורים שונים.
  3. הפונקציה delay היא פונקציה suspend. הנתון הזה מוצג ב-Android Studio על ידי הסמל במרזב הימני. למרות שהקורונה פועלת בשרשור הראשי, delay לא יחסום את השרשור למשך שנייה אחת. במקום זאת, סדרן העבודה יתוזמן את המשך הפעולה למשך שנייה אחת בהודעה הבאה.

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

בקטע הבא נסביר איך לבדוק את הפונקציה הזו.

בתרגיל הזה יש לכתוב בדיקה של הקוד שכתבתם. התרגיל הזה מראה לך איך לבדוק שגרים שפועלים בתאריך Dispatchers.Main באמצעות ספריית kotlinx-coroutines-test. בהמשך שיעור ה-Codelab הזה תטמיעו בדיקה של אינטראקציה עם פונקציות.

בדיקת הקוד הקיים

פתיחת MainViewModelTest.kt בתיקייה androidTest.

PrimaryViewModelTest.kt

class MainViewModelTest {
   @get:Rule
   val coroutineScope =  MainCoroutineScopeRule()
   @get:Rule
   val instantTaskExecutorRule = InstantTaskExecutorRule()

   lateinit var subject: MainViewModel

   @Before
   fun setup() {
       subject = MainViewModel(
           TitleRepository(
                   MainNetworkFake("OK"),
                   TitleDaoFake("initial")
           ))
   }
}

כלל הוא דרך להריץ קוד לפני ואחרי ביצוע בדיקה ב-JUNIT. שני כללים מאפשרים לנו לבדוק את CentralViewModel בבדיקה מחוץ למכשיר:

  1. InstantTaskExecutorRule הוא כלל JUNIT שמגדיר את LiveData לביצוע כל משימה באופן סינכרוני
  2. MainCoroutineScopeRule הוא כלל מותאם אישית בבסיס הקוד הזה שמגדיר את Dispatchers.Main כך שישתמש ב-TestCoroutineDispatcher מ-kotlinx-coroutines-test. כך הבדיקות יכולות לקדם שעון וירטואלי לבדיקות, והקוד יכול להשתמש ב-Dispatchers.Main בבדיקות של יחידות.

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

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

כתיבת בדיקה ששולטת בשגרות

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

PrimaryViewModelTest.kt

@Test
fun whenMainClicked_updatesTaps() {
   subject.onMainViewClicked()
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
   coroutineScope.advanceTimeBy(1000)
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}

בהתקשרות אל onMainViewClicked יושק הקורטוט שלנו שיצרנו. בבדיקה הזאת הטקסט של ההקשות נשאר "0 הקשות" מיד לאחר הקריאה ל-onMainViewClicked, ואז שנייה אחת מאוחר יותר הוא מתעדכן ל- "1 Taps".

הבדיקה הזו משתמשת בזמן וירטואלי כדי לשלוט בהטמעת הקורטינה שהושקה על ידי onMainViewClicked. ה-MainCoroutineScopeRule מאפשר השהיה, המשך ושליטה בהפעלה של קורסים שמופעלים ב-Dispatchers.Main. כאן אנחנו מתקשרים אל advanceTimeBy(1_000) ולגרום למוקדן הראשי לבצע באופן מיידי קורסים שמתוזמנים להמשיך שנייה אחת מאוחר יותר.

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

הפעלת הבדיקה הקיימת

  1. אפשר ללחוץ לחיצה ימנית על שם הכיתה MainViewModelTest בעורך כדי לפתוח תפריט הקשר.
  2. בתפריט ההקשר, בוחרים באפשרות exe.pngRun 'PrimaryViewModelTest'
  3. בהפעלות עתידיות ניתן יהיה לבחור את תצורת הבדיקה הזו בתצורות שלצד הלחצן exe.png בסרגל הכלים. כברירת מחדל, ההגדרה תיקרא PrimaryViewModelTest.

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

בתרגיל הבא נסביר איך להשתמש בממשקי API קיימים של קריאה חוזרת (callback) כדי להשתמש בשגרות.

בשלב זה תתחילו להמיר מאגר כדי להשתמש בשגרות. כדי לעשות זאת, אנחנו נוסיף קורסים וירטואליים לViewModel, ל-Repository, ל-Room ול-Retrofit.

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

  1. המערכת של MainDatabase מיישמת מסד נתונים באמצעות חדר ששומר וטוען Title.
  2. האפליקציה MainNetwork מטמיעה API של רשת שמאחזרת כותרת חדשה. הוא משתמש ב-Retrofit כדי לאחזר כותרים. האפליקציה Retrofit מוגדרת להחזיר שגיאות או נתונים מדומים באופן אקראי, אבל אחרת היא פועלת כאילו היא שולחת בקשות רשת אמיתיות.
  3. ב-TitleRepository מיושם API אחד לאחזור או לרענון הכותרת על ידי שילוב נתונים מהרשת וממסד הנתונים.
  4. MainViewModel מייצג את מצב המסך ומטפל באירועים. פעולה זו מורה למאגר לרענן את הכותרת כאשר המשתמש מקיש על המסך.

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

גרסת הקריאה החוזרת

יש לפתוח את MainViewModel.kt כדי לראות את ההצהרה של refreshTitle.

PrimaryViewModel.kt

/**
* Update title text via this LiveData
*/
val title = repository.title


// ... other code ...


/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   // TODO: Convert refreshTitle to use coroutines
   _spinner.value = true
   repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
       override fun onCompleted() {
           _spinner.postValue(false)
       }

       override fun onError(cause: Throwable) {
           _snackBar.postValue(cause.message)
           _spinner.postValue(false)
       }
   })
}

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

ההטמעה הזאת מבוססת על קריאה חוזרת (callback) לביצוע כמה פעולות:

  • לפני התחלת שאילתה, מוצג סמל טעינה עם _spinner.value = true
  • כשהתוצאה מתקבלת, היא מנקה את סמל הביצוע של הטעינה באמצעות _spinner.value = false
  • אם מופיעה הודעת שגיאה, היא מנחה את סרגל הצד להציג ומנקה את הסמל

חשוב לציין שהקריאה החוזרת (onCompleted) של השיחה החוזרת לא עוברת את title. מכיוון שאנחנו כותבים את כל הכותרים במסד הנתונים של Room, ממשק המשתמש מתעדכן לכותרת הנוכחית על ידי צפייה ב-LiveData שעודכן על ידי Room.

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

גרסת coroutines

רוצה לכתוב מחדש את refreshTitle באמצעות שגרות?

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

TitleRepository.kt

suspend fun refreshTitle() {
    // TODO: Refresh from network and write to database
    delay(500)
}

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

ב-MainViewModel, מחליפים את גרסת הקריאה החוזרת (refreshTitle) בגרסה שמפעילה שגרה חדשה:

PrimaryViewModel.kt

/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           repository.refreshTitle()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

נעבור על הפונקציה הזו:

viewModelScope.launch {

בדיוק כמו coroutin לעדכן את מספר ההקשות, מתחילים בהשקת שגרת המשך ב-viewModelScope. הפעולה הזו תשתמש בערך Dispatchers.Main תקין. למרות ש-refreshTitle יבצע בקשת רשת ושאילתת מסד נתונים, היא תוכל להשתמש בתרחישים (corouts) כדי לחשוף ממשק safe-safe. כלומר, אפשר להתקשר אליו מהשרשור הראשי.

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

שורות השורה הבאות של קוד מתקשרים בפועל אל refreshTitle בrepository.

try {
    _spinner.value = true
    repository.refreshTitle()
}

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

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

} catch (error: TitleRefreshError) {
    _snackBar.value = error.message
} finally {
    _spinner.value = false
}

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

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

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

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

בתרגיל הבא עליכם לעדכן את המאגר כדי לעבוד בפועל.

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

בדיקה של קוד הקריאה החוזרת (callback) ב-רענוןTitle

פותחים את TitleRepository.kt ובודקים את ההטמעה הקיימת של קריאה חוזרת (callback).

TitleRepository.kt

// TitleRepository.kt

fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
   // This request will be run on a background thread by retrofit
   BACKGROUND.submit {
       try {
           // Make network request using a blocking call
           val result = network.fetchNextTitle().execute()
           if (result.isSuccessful) {
               // Save it to database
               titleDao.insertTitle(Title(result.body()!!))
               // Inform the caller the refresh is completed
               titleRefreshCallback.onCompleted()
           } else {
               // If it's not successful, inform the callback of the error
               titleRefreshCallback.onError(
                       TitleRefreshError("Unable to refresh title", null))
           }
       } catch (cause: Throwable) {
           // If anything throws an exception, inform the caller
           titleRefreshCallback.onError(
                   TitleRefreshError("Unable to refresh title", cause))
       }
   }
}

ב-TitleRepository.kt השיטה refreshTitleWithCallbacks מוטמעת עם קריאה חוזרת (callback) כדי להעביר למתקשר את מצב הטעינה והשגיאה.

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

  1. מעבר לשרשור אחר עם BACKGROUND ExecutorService
  2. הפעלה של בקשת הרשת fetchNextTitle באמצעות שיטת החסימה execute(). הפעולה הזו תפעיל את בקשת הרשת בשרשור הנוכחי, במקרה זה, אחד מהשרשורים בBACKGROUND.
  3. אם התוצאה מוצלחת, יש לשמור אותה במסד הנתונים עם insertTitle ולהתקשר לשיטה onCompleted().
  4. אם התוצאה לא הייתה מוצלחת, או אם קיימת חריגה, התקשרו לשיטת onError כדי להודיע למתקשר על רענון שלא בוצע.

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

התקשרות לשיחות ממספר coroutines

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

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

כדי לעבור בין כל סדרות תקשורת, Coroutine משתמש/ת ב-withContext. קריאה לwithContext מעבירה אל המוקדן האחר רק עבור המלבדה ואז חוזרת למוקדן ש השם שלו היה התוצאה של אותה למדה.

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

TitleRepository.kt

suspend fun refreshTitle() {
   // interact with *blocking* network and IO calls from a coroutine
   withContext(Dispatchers.IO) {
       val result = try {
           // Make network request using a blocking call
           network.fetchNextTitle().execute()
       } catch (cause: Throwable) {
           // If the network throws an exception, inform the caller
           throw TitleRefreshError("Unable to refresh title", cause)
       }
      
       if (result.isSuccessful) {
           // Save it to database
           titleDao.insertTitle(Title(result.body()!!))
       } else {
           // If it's not successful, inform the callback of the error
           throw TitleRefreshError("Unable to refresh title", null)
       }
   }
}

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

הקוד הזה עדיין משתמש בחסימת שיחות. קריאה ל-execute() ול-insertTitle(...) תחסום את השרשור שבו פועל התרחיש הזה. עם זאת, המעבר ל-Dispatchers.IO באמצעות withContext גורם לחסימה של אחד מהשרשורים במחלקת ה-IO. הקורטין שקראו לכך, העשוי לפעול ב-Dispatchers.Main, יושעה עד שwithContext הלמבה תושלם.

בהשוואה לגרסת הקריאה החוזרת, יש שני הבדלים חשובים:

  1. במקרה כזה, withContext מחזירה את התוצאה למוקדן שהתקשר אליה. במקרה זה, Dispatchers.Main. הגרסה של הקריאה החוזרת (callback) כוללת קריאות חוזרות (callback) בשרשור בשירות הביצוע של BACKGROUND.
  2. המתקשר לא צריך להעביר קריאה חוזרת לפונקציה הזו. הם יכולים להסתמך על ההשעיה ולהמשיך אותה כדי לקבל את התוצאה או השגיאה.

הפעלה חוזרת של האפליקציה

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

בשלב הבא, תשלבו שגרים בחשבונות שלכם – ב'חדר' וב'רטרופיט'.

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

שגרות בחדר

פותחים את MainDatabase.kt והופכים את insertTitle לפונקציית השעיה:

PrimaryDatabase.kt

// add the suspend modifier to the existing insertTitle

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)

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

וזה כל מה שצריך לעשות כדי להשתמש בשגרות ב'חדר'. יפה יפה.

שגרות רטרופיט

בשלב הבא, נראה איך לשלב שגרות עם רטרופיט. פותחים את MainNetwork.kt ומשנים את הפונקציה fetchNextTitle לפונקציית השעיה.

CentralNetwork.kt

// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String

interface MainNetwork {
   @GET("next_title.json")
   suspend fun fetchNextTitle(): String
}

כדי להשתמש בפונקציות השעה ב-Retrofit, עליך לבצע שתי פעולות:

  1. הוספת מגביל התאמה לפונקציה
  2. יש להסיר את wrapper של Call מסוג ההחזרה. כאן אנחנו מחזירים את String, אבל אפשר גם להחזיר סוג מורכב של גיבוי מסוג json. אם עדיין ברצונך להעניק גישה מלאה ל-Result, אפשר להחזיר Result<String> במקום String מפונקציית ההשעיה.

מערכת Retrofit תגדיר באופן אוטומטי את פונקציות ההשעיה כראשיות, כדי שניתן יהיה להתקשר אליהן ישירות מ-Dispatchers.Main.

שימוש ב'חדר' ורטרואקטיבית

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

שם הפריטRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

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

עוד יותר טוב, נפטרנו מהwithContext. מאחר ש'חדר' ו'רטרופיט' מספקים פונקציות השעיה חשובות, אפשר לתמלל את העבודה האסינכרונית הזו מ-Dispatchers.Main.

תיקון שגיאות במהדר

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

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

TestingFakes.kt

מעדכנים את הזיוף לבדיקה כדי לתמוך בהתאמות החדשות להשעיה.

TitleDaoFake

  1. מקישים על Enter-Enter כדי להוסיף מגבילי השעיה לכל הפונקציות בהיררכיה

PrimaryNetworkFake

  1. מקישים על Enter-Enter כדי להוסיף מגבילי השעיה לכל הפונקציות בהיררכיה
  2. החלפת fetchNextTitle בפונקציה הזו
override suspend fun fetchNextTitle() = result

PrimaryNetworkCompletableFake

  1. מקישים על Enter-Enter כדי להוסיף מגבילי השעיה לכל הפונקציות בהיררכיה
  2. החלפת fetchNextTitle בפונקציה הזו
override suspend fun fetchNextTitle() = completable.await()

TitleRepository.kt

  • יש למחוק את הפונקציה refreshTitleWithCallbacks כי היא לא בשימוש יותר.

הפעלת האפליקציה

הפעילו את האפליקציה שוב, לאחר הידור שלה, תראו שהיא טוענת נתונים באמצעות תרחישים (corouts – כל הדרך, מ-ViewModel לחדר)

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

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

מאחר ש-refreshTitle חשוף כ-API ציבורי, הוא ייבדק ישירות כדי להראות כיצד להפעיל פונקציות קורינתיות מבדיקות.

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

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

כתיבת בדיקה שמפעילה פונקציית השעיה

פתח את TitleRepositoryTest.kt בתיקייה test שיש בה שתי rfcS.

אפשר לנסות להתקשר אל refreshTitle מהבדיקה הראשונה whenRefreshTitleSuccess_insertsRows.

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   subject.refreshTitle()
}

מכיוון ש-refreshTitle היא פונקציה suspend ש-Kotlin לא יודעת איך לקרוא לה, מלבד פונקציית coroutine או פונקציית השעיה אחרת, תתקבל שגיאת מהדר, כמו &PLURAL;

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

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   // launch starts a coroutine then immediately returns
   GlobalScope.launch {
       // since this is asynchronous code, this may be called *after* the test completes
       subject.refreshTitle()
   }
   // test function returns immediately, and
   // doesn't see the results of refreshTitle
}

הבדיקה הזו לפעמים תיכשל. הקריאה אל launch תחזור ותתבצע בו-זמנית עם שאר הפנייה. בבדיקה אין דרך לדעת אם refreshTitle כבר פועל או לא, וכל הצהרה כמו עדכון של מסד הנתונים עודכנה תהיה מרוככת. אם הערך refreshTitle השליך אותו, הוא לא ימוקם בערימת השיחות. במקום זאת הוא ימוקם ב-handler של GlobalScope&#39 שלא נתפס.

הספרייה kotlinx-coroutines-test מכילה את הפונקציה runBlockingTest שחוסמת בזמן הקריאה לפונקציות בהשעיה. כאשר runBlockingTest מפעיל פונקציית השעיה או launches שגרת חדשה, היא מפעילה אותה כברירת מחדל. אפשר לחשוב על זה כדרך להמיר פונקציות להשעיית יומן וקריאה לפעולות פונקציות רגילות.

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

הטמעת בדיקה באמצעות שגרת שגרה

גלישת השיחה אל refreshTitle עם runBlockingTest והסרת ה-wrapper של GlobalScope.launch מ-topic.refreshTitle().

TitleRepositoryTest.kt

@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
   val titleDao = TitleDaoFake("title")
   val subject = TitleRepository(
           MainNetworkFake("OK"),
           titleDao
   )

   subject.refreshTitle()
   Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}

בדיקה זו משתמשת בזיוף שצוין כדי לבדוק שהערך "OK" הוכנס למסד הנתונים עד refreshTitle.

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

לאחר סיום הקורטיה, runBlockingTest יחזור.

כתיבת בדיקה של זמן קצוב לתפוגה

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

TitleRepositoryTest.kt

@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
   val network = MainNetworkCompletableFake()
   val subject = TitleRepository(
           network,
           TitleDaoFake("title")
   )

   launch {
       subject.refreshTitle()
   }

   advanceTimeBy(5_000)
}

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

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

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

כדאי להריץ אותו עכשיו ולבדוק מה קורה:

Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]

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

הוספת זמן קצוב לתפוגה

יש לפתוח את TitleRepository ולהוסיף זמן קצוב לתפוגה של חמש שניות באחזור הרשת. אפשר לעשות זאת באמצעות הפונקציה withTimeout:

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = withTimeout(5_000) {
           network.fetchNextTitle()
       }
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

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

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

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

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

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

// MainViewModel.kt

fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           // this is the only part that changes between sources
           repository.refreshTitle() 
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

שימוש בפונקציות Corouting בפונקציות ברמה גבוהה יותר

הוספת הקוד הזה ל-PrimaryViewModel.kt

PrimaryViewModel.kt

private fun launchDataLoad(block: suspend () -> Unit): Job {
   return viewModelScope.launch {
       try {
           _spinner.value = true
           block()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

עכשיו צריך לשלב מחדש את refreshTitle() כדי להשתמש בפונקציית ההזמנה הגבוהה יותר.

PrimaryViewModel.kt

fun refreshTitle() {
   launchDataLoad {
       repository.refreshTitle()
   }
}

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

כדי ליצור את ההפשטה הזו, launchDataLoad לוקחת ארגומנט block שהוא למדה של השעיה. למדת השעיה מאפשרת לכם לקרוא לפונקציות ההשעיה. כך קוטן מטמיע את בוני הקוטג'ים launch ו-runBlocking שבהם אנחנו משתמשים במעבדה זו.

// suspend lambda

block: suspend () -> Unit

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

לעתים קרובות אין צורך להצהיר על טלה למברמה משלך, אבל השימוש בהן יכול לעזור ליצור הפשטות כאלה הכוללות לוגיקה חוזרת!

בתרגיל הזה תלמדו איך להשתמש בקוד המבוסס על שגרה דרך Work Manager.

מה זה WorkManager

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

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

לכן, WorkManager הוא אפשרות טובה לביצוע משימות שצריכות להתבצע בסופו של דבר.

כמה דוגמאות למשימות שבהן אפשר להשתמש ב-WorkManager:

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

שימוש בשגרות עם WorkManager

WorkManager מספק הטמעות שונות של הכיתה ListanableWorker מסוג זה לתרחישים שונים.

קורס Worker הפשוט ביותר מאפשר לנו לבצע פעולה סינכרונית מצד WorkManager. עם זאת, עד שעבדנו עד עכשיו כדי להמיר את בסיס הקוד שלנו כדי להשתמש בפונקציות קורינתיות ולהשעות פונקציות, הדרך הטובה ביותר להשתמש ב-WorkManager היא באמצעות הכיתה CoroutineWorker שמאפשרת להגדיר את הפונקציה doWork() כפונקציית השעיה.

כדי להתחיל, יש לפתוח את RefreshMainDataWork. הוא כבר נרחב ב-CoroutineWorker ויש להטמיע את doWork.

בפונקציה doWork של suspend, יש להתקשר אל refreshTitle() מהמאגר ולהחזיר את התוצאה המתאימה!

לאחר שתסיימו את כל ההוראות, הקוד ייראה כך:

override suspend fun doWork(): Result {
   val database = getDatabase(applicationContext)
   val repository = TitleRepository(network, database.titleDao)

   return try {
       repository.refreshTitle()
       Result.success()
   } catch (error: TitleRefreshError) {
       Result.failure()
   }
}

חשוב לשים לב שהפונקציה CoroutineWorker.doWork() היא השעיה. בניגוד לסיווג פשוט יותר של Worker, הקוד הזה לא פועל אצל האופרטור שצוין בהגדרת WorkManager, אלא משתמש בשולח בסדרת חברים ב-coroutineContext (ברירת מחדל Dispatchers.Default).

בדיקת CoroutineWorker

אין צורך להשלים קוד בסיס ללא בדיקה.

WorkManager מציע כמה דרכים שונות לבדוק את הכיתות שלך ב-Worker. לקבלת מידע נוסף על תשתית הבדיקה המקורית, ניתן לקרוא את התיעוד.

גרסה ListenableWorker בקוד שלנו נשתמש באחד מממשקי ה-API החדשים האלה: TestListenableWorkerBuilder.

כדי להוסיף את הבדיקה החדשה, יש לעדכן את הקובץ RefreshMainDataWorkTest שבתיקייה androidTest.

תוכן הקובץ הוא:

package com.example.android.kotlincoroutines.main

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4


@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {

@Test
fun testRefreshMainDataWork() {
   val fakeNetwork = MainNetworkFake("OK")

   val context = ApplicationProvider.getApplicationContext<Context>()
   val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
           .setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
           .build()

   // Start the work synchronously
   val result = worker.startWork().get()

   assertThat(result).isEqualTo(Result.success())
}

}

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

בניסוי עצמו נעשה שימוש ב-TestListenableWorkerBuilder כדי ליצור את העובד שלנו שנוכל להריץ את השיטה startWork().

WorkManager הוא רק דוגמה אחת לאופן שבו ניתן להשתמש בשגרות שונות כדי לפשט את עיצוב ממשקי ה-API.

במעבדה הזו עברנו את העקרונות הבסיסיים שצריך להתחיל לעבוד עם קורסים באפליקציה!

הפרטים עוסקים בנושאים הבאים:

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

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

למידע נוסף

כדי לקבל מידע מתקדם יותר על שימוש בשגרות Android ב-Android, עיינו ב-"Advanced Coroutines with Kotlin Flow and LiveData" .

שגרת קוטלין כוללת תכונות רבות שלא נכללו במעבדת הקוד הזו. אם אתם רוצים לקבל מידע נוסף על שגרת קוטלין, כדאי לקרוא את המדריך של שגרת המשך שפורסם על ידי JetBrains. כמו כן, מומלץ לבדוק &"לשפר את ביצועי האפליקציה באמצעות שגרת Kotlin "כדי להשתמש בדפוסי שימוש נוספים של פונקציות coroutine ב-Android.