במעבדה הזו תוכלו ללמוד איך להשתמש ב-Kotlin Coroutines באפליקציה ל-Android – דרך חדשה לנהל שרשורי רקע פשוטים יותר כדי להפחית את הצורך בקריאות חוזרות (callbacks). קורסטים הם תכונה של Kotlin שממירה קריאה חוזרת (callback) אסינכרונית למשימות ארוכות, כגון גישה למסד נתונים או לרשת, לקוד רציף.
הנה קטע קוד שיספק לך מושג לגבי הפעולות שלך'.
// 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 זה, צריך ללחוץ על הקישור הבא:
... או משכפלים את המאגר של GitHub משורת הפקודה באמצעות הפקודה הבאה:
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
שאלות נפוצות
קודם כל, נראה איך נראית האפליקציה לדוגמה. יש לפעול לפי ההוראות האלה כדי לפתוח את האפליקציה לדוגמה ב-Android Studio.
- אם הורדת את קובץ ה-ZIP של
kotlin-coroutines
, עליך לחלץ את הקובץ. - פותחים את הפרויקט
coroutines-codelab
ב-Android Studio. - יש לבחור את מודול האפליקציה
start
. - לוחצים על הלחצן
הפעלה, ובוחרים אמולטור או מחברים את מכשיר ה-Android, שתומך ב-Android Lollipop (ה-SDK המינימלי הנתמך הוא 21). המסך Kotlin Coroutine צריך להופיע:
האפליקציה הזו למתחילים משתמשת בשרשורים כדי להוסיף ספירה של העיכוב הקצר אחרי שלוחצים על המסך. הוא גם יאחזר כותרת חדשה מהרשת ויציג אותה במסך. כדאי לנסות עכשיו, והעיכוב בהודעה אמור להשתנות לאחר זמן קצר. ב-codelab הזה יש להמיר את האפליקציה הזאת לשימוש בשגרות.
האפליקציה הזו משתמשת ברכיבי ארכיטקטורה כדי להפריד את קוד ממשק המשתמש ב-MainActivity
מהלוגיקה של האפליקציה ב-MainViewModel
. מומלץ להקדיש כמה רגעים כדי להכיר את מבנה הפרויקט.
- בממשק המשתמש של
MainActivity
מוצג רישום של ממשק המשתמש, רישום האזנה לקליקים והצגתSnackbar
. פעולה זו מעבירה את האירועים אלMainViewModel
ומעדכנת את המסך על סמךLiveData
באפליקציהMainViewModel
. MainViewModel
יטפל באירועים בonMainViewClicked
וי לתקשר עםMainActivity
באמצעותLiveData.
- האפליקציה
Executors
מגדירה אתBACKGROUND,
שיכול להפעיל שרשורים ברקע. 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-ktx
X ב-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")
}
}
הקוד הזה מבצע את אותה פעולה, בהמתנה שנייה אחת לפני הצגת קיוסק. עם זאת, יש כמה הבדלים חשובים:
viewModelScope.
launch
יתחיל שגרת המשך ב-viewModelScope
. המשמעות היא שכאשר מבטלים את העבודה שהעברנו אלviewModelScope
, כל המשימות שנעשה בהן עבודה או את ההיקף הזה יבוטלו. אם המשתמש עוזב את הפעילות לפני ש-delay
חוזר, ההוראה הזו תבוטל באופן אוטומטי כשהקריאה ל-onCleared
תתבצע עם השמדת ה-ViewModel.- מאחר של
viewModelScope
יש סדרן ברירת מחדל שלDispatchers.Main
, הקורטין הזה יושק בשרשור הראשי. בהמשך נראה איך להשתמש בשרשורים שונים. - הפונקציה
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 בבדיקה מחוץ למכשיר:
InstantTaskExecutorRule
הוא כלל JUNIT שמגדיר אתLiveData
לביצוע כל משימה באופן סינכרוני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
, היא לא צריכה להמתין שנייה אחת עד שהערך יוגדר.
הפעלת הבדיקה הקיימת
- אפשר ללחוץ לחיצה ימנית על שם הכיתה
MainViewModelTest
בעורך כדי לפתוח תפריט הקשר. - בתפריט ההקשר, בוחרים באפשרות
Run 'PrimaryViewModelTest'
- בהפעלות עתידיות ניתן יהיה לבחור את תצורת הבדיקה הזו בתצורות שלצד הלחצן
בסרגל הכלים. כברירת מחדל, ההגדרה תיקרא PrimaryViewModelTest.
אתם אמורים לראות את כרטיס הבדיקה! ההפעלה אמורה להימשך פחות משנייה.
בתרגיל הבא נסביר איך להשתמש בממשקי API קיימים של קריאה חוזרת (callback) כדי להשתמש בשגרות.
בשלב זה תתחילו להמיר מאגר כדי להשתמש בשגרות. כדי לעשות זאת, אנחנו נוסיף קורסים וירטואליים לViewModel
, ל-Repository
, ל-Room
ול-Retrofit
.
כדאי להבין מה אחראי על כל חלק בארכיטקטורה לפני שנעביר אותם לשימוש בשגרות.
- המערכת של
MainDatabase
מיישמת מסד נתונים באמצעות חדר ששומר וטועןTitle
. - האפליקציה
MainNetwork
מטמיעה API של רשת שמאחזרת כותרת חדשה. הוא משתמש ב-Retrofit כדי לאחזר כותרים. האפליקציהRetrofit
מוגדרת להחזיר שגיאות או נתונים מדומים באופן אקראי, אבל אחרת היא פועלת כאילו היא שולחת בקשות רשת אמיתיות. - ב-
TitleRepository
מיושם API אחד לאחזור או לרענון הכותרת על ידי שילוב נתונים מהרשת וממסד הנתונים. 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).
ואם אתם יוצאים מהכלל – מקורטין – ברירת המחדל הזו תבטל את ההורה של ההורה. כלומר, קל לבטל כמה משימות קשורות יחד.
לאחר מכן, בבלוק אחרון, נוכל לוודא שהספינר תמיד כבוי לאחר שהשאילתה רצה.
מריצים את האפליקציה שוב על ידי בחירה בתצורה התחלה ולאחר מכן הקשה על. אמורים להופיע סמל טעינה בעת ההקשה בכל מקום. שם הפריט יישאר ללא שינוי כי עדיין לא חידשנו את הרשת או את מסד הנתונים שלנו.
בתרגיל הבא עליכם לעדכן את המאגר כדי לעבוד בפועל.
בתרגיל הזה נלמד אותך איך לשנות את השרשור שבו פועל התרחיש, כדי להטמיע גרסה פעילה של 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) כדי להעביר למתקשר את מצב הטעינה והשגיאה.
פונקציה זו מבצעת מספר דברים כדי ליישם את הרענון.
- מעבר לשרשור אחר עם
BACKGROUND
ExecutorService
- הפעלה של בקשת הרשת
fetchNextTitle
באמצעות שיטת החסימהexecute()
. הפעולה הזו תפעיל את בקשת הרשת בשרשור הנוכחי, במקרה זה, אחד מהשרשורים בBACKGROUND
. - אם התוצאה מוצלחת, יש לשמור אותה במסד הנתונים עם
insertTitle
ולהתקשר לשיטהonCompleted()
. - אם התוצאה לא הייתה מוצלחת, או אם קיימת חריגה, התקשרו לשיטת 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
הלמבה תושלם.
בהשוואה לגרסת הקריאה החוזרת, יש שני הבדלים חשובים:
- במקרה כזה,
withContext
מחזירה את התוצאה למוקדן שהתקשר אליה. במקרה זה,Dispatchers.Main
. הגרסה של הקריאה החוזרת (callback) כוללת קריאות חוזרות (callback) בשרשור בשירות הביצוע שלBACKGROUND
. - המתקשר לא צריך להעביר קריאה חוזרת לפונקציה הזו. הם יכולים להסתמך על ההשעיה ולהמשיך אותה כדי לקבל את התוצאה או השגיאה.
הפעלה חוזרת של האפליקציה
אם תפעילו שוב את האפליקציה, תראו שההטמעה החדשה המבוססת על קורסטים טוענת את התוצאות מהרשת!
בשלב הבא, תשלבו שגרים בחשבונות שלכם – ב'חדר' וב'רטרופיט'.
כדי להמשיך בשילוב הפונקציות, נשתמש בפונקציות של ההשעיה בגרסה היציבה של החדר ושל 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, עליך לבצע שתי פעולות:
- הוספת מגביל התאמה לפונקציה
- יש להסיר את 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
- מקישים על Enter-Enter כדי להוסיף מגבילי השעיה לכל הפונקציות בהיררכיה
PrimaryNetworkFake
- מקישים על Enter-Enter כדי להוסיף מגבילי השעיה לכל הפונקציות בהיררכיה
- החלפת
fetchNextTitle
בפונקציה הזו
override suspend fun fetchNextTitle() = result
PrimaryNetworkCompletableFake
- מקישים על Enter-Enter כדי להוסיף מגבילי השעיה לכל הפונקציות בהיררכיה
- החלפת
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
' שלא נתפס.
הספרייה 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.