ה-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: הורדה והפעלה של אפליקציית המתחילים
- מורידים את האפליקציה TrackMySleepQuality-Coroutines-Starter מ-GitHub.
- מבצעים Build ומריצים את האפליקציה. ממשק המשתמש של קטע
SleepTrackerFragment
מוצג באפליקציה, אבל לא מוצגים נתונים. הכפתורים לא מגיבים להקשה.
שלב 2: בודקים את הקוד
קוד ההתחלה של Codelab הזה זהה לקוד הפתרון של Codelab 6.1 בנושא יצירת מסד נתונים של Room.
- פותחים את הקובץ res/layout/activity_main.xml. הפריסה הזו מכילה את קטע הקוד
nav_host_fragment
. שימו לב גם לתג<merge>
.
אפשר להשתמש בתגmerge
כדי למנוע פריסות מיותרות כשמצרפים פריסות, ומומלץ להשתמש בו. דוגמה לפריסה מיותרת היא ConstraintLayout > LinearLayout > TextView, שבה המערכת יכולה לבטל את LinearLayout. אופטימיזציה כזו יכולה לפשט את היררכיית התצוגה ולשפר את ביצועי האפליקציה. - בתיקייה navigation, פותחים את הקובץ navigation.xml. אפשר לראות שני פרגמנטים ואת פעולות הניווט שמקשרות ביניהם.
- בתיקייה layout, לוחצים לחיצה כפולה על רכיב sleep tracker כדי לראות את פריסת ה-XML שלו. חשוב לשים לב:
- נתוני הפריסה עטופים ברכיב
<layout>
כדי לאפשר קישור נתונים. -
ConstraintLayout
והתצוגות האחרות מסודרות בתוך רכיב<layout>
. - הקובץ מכיל תג placeholder
<data>
.
אפליקציית המתחילים מספקת גם מידות, צבעים וסגנון לממשק המשתמש. האפליקציה מכילה מסד נתונים Room
, אובייקט DAO וישות SleepNight
. אם לא השלמתם את ה-codelab הקודם, חשוב שתבדקו בעצמכם את ההיבטים האלה של הקוד.
עכשיו שיש לכם מסד נתונים וממשק משתמש, אתם צריכים לאסוף נתונים, להוסיף אותם למסד הנתונים ולהציג אותם. כל העבודה הזו מתבצעת במודל התצוגה. מודל התצוגה של מעקב השינה יטפל בלחיצות על הלחצנים, יקיים אינטראקציה עם מסד הנתונים באמצעות ה-DAO ויספק נתונים לממשק המשתמש באמצעות LiveData
. כל פעולות מסד הנתונים יצטרכו להתבצע מחוץ ל-thread הראשי של ממשק המשתמש, ותוכלו לעשות זאת באמצעות קורוטינות.
שלב 1: מוסיפים את SleepTrackerViewModel
- בחבילה sleeptracker, פותחים את SleepTrackerViewModel.kt.
- בודקים את המחלקה
SleepTrackerViewModel
, שמסופקת באפליקציית המתחילים ומוצגת גם בהמשך. שימו לב שהכיתה מתרחבתAndroidViewModel()
. המחלקה הזו זהה למחלקהViewModel
, אבל היא מקבלת את הקשר של האפליקציה כפרמטר ומאפשרת להשתמש בו כמאפיין. יהיה צורך במידע הזה בהמשך.
class SleepTrackerViewModel(
val database: SleepDatabaseDao,
application: Application) : AndroidViewModel(application) {
}
שלב 2: מוסיפים את SleepTrackerViewModelFactory
- בחבילה sleeptracker, פותחים את SleepTrackerViewModelFactory.kt.
- בודקים את הקוד שמופיע בהמשך עבור המפעל:
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
- ב-
SleepTrackerFragment
, מקבלים הפניה להקשר של האפליקציה. מזינים את ההפניה ב-onCreateView()
, מתחת ל-binding
. צריך הפניה לאפליקציה שהקטע הזה מצורף אליה, כדי להעביר אותה לספק של יצירת מודל התצוגה.
הפונקציהrequireNotNull
Kotlin מחזירהIllegalArgumentException
אם הערך הואnull
.
val application = requireNotNull(this.activity).application
- צריך הפניה למקור הנתונים דרך הפניה ל-DAO. ב-
onCreateView()
, לפניreturn
, מגדיריםdataSource
. כדי לקבל הפניה ל-DAO של מסד הנתונים, משתמשים ב-SleepDatabase.getInstance(application).sleepDatabaseDao
.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
- ב-
onCreateView()
, לפניreturn
, יוצרים מופע שלviewModelFactory
. צריך להעביר אתdataSource
ואתapplication
.
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
- עכשיו, אחרי שיש לכם מפעל, צריך לקבל הפניה אל
SleepTrackerViewModel
. הפרמטרSleepTrackerViewModel::class.java
מתייחס למחלקת Java של האובייקט הזה בזמן הריצה.
val sleepTrackerViewModel =
ViewModelProviders.of(
this, viewModelFactory).get(SleepTrackerViewModel::class.java)
- הקוד הסופי אמור להיראות כך:
// 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
:
- בתוך הבלוק
<data>
, יוצרים<variable>
שמפנה למחלקהSleepTrackerViewModel
.
<data>
<variable
name="sleepTrackerViewModel"
type="com.example.android.trackmysleepquality.sleeptracker.SleepTrackerViewModel" />
</data>
ב-SleepTrackerFragment
:
- הגדרת הפעילות הנוכחית כבעלים של מחזור החיים של הקישור. מוסיפים את הקוד הזה בתוך השיטה
onCreateView()
, לפני ההצהרהreturn
:
binding.setLifecycleOwner(this)
- מקצים את משתנה הקישור
sleepTrackerViewModel
ל-sleepTrackerViewModel
. מציבים את הקוד הזה בתוךonCreateView()
, מתחת לקוד שיוצר אתSleepTrackerViewModel
:
binding.sleepTrackerViewModel = sleepTrackerViewModel
- סביר להניח שתופיע שגיאה, כי צריך ליצור מחדש את אובייקט הקישור. כדי להיפטר מהשגיאה, צריך לנקות את הפרויקט ולבנות אותו מחדש.
- לסיום, כמו תמיד, חשוב לוודא שהקוד נבנה ופועל ללא שגיאות.
ב-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
. לכן, בין היתר, משתמשים בקורוטינות כדי להטמיע את הפונקציות לטיפול בלחיצות על הלחצנים באפליקציה.
- פותחים את הקובץ
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"
- פותחים את הקובץ
SleepTrackerViewModel
. - בגוף המחלקה, מגדירים את
viewModelJob
ומקצים לו מופע שלJob
. המאפייןviewModelJob
מאפשר לבטל את כל הקורוטינות שהופעלו על ידי מודל התצוגה הזה כשהוא כבר לא בשימוש ונהרס. כך לא יקרה מצב שבו קורוטינות לא יוכלו לחזור.
private var viewModelJob = Job()
- בסוף גוף המחלקה, מבטלים את
onCleared()
ומבטלים את כל הקורוטינות. כש-ViewModel
נהרס, מתבצעת קריאה ל-onCleared()
.
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
- מגדירים
uiScope
לקורוטינות מיד מתחת להגדרה שלviewModelJob
. ההיקף קובע באיזה שרשור יפעל הקורוטינה, וההיקף צריך לדעת גם על העבודה. כדי לקבל היקף, צריך לבקש מופע שלCoroutineScope
ולהעביר משגר ועבודה.
השימוש ב-Dispatchers.Main
מציין שקורוטינות שהופעלו ב-uiScope
יפעלו ב-thread הראשי. זה הגיוני להרבה קורוטינות שמופעלות על ידי ViewModel
, כי אחרי שהקורוטינות האלה מבצעות עיבוד מסוים, הן גורמות לעדכון של ממשק המשתמש.
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
- מתחת להגדרה של
uiScope
, מגדירים משתנה בשםtonight
שיכיל את הלילה הנוכחי. הופכים את המשתנה ל-MutableLiveData
, כי צריך להיות אפשר לצפות בנתונים ולשנות אותם.
private var tonight = MutableLiveData<SleepNight?>()
- כדי לאתחל את המשתנה
tonight
בהקדם האפשרי, יוצרים בלוקinit
מתחת להגדרה שלtonight
ומפעילים אתinitializeTonight()
. בשלב הבא תגדירו אתinitializeTonight()
.
init {
initializeTonight()
}
- מתחת לבלוק
init
, מטמיעים אתinitializeTonight()
. ב-uiScope
, מפעילים קורוטינה. בתוך הפונקציה, מקבלים את הערך שלtonight
מהמסד הנתונים על ידי קריאה ל-getTonightFromDatabase()
, ומקצים את הערך ל-tonight.value
. בשלב הבא תגדירו אתgetTonightFromDatabase()
.
private fun initializeTonight() {
uiScope.launch {
tonight.value = getTonightFromDatabase()
}
}
- הטמעה של
getTonightFromDatabase()
. מגדירים אותה כפונקציהprivate suspend
שמחזירה ערךSleepNight
שניתן לאכלוס בערך null, אם איןSleepNight
פעיל. במקרה כזה, תופיע שגיאה כי הפונקציה חייבת להחזיר משהו.
private suspend fun getTonightFromDatabase(): SleepNight? { }
- בתוך גוף הפונקציה של
getTonightFromDatabase()
, מחזירים את התוצאה מקורוטינה שפועלת בהקשרDispatchers.IO
. כדאי להשתמש ב-I/O dispatcher, כי שליפת נתונים ממסד הנתונים היא פעולת קלט/פלט ולא קשורה לממשק המשתמש.
return withContext(Dispatchers.IO) {}
- בתוך בלוק ההחזרה, מאפשרים לקורוטינה לקבל את הלילה הנוכחי (הלילה האחרון) ממסד הנתונים. אם שעת ההתחלה ושעת הסיום לא זהות, כלומר הלילה כבר הסתיים, מחזירים
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()
.
- מתחילים בהגדרת הפונקציה של
onStartTracking()
. אפשר להוסיף את רכיבי ה-handler של הקליקים מעלonCleared()
בקובץSleepTrackerViewModel
.
fun onStartTracking() {}
- בתוך
onStartTracking()
, מפעילים קורוטינה ב-uiScope
, כי צריך את התוצאה הזו כדי להמשיך ולעדכן את ממשק המשתמש.
uiScope.launch {}
- בתוך ההפעלה של הקורוטינה, יוצרים
SleepNight
חדש, שמתעד את השעה הנוכחית כשעת ההתחלה.
val newNight = SleepNight()
- עדיין בתוך ההפעלה של הקורוטינה, קוראים ל-
insert()
כדי להוסיף אתnewNight
למסד הנתונים. תוצג שגיאה כי עדיין לא הגדרתם את פונקציית ההשעיהinsert()
. (זו לא פונקציית ה-DAO באותו שם).
insert(newNight)
- בנוסף, בתוך ההפעלה של הקורוטינה, מעדכנים את
tonight
.
tonight.value = getTonightFromDatabase()
- מתחת ל-
onStartTracking()
, מגדירים אתinsert()
כפונקציהprivate suspend
שמקבלת אתSleepNight
כארגומנט.
private suspend fun insert(night: SleepNight) {}
- בגוף של
insert()
, מפעילים קורוטינה בהקשר של קלט/פלט ומוסיפים את הלילה למסד הנתונים על ידי קריאה ל-insert()
מ-DAO.
withContext(Dispatchers.IO) {
database.insert(night)
}
- בקובץ הפריסה
fragment_sleep_tracker.xml
, מוסיפים את handler הקליקים שלonStartTracking()
אלstart_button
באמצעות הקסם של קישור הנתונים שהגדרתם קודם. הסימון של הפונקציה@{() ->
יוצר פונקציית lambda שלא מקבלת ארגומנטים ומפעילה את הגורם המטפל בקליקים ב-sleepTrackerViewModel
.
android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"
- יוצרים ומריצים את האפליקציה. מקישים על הלחצן התחלה. הפעולה הזו יוצרת נתונים, אבל עדיין לא רואים כלום. אחר כך מתקנים את זה.
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
מקבל נתונים חדשים ממסד הנתונים.
- פותחים את הקובץ
Util.kt
ומבטלים את ההערה על הקוד של ההגדרה שלformatNights()
ושל ההצהרות המשויכותimport
. כדי לבטל את ההערה של קוד ב-Android Studio, בוחרים את כל הקוד שמסומן ב-//
ולוחצים עלCmd+/
או עלControl+/
. - שימו לב שהפונקציה
formatNights()
מחזירה סוגSpanned
, שהוא מחרוזת בפורמט HTML. - פותחים את strings.xml. שימו לב לשימוש ב-
CDATA
כדי לעצב את משאבי המחרוזת להצגת נתוני השינה. - פותחים את SleepTrackerViewModel. בקטע
SleepTrackerViewModel
class, מתחת להגדרה שלuiScope
, מגדירים משתנה שנקראnights
. מקבלים את כל הלילות ממסד הנתונים ומקצים אותם למשתנהnights
.
private val nights = database.getAllNights()
- מתחת להגדרה של
nights
, מוסיפים קוד כדי להפוך אתnights
ל-nightsString
. משתמשים בפונקציהformatNights()
מ-Util.kt
.
מעבירים אתnights
לפונקציהmap()
מהמחלקהTransformations
. כדי לקבל גישה למשאבי המחרוזות, מגדירים את פונקציית המיפוי כקריאה ל-formatNights()
. העברתnights
ואובייקטResources
.
val nightsString = Transformations.map(nights) { nights ->
formatNights(nights, application.resources)
}
- פותחים את קובץ הפריסה
fragment_sleep_tracker.xml
. ב-TextView
, במאפייןandroid:text
, אפשר עכשיו להחליף את מחרוזת המשאב בהפניה אלnightsString
.
"@{sleepTrackerViewModel.nightsString}"
- בונים מחדש את הקוד ומריצים את האפליקציה. עכשיו אמורים להופיע כל נתוני השינה עם זמני ההתחלה.
- מקישים על הלחצן התחלה עוד כמה פעמים כדי לראות נתונים נוספים.
בשלב הבא, מפעילים את הפונקציונליות של הלחצן עצירה.
שלב 4: מוסיפים את הפונקציה לטיפול בקליקים של הלחצן Stop
באמצעות אותו דפוס כמו בשלב הקודם, מטמיעים את click handler עבור הלחצן Stop ב-SleepTrackerViewModel.
- מוסיפים את
onStopTracking()
אלViewModel
. מפעילים קורוטינה ב-uiScope
. אם עדיין לא הוגדר שעת הסיום, מגדירים אתendTimeMilli
לשעת המערכת הנוכחית ומפעילים אתupdate()
עם נתוני הלילה.
ב-Kotlin, התחבירreturn@
label
מציין את הפונקציה שממנה ההצהרה הזו מוחזרת, מתוך כמה פונקציות מקוננות.
fun onStopTracking() {
uiScope.launch {
val oldNight = tonight.value ?: return@launch
oldNight.endTimeMilli = System.currentTimeMillis()
update(oldNight)
}
}
- מטמיעים את
update()
באמצעות אותו דפוס שבו השתמשתם כדי להטמיע אתinsert()
.
private suspend fun update(night: SleepNight) {
withContext(Dispatchers.IO) {
database.update(night)
}
}
- כדי לחבר את ה-handler של הלחיצה לממשק המשתמש, פותחים את קובץ הפריסה
fragment_sleep_tracker.xml
ומוסיפים את ה-handler של הלחיצה ל-stop_button
.
android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
- יוצרים ומריצים את האפליקציה.
- מקישים על התחלה ואז על עצירה. מוצגים שעת ההתחלה, שעת הסיום, איכות השינה ללא ערך ומשך השינה.
שלב 5: מוסיפים את הפונקציה לטיפול בקליקים של הלחצן 'ניקוי'
- באופן דומה, מטמיעים את
onClear()
ואתclear()
.
fun onClear() {
uiScope.launch {
clear()
tonight.value = null
}
}
suspend fun clear() {
withContext(Dispatchers.IO) {
database.clear()
}
}
- כדי לחבר את click handler לממשק המשתמש, פותחים את
fragment_sleep_tracker.xml
ומוסיפים את click handler ל-clear_button
.
android:onClick="@{() -> sleepTrackerViewModel.onClear()}"
- יוצרים ומריצים את האפליקציה.
- מקישים על ניקוי כדי למחוק את כל הנתונים. לאחר מכן מקישים על התחלה ועל הפסקה כדי ליצור נתונים חדשים.
פרויקט Android Studio: TrackMySleepQualityCoroutines
- משתמשים ב-
ViewModel
, ב-ViewModelFactory
ובקשירת נתונים כדי להגדיר את ארכיטקטורת ממשק המשתמש של האפליקציה. - כדי שהממשק יפעל בצורה חלקה, כדאי להשתמש ב-coroutines למשימות ארוכות, כמו כל פעולות מסד הנתונים.
- קורוטינות הן אסינכרוניות ולא חוסמות. הן משתמשות בפונקציות
suspend
כדי להפוך קוד אסינכרוני לרציף. - כשקורוטינה קוראת לפונקציה שמסומנת ב-
suspend
, במקום לחסום עד שהפונקציה מחזירה ערך כמו בקריאה רגילה לפונקציה, היא מפסיקה את הביצוע עד שהתוצאה מוכנה. התוצאה תופיע כשההקלטה תמשיך מהנקודה שבה היא הופסקה. - ההבדל בין חסימה לבין השהיה הוא שאם השרשור חסום, לא מתבצעת עבודה נוספת. אם השרשור מושהה, עבודות אחרות מתבצעות עד שהתוצאה זמינה.
כדי להפעיל קורוטינה, צריך עבודה, מפיץ והיקף:
- בעצם, עבודה היא כל דבר שאפשר לבטל. לכל קורוטינה יש עבודה, ואפשר להשתמש בעבודה כדי לבטל קורוטינה.
- ה-dispatcher שולח קורוטינות להרצה בשרשורים שונים.
Dispatcher.Main
מריץ משימות ב-thread הראשי, ו-Dispartcher.IO
משמש להעברת משימות חוסמות של קלט/פלט למאגר משותף של threads. - ההיקף משלב מידע, כולל עבודה ומשגר, כדי להגדיר את ההקשר שבו הקורוטינה פועלת. היקפים עוקבים אחרי קורוטינות.
כדי להטמיע click handlers שמפעילים פעולות במסד הנתונים, פועלים לפי התבנית הבאה:
- מפעילים קורוטינה שפועלת ב-thread הראשי או ב-thread של ממשק המשתמש, כי התוצאה משפיעה על ממשק המשתמש.
- כדי לא לחסום את השרשור של ממשק המשתמש בזמן ההמתנה לתוצאה, צריך להפעיל פונקציית השהיה כדי לבצע את העבודה שדורשת זמן רב.
- העבודה שמתבצעת לאורך זמן לא קשורה לממשק המשתמש, ולכן צריך לעבור להקשר של קלט/פלט. כך העבודה יכולה להתבצע במאגר שרשורים שעבר אופטימיזציה והוקצה לפעולות מהסוג הזה.
- לאחר מכן קוראים לפונקציית מסד הנתונים כדי לבצע את העבודה.
אפשר להשתמש בTransformations
מיפוי כדי ליצור מחרוזת מאובייקט LiveData
בכל פעם שהאובייקט משתנה.
קורס ב-Udacity:
מסמכי תיעוד למפתחי Android:
RoomDatabase
- שימוש חוזר בפריסות באמצעות <include/>
ViewModelProvider.Factory
SimpleDateFormat
HtmlCompat
מסמכים ומאמרים נוספים:
- Factory pattern
- Codelab בנושא Coroutines
- Coroutines, official documentation
- הקשר של קורוטינה ו-dispatchers
Dispatchers
- חריגה ממגבלת המהירות ב-Android
Job
launch
- החזרות וקפיצות ב-Kotlin
- CDATA מייצג character data (נתוני תווים). CDATA פירושו שהנתונים שבין המחרוזות האלה כוללים נתונים שאפשר לפרש כסימון XML, אבל לא צריך לפרש אותם כך.
בקטע הזה מפורטות אפשרויות למשימות ביתיות לתלמידים שעובדים על ה-Codelab הזה כחלק מקורס בהנחיית מדריך. המורה צריך:
- אם צריך, מקצים שיעורי בית.
- להסביר לתלמידים איך להגיש מטלות.
- בודקים את שיעורי הבית.
אנשי ההוראה יכולים להשתמש בהצעות האלה כמה שרוצים, ומומלץ להם להקצות כל שיעורי בית אחרים שהם חושבים שמתאימים.
אם אתם עובדים על ה-codelab הזה לבד, אתם יכולים להשתמש במשימות האלה כדי לבדוק את הידע שלכם.
עונים על השאלות
שאלה 1
אילו מהאפשרויות הבאות הן יתרונות של קורוטינות:
- הן לא חוסמות
- הם פועלים באופן אסינכרוני.
- אפשר להריץ אותם ב-thread שאינו ה-thread הראשי.
- הם תמיד משפרים את מהירות ההרצה של האפליקציה.
- הם יכולים להשתמש בחריגים.
- אפשר לכתוב ולקרוא אותם כקוד לינארי.
שאלה 2
מהי פונקציית השעיה?
- פונקציה רגילה שמסומנת במילת המפתח
suspend
. - פונקציה שאפשר לקרוא לה בתוך קורוטינות.
- בזמן שהפונקציה suspend פועלת, השרשור של השיחה מושהה.
- פונקציות ההשהיה חייבות תמיד לפעול ברקע.
שאלה 3
מה ההבדל בין חסימה של שרשור לבין השהיה שלו? צריך לסמן את כל האפשרויות הנכונות.
- כשביצוע נחסם, אי אפשר לבצע עבודה אחרת בשרשור החסום.
- כשהביצוע מושהה, ה-thread יכול לבצע עבודה אחרת בזמן ההמתנה לסיום העבודה שהועברה.
- ההשעיה יעילה יותר, כי יכול להיות שהשרשורים לא ימתינו ולא יעשו כלום.
- גם אם החסימה או ההשעיה מתרחשות, ההרצה עדיין ממתינה לתוצאה של שגרת המשנה לפני שהיא ממשיכה.
עוברים לשיעור הבא:
קישורים ל-codelabs אחרים בקורס הזה מופיעים בדף הנחיתה של ה-codelabs בנושא יסודות Android Kotlin.