‫Android Kotlin Fundamentals 06.2: Coroutines and Room

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

מבוא

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

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

מה שכדאי לדעת

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

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

מה תלמדו

  • איך שרשורים פועלים ב-Android.
  • איך משתמשים ב-coroutines של Kotlin כדי להעביר פעולות במסד נתונים מה-thread הראשי.
  • איך מציגים נתונים מעוצבים ב-TextView.

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

  • הרחבת האפליקציה TrackMySleepQuality כדי לאסוף, לאחסן ולהציג נתונים במסד הנתונים וממנו.
  • משתמשים ב-coroutines כדי להריץ ברקע פעולות ארוכות טווח במסד נתונים.
  • משתמשים ב-LiveData כדי להפעיל את הניווט ואת התצוגה של חטיף.
  • משתמשים ב-LiveData כדי להפעיל ולהשבית את הלחצנים.

ב-codelab הזה, תבנו את מודל התצוגה, את שגרות המשנה ואת החלקים של הצגת הנתונים באפליקציה TrackMySleepQuality.

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

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

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

מסלול המשתמש הוא כדלקמן:

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

האפליקציה הזו משתמשת בארכיטקטורה פשוטה, כפי שמוצג בהמשך בהקשר של הארכיטקטורה המלאה. האפליקציה משתמשת רק ברכיבים הבאים:

  • בקר ממשק משתמש
  • הצגת הדגם ו-LiveData
  • מסד נתונים של Room

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

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

שלב 1: הורדה והפעלה של אפליקציית המתחילים

  1. מורידים את האפליקציה TrackMySleepQuality-Coroutines-Starter מ-GitHub.
  2. מבצעים Build ומריצים את האפליקציה. ממשק המשתמש של קטע SleepTrackerFragment מוצג באפליקציה, אבל לא מוצגים נתונים. הכפתורים לא מגיבים להקשה.

שלב 2: בודקים את הקוד

קוד ההתחלה של Codelab הזה זהה לקוד הפתרון של Codelab 6.1 בנושא יצירת מסד נתונים של Room.

  1. פותחים את הקובץ res/layout/activity_main.xml. הפריסה הזו מכילה את קטע הקוד nav_host_fragment. שימו לב גם לתג <merge>.

    אפשר להשתמש בתג merge כדי למנוע פריסות מיותרות כשמצרפים פריסות, ומומלץ להשתמש בו. דוגמה לפריסה מיותרת היא ConstraintLayout > LinearLayout > TextView, שבה המערכת יכולה לבטל את LinearLayout. אופטימיזציה כזו יכולה לפשט את היררכיית התצוגה ולשפר את ביצועי האפליקציה.
  2. בתיקייה navigation, פותחים את הקובץ navigation.xml. אפשר לראות שני פרגמנטים ואת פעולות הניווט שמקשרות ביניהם.
  3. בתיקייה layout, לוחצים לחיצה כפולה על רכיב sleep tracker כדי לראות את פריסת ה-XML שלו. חשוב לשים לב:
  • נתוני הפריסה עטופים ברכיב <layout> כדי לאפשר קישור נתונים.
  • ConstraintLayout והתצוגות האחרות מסודרות בתוך רכיב <layout>.
  • הקובץ מכיל תג placeholder ‏<data>.

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

עכשיו שיש לכם מסד נתונים וממשק משתמש, אתם צריכים לאסוף נתונים, להוסיף אותם למסד הנתונים ולהציג אותם. כל העבודה הזו מתבצעת במודל התצוגה. מודל התצוגה של מעקב השינה יטפל בלחיצות על הלחצנים, יקיים אינטראקציה עם מסד הנתונים באמצעות ה-DAO ויספק נתונים לממשק המשתמש באמצעות LiveData. כל פעולות מסד הנתונים יצטרכו להתבצע מחוץ ל-thread הראשי של ממשק המשתמש, ותוכלו לעשות זאת באמצעות קורוטינות.

שלב 1: מוסיפים את SleepTrackerViewModel

  1. בחבילה sleeptracker, פותחים את SleepTrackerViewModel.kt.
  2. בודקים את המחלקה SleepTrackerViewModel, שמסופקת באפליקציית המתחילים ומוצגת גם בהמשך. שימו לב שהכיתה מתרחבת AndroidViewModel(). המחלקה הזו זהה למחלקה ViewModel, אבל היא מקבלת את הקשר של האפליקציה כפרמטר ומאפשרת להשתמש בו כמאפיין. יהיה צורך במידע הזה בהמשך.
class SleepTrackerViewModel(
       val database: SleepDatabaseDao,
       application: Application) : AndroidViewModel(application) {
}

שלב 2: מוסיפים את SleepTrackerViewModelFactory

  1. בחבילה sleeptracker, פותחים את SleepTrackerViewModelFactory.kt.
  2. בודקים את הקוד שמופיע בהמשך עבור המפעל:
class SleepTrackerViewModelFactory(
       private val dataSource: SleepDatabaseDao,
       private val application: Application) : ViewModelProvider.Factory {
   @Suppress("unchecked_cast")
   override fun <T : ViewModel?> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(SleepTrackerViewModel::class.java)) {
           return SleepTrackerViewModel(dataSource, application) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
   }
}

חשוב לשים לב לנקודות הבאות:

  • הפונקציה SleepTrackerViewModelFactory שצוינה מקבלת את אותו ארגומנט כמו הפונקציה ViewModel ומרחיבה את הפונקציה ViewModelProvider.Factory.
  • בתוך הפונקציה factory, הקוד מבצע החלפה של create(), שמקבלת כל סוג של מחלקה כארגומנט ומחזירה ViewModel.
  • בגוף של create(), הקוד בודק אם יש מחלקה SleepTrackerViewModel זמינה, ואם יש, מחזיר מופע שלה. אחרת, הקוד יחזיר חריגה.

שלב 3: מעדכנים את SleepTrackerFragment

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

    הפונקציה requireNotNull Kotlin מחזירה IllegalArgumentException אם הערך הוא null.
val application = requireNotNull(this.activity).application
  1. צריך הפניה למקור הנתונים דרך הפניה ל-DAO. ב-onCreateView(), לפני return, מגדירים dataSource. כדי לקבל הפניה ל-DAO של מסד הנתונים, משתמשים ב-SleepDatabase.getInstance(application).sleepDatabaseDao.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. ב-onCreateView(), לפני return, יוצרים מופע של viewModelFactory. צריך להעביר את dataSource ואת application.
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
  1. עכשיו, אחרי שיש לכם מפעל, צריך לקבל הפניה אל SleepTrackerViewModel. הפרמטר SleepTrackerViewModel::class.java מתייחס למחלקת Java של האובייקט הזה בזמן הריצה.
val sleepTrackerViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)
  1. הקוד הסופי אמור להיראות כך:
// Create an instance of the ViewModel Factory.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

// Get a reference to the ViewModel associated with this fragment.
val sleepTrackerViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)

הנה שיטת onCreateView() עד עכשיו:

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        // Get a reference to the binding object and inflate the fragment views.
        val binding: FragmentSleepTrackerBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_sleep_tracker, container, false)

        val application = requireNotNull(this.activity).application

        val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao

        val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

        val sleepTrackerViewModel =
                ViewModelProviders.of(
                        this, viewModelFactory).get(SleepTrackerViewModel::class.java)

        return binding.root
    }

שלב 4: הוספת קשירת נתונים למודל התצוגה

אחרי שמגדירים את ViewModel הבסיסי, צריך להשלים את ההגדרה של קישור הנתונים ב-SleepTrackerFragment כדי לקשר את ViewModel לממשק המשתמש.


בקובץ הפריסה fragment_sleep_tracker.xml:

  1. בתוך הבלוק <data>, יוצרים <variable> שמפנה למחלקה SleepTrackerViewModel.
<data>
   <variable
       name="sleepTrackerViewModel"
       type="com.example.android.trackmysleepquality.sleeptracker.SleepTrackerViewModel" />
</data>

ב-SleepTrackerFragment:

  1. הגדרת הפעילות הנוכחית כבעלים של מחזור החיים של הקישור. מוסיפים את הקוד הזה בתוך השיטה onCreateView(), לפני ההצהרה return:
binding.setLifecycleOwner(this)
  1. מקצים את משתנה הקישור sleepTrackerViewModel ל-sleepTrackerViewModel. מציבים את הקוד הזה בתוך onCreateView(), מתחת לקוד שיוצר את SleepTrackerViewModel:
binding.sleepTrackerViewModel = sleepTrackerViewModel
  1. סביר להניח שתופיע שגיאה, כי צריך ליצור מחדש את אובייקט הקישור. כדי להיפטר מהשגיאה, צריך לנקות את הפרויקט ולבנות אותו מחדש.
  2. לסיום, כמו תמיד, חשוב לוודא שהקוד נבנה ופועל ללא שגיאות.

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

ל-coroutines יש את המאפיינים הבאים:

  • קורוטינות הן אסינכרוניות ולא חוסמות.
  • קורוטינות משתמשות בפונקציות suspend כדי להפוך קוד אסינכרוני לקוד רציף.

קורוטינות הן אסינכרוניות.

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

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

קורוטינות לא חוסמות.

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

קורוטינות משתמשות בפונקציות השהיה כדי להפוך קוד אסינכרוני לקוד רציף.

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

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

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

כדי להשתמש בקורוטינות ב-Kotlin, צריך שלושה דברים:

  • משרה
  • סדרן עבודה
  • היקף הרשאות

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

Dispatcher: ה-dispatcher שולח קורוטינות להרצה בשרשורים שונים. לדוגמה, Dispatcher.Main מריץ משימות ב-thread הראשי, ו-Dispatcher.IO מעביר משימות חוסמות של קלט/פלט למאגר משותף של threads.

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

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

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

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

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

כשלוחצים על הלחצן Start (התחלה) באפליקציה Sleep Tracker (מעקב שינה), רוצים להפעיל פונקציה ב-SleepTrackerViewModel כדי ליצור מופע חדש של SleepNight ולאחסן את המופע במסד הנתונים.

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

  1. פותחים את הקובץ build.gradle ברמת האפליקציה ומחפשים את התלויות של קורוטינות. כדי להשתמש בקורוטינות, צריך את יחסי התלות האלה, שכבר נוספו בשבילכם.

    ה-$coroutine_version מוגדר בקובץ build.gradle של הפרויקט כ-coroutine_version = '1.0.0'.
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
  1. פותחים את הקובץ SleepTrackerViewModel.
  2. בגוף המחלקה, מגדירים את viewModelJob ומקצים לו מופע של Job. המאפיין viewModelJob מאפשר לבטל את כל הקורוטינות שהופעלו על ידי מודל התצוגה הזה כשהוא כבר לא בשימוש ונהרס. כך לא יקרה מצב שבו קורוטינות לא יוכלו לחזור.
private var viewModelJob = Job()
  1. בסוף גוף המחלקה, מבטלים את onCleared() ומבטלים את כל הקורוטינות. כש-ViewModel נהרס, מתבצעת קריאה ל-onCleared().
override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. מגדירים uiScope לקורוטינות מיד מתחת להגדרה של viewModelJob. ההיקף קובע באיזה שרשור יפעל הקורוטינה, וההיקף צריך לדעת גם על העבודה. כדי לקבל היקף, צריך לבקש מופע של CoroutineScope ולהעביר משגר ועבודה.

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

private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
  1. מתחת להגדרה של uiScope, מגדירים משתנה בשם tonight שיכיל את הלילה הנוכחי. הופכים את המשתנה ל-MutableLiveData, כי צריך להיות אפשר לצפות בנתונים ולשנות אותם.
private var tonight = MutableLiveData<SleepNight?>()
  1. כדי לאתחל את המשתנה tonight בהקדם האפשרי, יוצרים בלוק init מתחת להגדרה של tonight ומפעילים את initializeTonight(). בשלב הבא תגדירו את initializeTonight().
init {
   initializeTonight()
}
  1. מתחת לבלוק init, מטמיעים את initializeTonight(). ב-uiScope, מפעילים קורוטינה. בתוך הפונקציה, מקבלים את הערך של tonight מהמסד הנתונים על ידי קריאה ל-getTonightFromDatabase(), ומקצים את הערך ל-tonight.value. בשלב הבא תגדירו את getTonightFromDatabase().
private fun initializeTonight() {
   uiScope.launch {
       tonight.value = getTonightFromDatabase()
   }
}
  1. הטמעה של getTonightFromDatabase(). מגדירים אותה כפונקציה private suspend שמחזירה ערך SleepNight שניתן לאכלוס בערך null, אם אין SleepNight פעיל. במקרה כזה, תופיע שגיאה כי הפונקציה חייבת להחזיר משהו.
private suspend fun getTonightFromDatabase(): SleepNight? { }
  1. בתוך גוף הפונקציה של getTonightFromDatabase(), מחזירים את התוצאה מקורוטינה שפועלת בהקשר Dispatchers.IO. כדאי להשתמש ב-I/O dispatcher, כי שליפת נתונים ממסד הנתונים היא פעולת קלט/פלט ולא קשורה לממשק המשתמש.
  return withContext(Dispatchers.IO) {}
  1. בתוך בלוק ההחזרה, מאפשרים לקורוטינה לקבל את הלילה הנוכחי (הלילה האחרון) ממסד הנתונים. אם שעת ההתחלה ושעת הסיום לא זהות, כלומר הלילה כבר הסתיים, מחזירים null. אחרת, מחזירה את הלילה.
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night

פונקציית ההשעיה getTonightFromDatabase() שהושלמה אמורה להיראות כך. לא אמורות להיות יותר שגיאות.

private suspend fun getTonightFromDatabase(): SleepNight? {
   return withContext(Dispatchers.IO) {
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night
   }
}

שלב 2: מוסיפים את click handler ללחצן 'התחלה'

עכשיו אפשר להטמיע את onStartTracking(), הפונקציה לטיפול בלחיצות על הלחצן Start. צריך ליצור SleepNight חדש, להוסיף אותו למסד הנתונים ולהקצות אותו ל-tonight. המבנה של onStartTracking() יהיה דומה מאוד לזה של initializeTonight().

  1. מתחילים בהגדרת הפונקציה של onStartTracking(). אפשר להוסיף את רכיבי ה-handler של הקליקים מעל onCleared() בקובץ SleepTrackerViewModel.
fun onStartTracking() {}
  1. בתוך onStartTracking(), מפעילים קורוטינה ב-uiScope, כי צריך את התוצאה הזו כדי להמשיך ולעדכן את ממשק המשתמש.
uiScope.launch {}
  1. בתוך ההפעלה של הקורוטינה, יוצרים SleepNight חדש, שמתעד את השעה הנוכחית כשעת ההתחלה.
        val newNight = SleepNight()
  1. עדיין בתוך ההפעלה של הקורוטינה, קוראים ל-insert() כדי להוסיף את newNight למסד הנתונים. תוצג שגיאה כי עדיין לא הגדרתם את פונקציית ההשעיה insert(). (זו לא פונקציית ה-DAO באותו שם).
       insert(newNight)
  1. בנוסף, בתוך ההפעלה של הקורוטינה, מעדכנים את tonight.
       tonight.value = getTonightFromDatabase()
  1. מתחת ל-onStartTracking(), מגדירים את insert() כפונקציה private suspend שמקבלת את SleepNight כארגומנט.
private suspend fun insert(night: SleepNight) {}
  1. בגוף של insert(), מפעילים קורוטינה בהקשר של קלט/פלט ומוסיפים את הלילה למסד הנתונים על ידי קריאה ל-insert() מ-DAO.
   withContext(Dispatchers.IO) {
       database.insert(night)
   }
  1. בקובץ הפריסה fragment_sleep_tracker.xml, מוסיפים את handler הקליקים של onStartTracking() אל start_button באמצעות הקסם של קישור הנתונים שהגדרתם קודם. הסימון של הפונקציה @{() -> יוצר פונקציית lambda שלא מקבלת ארגומנטים ומפעילה את הגורם המטפל בקליקים ב-sleepTrackerViewModel.
android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"
  1. יוצרים ומריצים את האפליקציה. מקישים על הלחצן התחלה. הפעולה הזו יוצרת נתונים, אבל עדיין לא רואים כלום. אחר כך מתקנים את זה.
fun someWorkNeedsToBeDone {
   uiScope.launch {

        suspendFunction()

   }
}

suspend fun suspendFunction() {
   withContext(Dispatchers.IO) {
       longrunningWork()
   }
}

שלב 3: הצגת הנתונים

ב-SleepTrackerViewModel, המשתנה nights מפנה אל LiveData כי getAllNights() ב-DAO מחזיר LiveData.

זהו פיצ'ר של Room שמעדכן את LiveData nights בכל פעם שמתבצע שינוי בנתונים במסד הנתונים, כדי להציג את הנתונים העדכניים ביותר. אין צורך להגדיר את LiveData או לעדכן אותו. ‫Room מעדכן את הנתונים כך שיתאימו למסד הנתונים.

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

  1. פותחים את הקובץ Util.kt ומבטלים את ההערה על הקוד של ההגדרה של formatNights() ושל ההצהרות המשויכות import. כדי לבטל את ההערה של קוד ב-Android Studio, בוחרים את כל הקוד שמסומן ב-// ולוחצים על Cmd+/ או על Control+/.
  2. שימו לב שהפונקציה formatNights() מחזירה סוג Spanned, שהוא מחרוזת בפורמט HTML.
  3. פותחים את strings.xml. שימו לב לשימוש ב-CDATA כדי לעצב את משאבי המחרוזת להצגת נתוני השינה.
  4. פותחים את SleepTrackerViewModel. בקטע SleepTrackerViewModel class, מתחת להגדרה של uiScope, מגדירים משתנה שנקרא nights. מקבלים את כל הלילות ממסד הנתונים ומקצים אותם למשתנה nights.
private val nights = database.getAllNights()
  1. מתחת להגדרה של nights, מוסיפים קוד כדי להפוך את nights ל-nightsString. משתמשים בפונקציה formatNights() מ-Util.kt.

    מעבירים את nights לפונקציה map() מהמחלקה Transformations. כדי לקבל גישה למשאבי המחרוזות, מגדירים את פונקציית המיפוי כקריאה ל-formatNights(). העברת nights ואובייקט Resources.
val nightsString = Transformations.map(nights) { nights ->
   formatNights(nights, application.resources)
}
  1. פותחים את קובץ הפריסה fragment_sleep_tracker.xml. ב-TextView, במאפיין android:text, אפשר עכשיו להחליף את מחרוזת המשאב בהפניה אל nightsString.
"@{sleepTrackerViewModel.nightsString}"
  1. בונים מחדש את הקוד ומריצים את האפליקציה. עכשיו אמורים להופיע כל נתוני השינה עם זמני ההתחלה.
  2. מקישים על הלחצן התחלה עוד כמה פעמים כדי לראות נתונים נוספים.

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

שלב 4: מוסיפים את הפונקציה לטיפול בקליקים של הלחצן Stop

באמצעות אותו דפוס כמו בשלב הקודם, מטמיעים את click handler עבור הלחצן Stop ב-SleepTrackerViewModel.

  1. מוסיפים את onStopTracking() אל ViewModel. מפעילים קורוטינה ב-uiScope. אם עדיין לא הוגדר שעת הסיום, מגדירים את endTimeMilli לשעת המערכת הנוכחית ומפעילים את update() עם נתוני הלילה.

    ב-Kotlin, התחביר return@label מציין את הפונקציה שממנה ההצהרה הזו מוחזרת, מתוך כמה פונקציות מקוננות.
fun onStopTracking() {
   uiScope.launch {
       val oldNight = tonight.value ?: return@launch
       oldNight.endTimeMilli = System.currentTimeMillis()
       update(oldNight)
   }
}
  1. מטמיעים את update() באמצעות אותו דפוס שבו השתמשתם כדי להטמיע את insert().
private suspend fun update(night: SleepNight) {
   withContext(Dispatchers.IO) {
       database.update(night)
   }
}
  1. כדי לחבר את ה-handler של הלחיצה לממשק המשתמש, פותחים את קובץ הפריסה fragment_sleep_tracker.xml ומוסיפים את ה-handler של הלחיצה ל-stop_button.
android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
  1. יוצרים ומריצים את האפליקציה.
  2. מקישים על התחלה ואז על עצירה. מוצגים שעת ההתחלה, שעת הסיום, איכות השינה ללא ערך ומשך השינה.

שלב 5: מוסיפים את הפונקציה לטיפול בקליקים של הלחצן 'ניקוי'

  1. באופן דומה, מטמיעים את onClear() ואת clear().
fun onClear() {
   uiScope.launch {
       clear()
       tonight.value = null
   }
}

suspend fun clear() {
   withContext(Dispatchers.IO) {
       database.clear()
   }
}
  1. כדי לחבר את click handler לממשק המשתמש, פותחים את fragment_sleep_tracker.xml ומוסיפים את click handler ל-clear_button.
android:onClick="@{() -> sleepTrackerViewModel.onClear()}"
  1. יוצרים ומריצים את האפליקציה.
  2. מקישים על ניקוי כדי למחוק את כל הנתונים. לאחר מכן מקישים על התחלה ועל הפסקה כדי ליצור נתונים חדשים.

פרויקט Android Studio: ‏ TrackMySleepQualityCoroutines

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

כדי להפעיל קורוטינה, צריך עבודה, מפיץ והיקף:

  • בעצם, עבודה היא כל דבר שאפשר לבטל. לכל קורוטינה יש עבודה, ואפשר להשתמש בעבודה כדי לבטל קורוטינה.
  • ה-dispatcher שולח קורוטינות להרצה בשרשורים שונים. ‫Dispatcher.Main מריץ משימות ב-thread הראשי, ו-Dispartcher.IO משמש להעברת משימות חוסמות של קלט/פלט למאגר משותף של threads.
  • ההיקף משלב מידע, כולל עבודה ומשגר, כדי להגדיר את ההקשר שבו הקורוטינה פועלת. היקפים עוקבים אחרי קורוטינות.

כדי להטמיע click handlers שמפעילים פעולות במסד הנתונים, פועלים לפי התבנית הבאה:

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

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

קורס ב-Udacity:

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

מסמכים ומאמרים נוספים:

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

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

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

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

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

שאלה 1

אילו מהאפשרויות הבאות הן יתרונות של קורוטינות:

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

שאלה 2

מהי פונקציית השעיה?

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

שאלה 3

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

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

עוברים לשיעור הבא: 6.3 שימוש ב-LiveData כדי לשלוט במצבי הלחצנים

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