מושגים ויישום ב-Jetpack פיתוח נייטיב
במהלך הניווט של המשתמש באפליקציה, מחוץ לאפליקציה ובחזרה לאפליקציה, המופעים של Activity
באפליקציה עוברים בין מצבים שונים במחזור החיים שלהם.
המחלקות Activity מספקות מספר קריאות חוזרות (callback) שמאפשרות לפעילות לדעת מתי משתנה מצב או שהמערכת יוצרת, עוצרת או מפעילה מחדש פעילות או משמידה את התהליך שבו הפעילות נמצאת.
בשיטות של קריאה חוזרת במחזור חיים, אפשר להגדיר את אופן הפעולה של הפעילות כשמשתמש יוצא ממנה ונכנס אליה מחדש. לדוגמה, אם אתם מפתחים נגן וידאו לסטרימינג, אתם יכולים להשהות את הסרטון ולנתק את החיבור לרשת כשהמשתמש עובר לאפליקציה אחרת. כשהמשתמש חוזר, אתם יכולים להתחבר מחדש לרשת ולאפשר למשתמש להמשיך לצפות בסרטון מאותה נקודה.
כל קריאה חוזרת מאפשרת לבצע פעולה ספציפית שמתאימה לשינוי מסוים בסטטוס. ביצוע העבודה הנכונה בזמן הנכון וטיפול נכון במעברים משפרים את היציבות והביצועים של האפליקציה. לדוגמה, הטמעה טובה של קריאות חוזרות (callbacks) של מחזור החיים יכולה לעזור לאפליקציה שלכם להימנע מהבעיות הבאות:
- קריסה אם המשתמש מקבל שיחת טלפון או עובר לאפליקציה אחרת בזמן השימוש באפליקציה שלכם.
- צריכת משאבי מערכת יקרים כשהמשתמש לא משתמש בה באופן פעיל.
- התקדמות המשתמשים תאבד אם הם ייצאו מהאפליקציה ויחזרו אליה בשלב מאוחר יותר.
- קריסה או אובדן ההתקדמות של המשתמשים כשהמסך מסתובב בין תצוגה לרוחב לתצוגה לאורך.
במסמך הזה מוסבר בפירוט מחזור החיים של הפעילות. המסמך מתחיל בתיאור של פרדיגמת מחזור החיים. בהמשך מוסבר על כל אחת מהפונקציות לטיפול בקריאות חוזרות: מה קורה באופן פנימי בזמן ההפעלה שלהן ומה צריך להטמיע במהלך ההפעלה.
לאחר מכן מוצג בקצרה הקשר בין מצב הפעילות לבין הפגיעות של תהליך להפסקת הפעולה שלו על ידי המערכת. בסוף, יש דיון בכמה נושאים שקשורים למעברים בין מצבי פעילות.
מידע על טיפול במחזורי חיים, כולל הנחיות לגבי שיטות מומלצות, זמין במאמרים טיפול במחזורי חיים באמצעות רכיבים שמודעים למחזור החיים ושמירת מצבי ממשק המשתמש. כדי ללמוד איך ליצור אפליקציה חזקה באיכות ייצור באמצעות פעילויות בשילוב עם רכיבי ארכיטקטורה, אפשר לעיין במדריך לארכיטקטורת אפליקציות.
מושגים שקשורים למחזור החיים של פעילות
כדי לנווט בין המעברים בשלבים של מחזור החיים של הפעילות, המחלקה Activity
מספקת קבוצה בסיסית של שש פונקציות קריאה חוזרת: onCreate, onStart, onResume, onPause, onStop ו-onDestroy. המערכת מפעילה כל אחת מהפונקציות האלה כשפעילות עוברת למצב חדש.
איור 1 מציג ייצוג חזותי של הפרדיגמה הזו.
כשהמשתמש מתחיל לצאת מהפעילות, המערכת קוראת לשיטות כדי לפרק את הפעילות. במקרים מסוימים, הפעילות מפורקת רק באופן חלקי והיא עדיין נמצאת בזיכרון, למשל כשהמשתמש עובר לאפליקציה אחרת. במקרים כאלה, הפעילות עדיין יכולה לחזור לחזית.
אם המשתמש חוזר לפעילות, היא ממשיכה מהמקום שבו הוא הפסיק. עם כמה יוצאים מן הכלל, לאפליקציות אסור להתחיל פעילויות כשהן פועלות ברקע.
הסיכוי שהמערכת תסיים תהליך מסוים, יחד עם הפעילויות שבו, תלוי במצב הפעילות באותו זמן. מידע נוסף על הקשר בין מצב לבין פגיעות להוצאה מהזיכרון זמין בקטע בנושא מצב פעילות והוצאה מהזיכרון.
בהתאם למורכבות הפעילות, סביר להניח שלא תצטרכו להטמיע את כל השיטות של מחזור החיים. עם זאת, חשוב להבין כל אחת מהן וליישם את אלה שגורמות לאפליקציה להתנהג כמו שהמשתמשים מצפים.
קריאות חוזרות במחזור חיים
בקטע הזה מוסבר על שיטות הקריאה החוזרת שמשמשות במהלך מחזור החיים של הפעילות.
חלק מהפעולות שייכות לשיטות של מחזור החיים של הפעילות. עם זאת, צריך למקם את הקוד שמטמיע את הפעולות של רכיב תלוי ברכיב, ולא בשיטת מחזור החיים של הפעילות. כדי לעשות את זה, צריך להגדיר את מחזור החיים של הרכיב התלוי. כדי ללמוד איך להפוך את רכיבי התלות למודעים למחזור החיים, אפשר לעיין במאמר טיפול במחזורי חיים באמצעות רכיבים שמודעים למחזור החיים.
onCreate
חובה להטמיע את הקריאה החוזרת הזו, שמופעלת כשהמערכת יוצרת את הפעילות בפעם הראשונה. כשיוצרים פעילות, היא עוברת למצב נוצרה. ב-method onCreate, מבצעים לוגיקה בסיסית של הפעלת האפליקציה שמתרחשת רק פעם אחת במהלך מחזור החיים של הפעילות.
לדוגמה, ההטמעה של onCreate עשויה לקשור נתונים לרשימות,
לשייך את הפעילות לViewModel וליצור מופע של כמה משתנים בהיקף המחלקה. ה-method הזה מקבל את הפרמטר savedInstanceState, שהוא אובייקט Bundle שמכיל את המצב שנשמר קודם של הפעילות. אם הפעילות לא התקיימה אף פעם, הערך של האובייקט Bundle הוא null.
אם יש לכם רכיב שמודע למחזור החיים ומחובר למחזור החיים של הפעילות, הוא מקבל את האירוע ON_CREATE. השיטה שמסומנת ב-@OnLifecycleEvent נקראת כדי שהרכיב שמודע למחזור החיים יוכל לבצע את קוד ההגדרה שהוא צריך למצב שנוצר.
בדוגמה הבאה של ה-method onCreate מוצגת הגדרה בסיסית של הפעילות, כמו הצהרה על ממשק המשתמש (שמוגדר בקובץ פריסה של XML), הגדרה של משתני חברים והגדרה של חלק מממשק המשתמש. בדוגמה הזו, קובץ הפריסה של ה-XML מעביר את מזהה המשאב של הקובץ R.layout.main_activity אל setContentView.
Kotlin
lateinit var textView: TextView
// Some transient state for the activity instance.
var gameState: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
// Call the superclass onCreate to complete the creation of
// the activity, like the view hierarchy.
super.onCreate(savedInstanceState)
// Recover the instance state.
gameState = savedInstanceState?.getString(GAME_STATE_KEY)
// Set the user interface layout for this activity.
// The layout is defined in the project res/layout/main_activity.xml file.
setContentView(R.layout.main_activity)
// Initialize member TextView so it is available later.
textView = findViewById(R.id.text_view)
}
// This callback is called only when there is a saved instance previously saved using
// onSaveInstanceState(). Some state is restored in onCreate(). Other state can optionally
// be restored here, possibly usable after onStart() has completed.
// The savedInstanceState Bundle is same as the one used in onCreate().
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
textView.text = savedInstanceState?.getString(TEXT_VIEW_KEY)
}
// Invoked when the activity might be temporarily destroyed; save the instance state here.
override fun onSaveInstanceState(outState: Bundle?) {
outState?.run {
putString(GAME_STATE_KEY, gameState)
putString(TEXT_VIEW_KEY, textView.text.toString())
}
// Call superclass to save any view hierarchy.
super.onSaveInstanceState(outState)
}
Java
TextView textView;
// Some transient state for the activity instance.
String gameState;
@Override
public void onCreate(Bundle savedInstanceState) {
// Call the superclass onCreate to complete the creation of
// the activity, like the view hierarchy.
super.onCreate(savedInstanceState);
// Recover the instance state.
if (savedInstanceState != null) {
gameState = savedInstanceState.getString(GAME_STATE_KEY);
}
// Set the user interface layout for this activity.
// The layout is defined in the project res/layout/main_activity.xml file.
setContentView(R.layout.main_activity);
// Initialize member TextView so it is available later.
textView = (TextView) findViewById(R.id.text_view);
}
// This callback is called only when there is a saved instance previously saved using
// onSaveInstanceState(). Some state is restored in onCreate(). Other state can optionally
// be restored here, possibly usable after onStart() has completed.
// The savedInstanceState Bundle is same as the one used in onCreate().
@Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
textView.setText(savedInstanceState.getString(TEXT_VIEW_KEY));
}
// Invoked when the activity might be temporarily destroyed; save the instance state here.
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putString(GAME_STATE_KEY, gameState);
outState.putString(TEXT_VIEW_KEY, textView.getText());
// Call superclass to save any view hierarchy.
super.onSaveInstanceState(outState);
}
במקום להגדיר את קובץ ה-XML ולהעביר אותו אל setContentView, אפשר ליצור אובייקטים חדשים של View בקוד הפעילות ולבנות היררכיית תצוגות על ידי הוספת אובייקטים חדשים של View אל ViewGroup. לאחר מכן משתמשים בפריסה הזו על ידי העברת הרכיב הבסיסי ViewGroup אל setContentView. מידע נוסף על יצירת ממשק משתמש זמין במסמכי התיעוד בנושא ממשק משתמש.
הפעילות לא נשארת במצב 'נוצרה'. אחרי שהשיטה onCreate מסיימת את ההרצה, הפעילות עוברת למצב Started והמערכת מפעילה את השיטות onStart ו-onResume בזו אחר זו.
onStart
כשהפעילות עוברת למצב Started, המערכת מפעילה את onStart.
הקריאה הזו גורמת לכך שהפעילות תהיה גלויה למשתמש בזמן שהאפליקציה מתכוננת להעביר את הפעילות לחזית ולהפוך אותה לאינטראקטיבית. לדוגמה, בשיטה הזו מאתחלים את הקוד שמנהל את ממשק המשתמש.
כשהפעילות עוברת למצב Started, כל רכיב שמודע למחזור החיים ומקושר למחזור החיים של הפעילות מקבל את האירוע ON_START.
השיטה onStart מסתיימת במהירות, וכמו במצב Created, הפעילות לא נשארת במצב Started. אחרי שהקריאה החוזרת הזו מסתיימת, הפעילות עוברת למצב Resumed והמערכת מפעילה את השיטה onResume.
onResume
כשהפעילות עוברת למצב Resumed, היא עוברת לחזית והמערכת מפעילה את הקריאה החוזרת onResume. זהו המצב שבו האפליקציה יוצרת אינטראקציה עם המשתמש. האפליקציה נשארת במצב הזה עד שקורה משהו שגורם להפסקת המיקוד באפליקציה, כמו קבלת שיחה במכשיר, מעבר של המשתמש לפעילות אחרת או כיבוי המסך של המכשיר.
כשהפעילות עוברת למצב Resumed, כל רכיב שמודע למחזור החיים ומקושר למחזור החיים של הפעילות מקבל את האירוע ON_RESUME. כאן רכיבי מחזור החיים יכולים להפעיל כל פונקציונליות שצריכה לפעול בזמן שהרכיב גלוי ובחזית, כמו הפעלת תצוגה מקדימה של המצלמה.
כשמתרחש אירוע שמשבש את הפעילות, היא עוברת למצב Paused והמערכת מפעילה את הקריאה החוזרת onPause.
אם הפעילות חוזרת למצב Resumed (המשך) מהמצב Paused (השהיה), המערכת קוראת שוב לשיטה onResume. לכן, צריך להטמיע את onResume כדי לאתחל רכיבים שמשחררים במהלך onPause ולבצע את כל האתחולים האחרים שצריכים להתרחש בכל פעם שהפעילות עוברת למצב Resumed.
הנה דוגמה לרכיב שמודע למחזור החיים שלו ומקבל גישה למצלמה כשהרכיב מקבל את האירוע ON_RESUME:
Kotlin
class CameraComponent : LifecycleObserver {
...
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun initializeCamera() {
if (camera == null) {
getCamera()
}
}
...
}
Java
public class CameraComponent implements LifecycleObserver {
...
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
public void initializeCamera() {
if (camera == null) {
getCamera();
}
}
...
}
הקוד שלמעלה מאתחל את המצלמה ברגע ש-LifecycleObserver מקבל את האירוע ON_RESUME. עם זאת, במצב מרובה חלונות, יכול להיות שהפעילות שלכם תהיה גלויה לחלוטין גם כשהיא במצב מושהה. לדוגמה, אם האפליקציה במצב ריבוי חלונות והמשתמש מקיש על החלון שלא מכיל את הפעילות שלכם, הפעילות עוברת למצב מושהה.
אם רוצים שהמצלמה תהיה פעילה רק כשהאפליקציה במצב Resumed (גלוי ופעיל בחזית), צריך לאתחל את המצלמה אחרי אירוע ON_RESUME שמוצג למעלה. אם רוצים שהמצלמה תמשיך לפעול בזמן שהפעילות מושהית אבל גלויה, למשל במצב מרובה חלונות, צריך להפעיל את המצלמה אחרי אירוע ON_START.
עם זאת, אם המצלמה פעילה בזמן שהפעילות מושהית, יכול להיות שאפליקציה אחרת שמופעלת מחדש במצב ריבוי חלונות לא תוכל לגשת למצלמה. לפעמים צריך להשאיר את המצלמה פעילה בזמן שהפעילות מושהית, אבל יכול להיות שזה יפגע בחוויית המשתמש הכוללת.
לכן, חשוב לחשוב היטב באיזה שלב במחזור החיים הכי מתאים להשתלט על משאבי מערכת משותפים בהקשר של מצב מרובה חלונות. מידע נוסף על תמיכה במצב ריבוי חלונות זמין במאמר תמיכה במצב ריבוי חלונות.
לא משנה באיזה אירוע הכנה תבחרו לבצע פעולת אתחול, חשוב להשתמש באירוע מחזור החיים המתאים כדי לשחרר את המשאב. אם אתם מאתחלים משהו אחרי אירוע ON_START, אתם צריכים לשחרר או לסיים אותו אחרי אירוע ON_STOP. אם אתם מזמינים את החדר אחרי האירוע ON_RESUME, אתם צריכים לבטל את ההזמנה אחרי האירוע ON_PAUSE.
בקטע הקוד הקודם, קוד ההפעלה של המצלמה ממוקם ברכיב שמודע למחזור החיים. אפשר להוסיף את הקוד הזה ישירות לקריאות החוזרות (callbacks) של מחזור החיים של הפעילות, כמו onStart ו-onStop, אבל אנחנו לא ממליצים על כך. הוספת הלוגיקה הזו לרכיב עצמאי שמודע למחזור החיים מאפשרת לכם לעשות שימוש חוזר ברכיב בכמה פעילויות בלי לשכפל את הקוד. במאמר טיפול במחזורי חיים באמצעות רכיבים שמודעים למחזור החיים (תצוגות) מוסבר איך ליצור רכיב שמודע למחזור החיים.
onPause
המערכת קוראת לשיטה הזו כאינדיקציה הראשונה לכך שהמשתמש עוזב את הפעילות, אבל זה לא תמיד אומר שהפעילות נהרסת. היא מציינת שהפעילות כבר לא בחזית, אבל היא עדיין גלויה אם המשתמש נמצא במצב ריבוי חלונות. יש כמה סיבות לכך שפעילות מסוימת עשויה לעבור למצב הזה:
- אירוע שמפריע להרצת האפליקציה, כמו שמתואר בקטע על הקריאה החוזרת (callback)
onResume, גורם להשהיית הפעילות הנוכחית. זה המקרה הנפוץ ביותר. - במצב מרובה חלונות, רק אפליקציה אחת פעילה בכל רגע, והמערכת משהה את כל האפליקציות האחרות.
- כשפעילות חדשה, שקופה למחצה, כמו תיבת דו-שיח, נפתחת, הפעילות שהיא מכסה מושהית. כל עוד הפעילות גלויה באופן חלקי אבל לא במוקד ההתעניינות, היא מושהית.
כשפעילות עוברת למצב Paused, כל רכיב שמודע למחזור החיים ומקושר למחזור החיים של הפעילות מקבל את האירוע ON_PAUSE. כאן רכיבי מחזור החיים יכולים להפסיק כל פונקציונליות שלא צריכה לפעול בזמן שהרכיב לא בחזית, כמו הפסקת תצוגה מקדימה של מצלמה.
משתמשים בשיטה onPause כדי להשהות או לשנות פעולות שלא יכולות להימשך, או שיכולות להימשך באופן מוגבל, בזמן ש-Activity נמצא במצב Paused (מושהה), ושצפוי שהן ימשיכו בקרוב.
אפשר גם להשתמש בשיטה onPause כדי לשחרר משאבי מערכת, ידיות לטיפול בחיישנים (כמו GPS) או משאבים אחרים שמשפיעים על חיי הסוללה בזמן שהפעילות מושהית והמשתמש לא צריך אותם.
עם זאת, כמו שצוין בקטע בנושא onResume, יכול להיות שפעילות מושהית עדיין תהיה גלויה במלואה אם האפליקציה פועלת במצב ריבוי חלונות. כדאי להשתמש ב-onStop במקום ב-onPause כדי לשחרר או להתאים באופן מלא משאבים ופעולות שקשורים לממשק המשתמש, וכך לשפר את התמיכה במצב מרובה חלונות.
בדוגמה הבאה, LifecycleObserver מגיב לאירוע ON_PAUSE
כמו בדוגמה הקודמת של אירוע ON_RESUME, ומשחרר את המצלמה שמופעלת אחרי קבלת האירוע ON_RESUME:
Kotlin
class CameraComponent : LifecycleObserver {
...
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun releaseCamera() {
camera?.release()
camera = null
}
...
}
Java
public class JavaCameraComponent implements LifecycleObserver {
...
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void releaseCamera() {
if (camera != null) {
camera.release();
camera = null;
}
}
...
}
בדוגמה הזו, קוד השחרור של המצלמה ממוקם אחרי שהאירוע ON_PAUSE מתקבל על ידי LifecycleObserver.
ההפעלה של onPause קצרה מאוד ולא בהכרח מספקת מספיק זמן לביצוע פעולות שמירה. לכן לא מומלץ להשתמש ב-onPause כדי לשמור נתונים של אפליקציות או נתוני משתמש, לבצע קריאות לרשת או להריץ טרנזקציות במסד נתונים.
יכול להיות שהעבודה הזו לא תסתיים לפני שהשיטה תסתיים.
במקום זאת, מומלץ לבצע פעולות כיבוי שדורשות עומס כבד במהלך onStop. בקטע הבא מפורטות פעולות מתאימות שאפשר לבצע במהלך onStop. מידע נוסף על שמירת נתונים זמין בקטע בנושא שמירה ושחזור של מצב.
השלמת השיטה onPause לא אומרת שהפעילות יוצאת ממצב מושהה. במקום זאת, הפעילות נשארת במצב הזה עד שהיא מתחדשת או עד שהיא הופכת לבלתי נראית לחלוטין למשתמש. אם הפעילות ממשיכה, המערכת מפעילה שוב את הקריאה החוזרת onResume.
אם הפעילות חוזרת ממצב מושהה למצב פעיל, המערכת שומרת את מופע Activity בזיכרון, ומזכירה את המופע הזה כשהמערכת מפעילה את onResume. בתרחיש הזה, אין צורך לאתחל מחדש רכיבים שנוצרו במהלך אחת משיטות הקריאה החוזרת שמובילות למצב Resumed. אם הפעילות הופכת לבלתי נראית לחלוטין, המערכת קוראת ל-onStop.
onStop
כשהפעילות כבר לא גלויה למשתמש, היא עוברת למצב Stopped, והמערכת מפעילה את הקריאה החוזרת onStop. זה יכול לקרות כשפעילות שהופעלה לאחרונה מכסה את כל המסך. המערכת קוראת גם ל-onStop
כשהפעילות מסתיימת ועומדת להסתיים.
כשהפעילות עוברת למצב Stopped, כל רכיב שמודע למחזור החיים ומקושר למחזור החיים של הפעילות מקבל את האירוע ON_STOP. כאן רכיבי מחזור החיים יכולים להפסיק כל פונקציונליות שלא צריכה לפעול בזמן שהרכיב לא מוצג על המסך.
בשיטה onStop, משחררים או משנים את המשאבים שלא נדרשים בזמן שהאפליקציה לא מוצגת למשתמש. לדוגמה, האפליקציה יכולה להשהות אנימציות או לעבור מעדכוני מיקום מדויקים לעדכוני מיקום בהערכה גסה. השימוש ב-onStop במקום ב-onPause מאפשר להמשיך בעבודה שקשורה לממשק המשתמש, גם כשהמשתמש צופה בפעילות במצב מרובה חלונות.
בנוסף, מומלץ להשתמש ב-onStop כדי לבצע פעולות כיבוי שדורשות יחסית הרבה משאבי מעבד. לדוגמה, אם לא הצלחתם למצוא זמן טוב יותר לשמירת מידע במסד נתונים, תוכלו לעשות זאת במהלך onStop. בדוגמה הבאה מוצגת הטמעה של onStop ששומרת את התוכן של טיוטת הערה באחסון מתמיד:
Kotlin
override fun onStop() {
// Call the superclass method first.
super.onStop()
// Save the note's current draft, because the activity is stopping
// and we want to be sure the current note progress isn't lost.
val values = ContentValues().apply {
put(NotePad.Notes.COLUMN_NAME_NOTE, getCurrentNoteText())
put(NotePad.Notes.COLUMN_NAME_TITLE, getCurrentNoteTitle())
}
// Do this update in background on an AsyncQueryHandler or equivalent.
asyncQueryHandler.startUpdate(
token, // int token to correlate calls
null, // cookie, not used here
uri, // The URI for the note to update.
values, // The map of column names and new values to apply to them.
null, // No SELECT criteria are used.
null // No WHERE columns are used.
)
}
Java
@Override
protected void onStop() {
// Call the superclass method first.
super.onStop();
// Save the note's current draft, because the activity is stopping
// and we want to be sure the current note progress isn't lost.
ContentValues values = new ContentValues();
values.put(NotePad.Notes.COLUMN_NAME_NOTE, getCurrentNoteText());
values.put(NotePad.Notes.COLUMN_NAME_TITLE, getCurrentNoteTitle());
// Do this update in background on an AsyncQueryHandler or equivalent.
asyncQueryHandler.startUpdate (
mToken, // int token to correlate calls
null, // cookie, not used here
uri, // The URI for the note to update.
values, // The map of column names and new values to apply to them.
null, // No SELECT criteria are used.
null // No WHERE columns are used.
);
}
בדוגמת הקוד שלמעלה נעשה שימוש ישירות ב-SQLite. עם זאת, מומלץ להשתמש ב-Room, ספריית התמדה שמספקת שכבת הפשטה מעל SQLite. במדריך Room persistence library אפשר לקרוא מידע נוסף על היתרונות של השימוש ב-Room ועל אופן ההטמעה של Room באפליקציה.
כשהפעילות עוברת למצב Stopped, אובייקט Activity נשאר בזיכרון: הוא שומר את כל המידע על המצב והחברים, אבל הוא לא מצורף למנהל החלונות. כשהפעילות מתחדשת, המידע הזה נטען מחדש.
אין צורך לאתחל מחדש רכיבים שנוצרו במהלך אחת משיטות הקריאה החוזרת (callback) שמובילות למצב Resumed. המערכת גם עוקבת אחרי המצב הנוכחי של כל אובייקט View בפריסה, כך שאם המשתמש מזין טקסט בווידג'ט EditText, התוכן הזה נשמר ולא צריך לשמור אותו ולשחזר אותו.
מצב Stopped (הופסקה): הפעילות חוזרת לאינטראקציה עם המשתמש או שהיא מסיימת את הפעולה ונעלמת. אם הפעילות חוזרת, המערכת מפעילה את onRestart. אם הפעולה של Activity מסתיימת, המערכת קוראת ל-onDestroy.
onDestroy
הקריאה ל-onDestroy מתבצעת לפני שהפעילות מושמדת. המערכת מפעילה את הקריאה החוזרת הזו מאחת משתי סיבות:
- הפעילות מסתיימת, כי המשתמש סגר את הפעילות לגמרי או כי הפונקציה
finishהופעלה בפעילות. - המערכת משמידה את הפעילות באופן זמני בגלל שינוי בהגדרה, כמו סיבוב המכשיר או מעבר למצב מרובה חלונות.
כשהפעילות עוברת למצב destroyed, כל רכיב שמודע למחזור החיים ומקושר למחזור החיים של הפעילות מקבל את האירוע ON_DESTROY. כאן רכיבי מחזור החיים יכולים לנקות את כל מה שהם צריכים לפני שהרכיב Activity מושמד.
במקום להוסיף לוגיקה ל-Activity כדי לקבוע למה הוא נמחק, אפשר להשתמש באובייקט ViewModel כדי להכיל את נתוני התצוגה הרלוונטיים ל-Activity. אם Activity נוצר מחדש בגלל שינוי בהגדרות, ViewModel לא צריך לעשות כלום, כי הוא נשמר ומועבר למופע הבא של Activity.
אם Activity לא נוצר מחדש, השיטה onCleared נקראת ב-ViewModel, שם אפשר לנקות את כל הנתונים שצריך לפני ההרס. אפשר להבחין בין שני התרחישים האלה באמצעות ה-method isFinishing.
אם הפעילות מסתיימת, onDestroy היא הקריאה החוזרת האחרונה במחזור החיים שהפעילות מקבלת. אם הפונקציה onDestroy מופעלת כתוצאה משינוי בהגדרות, המערכת יוצרת באופן מיידי מופע חדש של פעילות ואז מפעילה את הפונקציה onCreate במופע החדש בהגדרה החדשה.
פונקציית הקריאה החוזרת onDestroy משחררת את כל המשאבים שלא שוחררו על ידי פונקציות קריאה חוזרת קודמות, כמו onStop.
שמירה ושחזור של מצב ממשק משתמש זמני
משתמש מצפה שמצב ממשק המשתמש של פעילות מסוימת יישאר זהה לאורך שינוי בהגדרות, כמו סיבוב או מעבר למצב מרובה חלונות. עם זאת, המערכת משמידה את הפעילות כברירת מחדל כשמתרחש שינוי כזה בהגדרה, ומבטלת כל מצב של ממשק משתמש שמאוחסן במופע הפעילות.
באופן דומה, משתמש מצפה שמצב ממשק המשתמש יישאר זהה אם הוא עובר זמנית מהאפליקציה שלכם לאפליקציה אחרת ואז חוזר לאפליקציה שלכם מאוחר יותר. עם זאת, המערכת יכולה להרוס את התהליך של האפליקציה בזמן שהמשתמש לא נמצא ליד המכשיר והפעילות שלכם מופסקת.
כשמגבלות המערכת גורמות לכיבוי סופי של הפעילות, צריך לשמור את מצב ממשק המשתמש הזמני של המשתמש באמצעות שילוב של ViewModel, onSaveInstanceState ו/או אחסון מקומי. במאמר שמירת מצבי ממשק המשתמש מוסבר על הציפיות של המשתמשים בהשוואה להתנהגות המערכת, ואיך לשמור בצורה הטובה ביותר נתונים מורכבים של מצב ממשק המשתמש במהלך פעילות שהמערכת יזמה ובתהליך סגירה.
בקטע הזה מוסבר מהו מצב של מופע ואיך מטמיעים את השיטה onSaveInstance, שהיא קריאה חוזרת בפעילות עצמה. אם נתוני ממשק המשתמש קלים, אפשר להשתמש רק ב-onSaveInstance כדי לשמור את מצב ממשק המשתמש גם כשמבצעים שינויים בהגדרות וגם כשמערכת מפעילה סגירה של תהליך. אבל בגלל ש-onSaveInstance כרוך בעלויות סריאליזציה/דה-סריאליזציה, ברוב המקרים משתמשים גם ב-ViewModel וגם ב-onSaveInstance, כמו שמתואר במאמר שמירת מצבי ממשק המשתמש.
מצב המופע
יש כמה תרחישים שבהם הפעילות נמחקת בגלל התנהגות רגילה של האפליקציה, למשל כשהמשתמש לוחץ על הכפתור "הקודם" או כשהפעילות מסמנת את המחיקה שלה על ידי קריאה לשיטה finish.
כשפעילות נהרסת כי המשתמש לוחץ על'הקודם' או שהפעילות מסתיימת בעצמה, גם המערכת וגם המשתמש לא יכולים יותר לגשת למופע Activity הזה. בתרחישים האלה, הציפייה של המשתמש תואמת להתנהגות המערכת, ולא צריך לבצע פעולות נוספות.
עם זאת, אם המערכת משמידה את הפעילות בגלל מגבלות מערכת (כמו שינוי בהגדרה או עומס על הזיכרון), אז למרות שמופע Activity בפועל נעלם, המערכת זוכרת שהוא היה קיים. אם המשתמש מנסה לחזור לפעילות, המערכת יוצרת מופע חדש של הפעילות הזו באמצעות קבוצה של נתונים שמורים שמתארים את מצב הפעילות כשהיא נהרסה.
הנתונים השמורים שהמערכת משתמשת בהם כדי לשחזר את המצב הקודם נקראים מצב המופע. זהו אוסף של צמדי מפתח/ערך שמאוחסנים באובייקט Bundle. כברירת מחדל, המערכת משתמשת במצב המופע Bundle כדי לשמור מידע על כל אובייקט View בפריסת הפעילות, כמו ערך הטקסט שהוזן בווידג'ט EditText.
לכן, אם מופע הפעילות נהרס ונוצר מחדש, המצב של הפריסה משוחזר למצב הקודם ללא צורך בכתיבת קוד. עם זאת, יכול להיות שבפעילות שלכם יש מידע נוסף על המצב שתרצו לשחזר, כמו משתני חברים שעוקבים אחרי ההתקדמות של המשתמש בפעילות.
אובייקט Bundle לא מתאים לשמירה של כמות גדולה של נתונים, כי הוא דורש סריאליזציה בשרשור הראשי וצורכת זיכרון של תהליך המערכת. כדי לשמור יותר מנפח קטן מאוד של נתונים, כדאי לשלב בין כמה שיטות לשמירת נתונים, כמו שימוש באחסון מקומי קבוע, בשיטה onSaveInstanceState ובסיווג ViewModel, כפי שמתואר במאמר שמירת מצבי ממשק המשתמש.
שמירת מצב פשוט וקל של ממשק המשתמש באמצעות onSaveInstanceState
כשהפעילות מתחילה להיפסק, המערכת קוראת לשיטה onSaveInstanceState כדי שהפעילות תוכל לשמור מידע על המצב בחבילת מצב של מופע. ההטמעה שמוגדרת כברירת מחדל של השיטה הזו שומרת מידע זמני על מצב היררכיית התצוגה של הפעילות, כמו הטקסט בווידג'ט EditText או מיקום הגלילה בווידג'ט ListView.
כדי לשמור מידע נוסף על מצב המופע של הפעילות, צריך לבטל את ברירת המחדל של onSaveInstanceState ולהוסיף צמדי מפתח/ערך לאובייקט Bundle שנשמר במקרה שהפעילות נהרסת באופן בלתי צפוי. כשמבצעים שינוי של onSaveInstanceState, צריך לקרוא להטמעה של מחלקת העל אם רוצים שההטמעה שמוגדרת כברירת מחדל תשמור את המצב של היררכיית התצוגה.
כך זה נראה בדוגמה הבאה:
Kotlin
override fun onSaveInstanceState(outState: Bundle?) {
// Save the user's current game state.
outState?.run {
putInt(STATE_SCORE, currentScore)
putInt(STATE_LEVEL, currentLevel)
}
// Always call the superclass so it can save the view hierarchy state.
super.onSaveInstanceState(outState)
}
companion object {
val STATE_SCORE = "playerScore"
val STATE_LEVEL = "playerLevel"
}
Java
static final String STATE_SCORE = "playerScore";
static final String STATE_LEVEL = "playerLevel";
// ...
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
// Save the user's current game state.
savedInstanceState.putInt(STATE_SCORE, currentScore);
savedInstanceState.putInt(STATE_LEVEL, currentLevel);
// Always call the superclass so it can save the view hierarchy state.
super.onSaveInstanceState(savedInstanceState);
}
כדי לשמור נתונים קבועים, כמו העדפות משתמשים או נתונים למסד נתונים, צריך לנצל הזדמנויות מתאימות כשהפעילות מתבצעת בחזית. אם אין אפשרות כזו, צריך לשמור את הנתונים המתמשכים במהלך השימוש בשיטה onStop.
שחזור מצב ממשק המשתמש של הפעילות באמצעות מצב המכונה השמור
כשפעילות נוצרת מחדש אחרי שהיא נהרסה קודם, אפשר לשחזר את מצב המופע השמור מ-Bundle שהמערכת מעבירה לפעילות. גם שיטת הקריאה החוזרת onCreate וגם שיטת הקריאה החוזרת onRestoreInstanceState מקבלות את אותו Bundle שמכיל את פרטי מצב המופע.
השיטה onCreate נקראת בין אם המערכת יוצרת מופע חדש של הפעילות ובין אם היא יוצרת מחדש מופע קודם, ולכן צריך לבדוק אם המצב Bundle הוא null לפני שמנסים לקרוא אותו. אם הערך הוא null, המערכת יוצרת מופע חדש של הפעילות, במקום לשחזר מופע קודם שהושמד.
בקטע הקוד הבא אפשר לראות איך משחזרים נתוני מצב ב-onCreate:
Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) // Always call the superclass first
// Check whether we're recreating a previously destroyed instance.
if (savedInstanceState != null) {
with(savedInstanceState) {
// Restore value of members from saved state.
currentScore = getInt(STATE_SCORE)
currentLevel = getInt(STATE_LEVEL)
}
} else {
// Probably initialize members with default values for a new instance.
}
// ...
}
Java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // Always call the superclass first
// Check whether we're recreating a previously destroyed instance.
if (savedInstanceState != null) {
// Restore value of members from saved state.
currentScore = savedInstanceState.getInt(STATE_SCORE);
currentLevel = savedInstanceState.getInt(STATE_LEVEL);
} else {
// Probably initialize members with default values for a new instance.
}
// ...
}
במקום לשחזר את המצב במהלך onCreate, אפשר להטמיע את onRestoreInstanceState, שהמערכת קוראת לו אחרי השיטה onStart. המערכת קוראת ל-onRestoreInstanceState רק אם יש מצב שמור לשחזור, כך שלא צריך לבדוק אם Bundle הוא null.
Kotlin
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
// Always call the superclass so it can restore the view hierarchy.
super.onRestoreInstanceState(savedInstanceState)
// Restore state members from saved instance.
savedInstanceState?.run {
currentScore = getInt(STATE_SCORE)
currentLevel = getInt(STATE_LEVEL)
}
}
Java
public void onRestoreInstanceState(Bundle savedInstanceState) {
// Always call the superclass so it can restore the view hierarchy.
super.onRestoreInstanceState(savedInstanceState);
// Restore state members from saved instance.
currentScore = savedInstanceState.getInt(STATE_SCORE);
currentLevel = savedInstanceState.getInt(STATE_LEVEL);
}
ניווט בין פעילויות
סביר שאפליקציה תיכנס לפעילות ותצא ממנה, אולי הרבה פעמים, במהלך מחזור החיים של האפליקציה, למשל כשהמשתמש מקיש על הכפתור "הקודם" במכשיר או כשהפעילות מפעילה פעילות אחרת.
בקטע הזה מוסבר על נושאים שחשוב להכיר כדי להטמיע מעברים מוצלחים בין פעילויות. הנושאים האלה כוללים התחלת פעילות מפעילות אחרת, שמירת מצב הפעילות ושחזור מצב הפעילות.
התחלת פעילות אחת מתוך פעילות אחרת
לפעמים פעילות צריכה להתחיל פעילות אחרת בשלב מסוים. לדוגמה, כשצריך להעביר אפליקציה מהמסך הנוכחי למסך חדש.
בהתאם לשאלה אם הפעילות רוצה לקבל תוצאה מהפעילות החדשה שהיא עומדת להתחיל, מתחילים את הפעילות החדשה באמצעות השיטה startActivity או השיטה startActivityForResult. בכל מקרה, מעבירים אובייקט Intent.
אובייקט Intent מציין את הפעילות המדויקת שרוצים להתחיל או מתאר את סוג הפעולה שרוצים לבצע. המערכת בוחרת את הפעילות המתאימה לכם, שיכולה להיות גם מאפליקציה אחרת. אובייקט Intent יכול גם להעביר כמויות קטנות של נתונים לשימוש בפעילות שמופעלת. מידע נוסף על המחלקה Intent זמין במאמר Intents and Intent Filters.
startActivity
אם הפעילות החדשה שהופעלה לא צריכה להחזיר תוצאה, הפעילות הנוכחית יכולה להפעיל אותה באמצעות קריאה לשיטה startActivity.
כשעובדים באפליקציה, לעיתים קרובות צריך פשוט להפעיל פעילות מוכרת. לדוגמה, בקטע הקוד הבא מוצג איך להפעיל פעילות שנקראת SignInActivity.
Kotlin
val intent = Intent(this, SignInActivity::class.java)
startActivity(intent)
Java
Intent intent = new Intent(this, SignInActivity.class);
startActivity(intent);
יכול להיות שהאפליקציה תרצה גם לבצע פעולה מסוימת, כמו שליחת אימייל, הודעת טקסט או עדכון סטטוס, באמצעות נתונים מהפעילות שלכם. במקרה כזה, יכול להיות שלאפליקציה שלכם אין פעילויות משלה לביצוע הפעולות האלה, ולכן תוכלו להשתמש בפעילויות שסופקו על ידי אפליקציות אחרות במכשיר, שיכולות לבצע את הפעולות בשבילכם.
כאן באמת נכנסים לתמונה היתרונות של כוונות. אפשר ליצור כוונה שמתארת פעולה שרוצים לבצע, והמערכת מפעילה את הפעילות המתאימה מאפליקציה אחרת. אם יש כמה פעילויות שיכולות לטפל ב-Intent, המשתמש יכול לבחור באיזו מהן להשתמש. לדוגמה, אם רוצים לאפשר למשתמש לשלוח הודעת אימייל, אפשר ליצור את הכוונה הבאה:
Kotlin
val intent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_EMAIL, recipientArray)
}
startActivity(intent)
Java
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_EMAIL, recipientArray);
startActivity(intent);
התוספת EXTRA_EMAIL ל-Intent היא מערך מחרוזות של כתובות אימייל שהאימייל יישלח אליהן. כשיישום אימייל מגיב ל-Intent הזה, הוא קורא את מערך המחרוזות שסופק בתוספת ומציב את הכתובות בשדה 'אל' של טופס יצירת האימייל. במצב הזה, הפעילות של אפליקציית האימייל מתחילה, וכשהמשתמש מסיים, הפעילות שלכם ממשיכה.
startActivityForResult
לפעמים רוצים לקבל תוצאה מפעילות כשהיא מסתיימת. לדוגמה, אפשר להתחיל פעילות שמאפשרת למשתמש לבחור אדם מרשימת אנשי הקשר. בסיום, התמונה של האדם שנבחר מופיעה. כדי לעשות זאת, מפעילים את ה-method startActivityForResult(Intent, int), שבה פרמטר המספר השלם מזהה את הקריאה.
המזהה הזה נועד להבחין בין כמה קריאות ל-startActivityForResult(Intent, int) מאותה פעילות. זה לא מזהה גלובלי, ואין סיכון שהוא יתנגש עם אפליקציות או פעילויות אחרות. התוצאה מוחזרת באמצעות השיטה onActivityResult(int, int, Intent).
כשפעילות צאצא מסתיימת, היא יכולה לקרוא ל-setResult(int) כדי להחזיר נתונים לפעילות ההורה שלה. פעילות הילד צריכה לספק קוד תוצאה, שיכול להיות התוצאות הסטנדרטיות RESULT_CANCELED, RESULT_OK או כל ערך מותאם אישית שמתחיל ב-RESULT_FIRST_USER.
בנוסף, הפונקציה child activity יכולה להחזיר אובייקט Intent שמכיל נתונים נוספים שרוצים להחזיר. פעילות האב משתמשת בשיטה onActivityResult(int, int, Intent), יחד עם מזהה המספר השלם שסופק במקור על ידי פעילות האב, כדי לקבל את המידע.
אם פעילות של צאצא נכשלת מסיבה כלשהי, כמו קריסה, פעילות האב מקבלת תוצאה עם הקוד RESULT_CANCELED.
Kotlin
class MyActivity : Activity() {
// ...
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
// When the user center presses, let them pick a contact.
startActivityForResult(
Intent(Intent.ACTION_PICK,Uri.parse("content://contacts")),
PICK_CONTACT_REQUEST)
return true
}
return false
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
when (requestCode) {
PICK_CONTACT_REQUEST ->
if (resultCode == RESULT_OK) {
// A contact was picked. Display it to the user.
startActivity(Intent(Intent.ACTION_VIEW, intent?.data))
}
}
}
companion object {
internal val PICK_CONTACT_REQUEST = 0
}
}
Java
public class MyActivity extends Activity {
// ...
static final int PICK_CONTACT_REQUEST = 0;
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
// When the user center presses, let them pick a contact.
startActivityForResult(
new Intent(Intent.ACTION_PICK,
new Uri("content://contacts")),
PICK_CONTACT_REQUEST);
return true;
}
return false;
}
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
if (requestCode == PICK_CONTACT_REQUEST) {
if (resultCode == RESULT_OK) {
// A contact was picked. Display it to the user.
startActivity(new Intent(Intent.ACTION_VIEW, data));
}
}
}
}
תיאום פעילויות
כשפעילות אחת מתחילה פעילות אחרת, שתיהן עוברות שינויים במחזור החיים. הפעילות הראשונה מפסיקה לפעול ועוברת למצב מושהה או למצב עצירה, בזמן שהפעילות השנייה נוצרת. אם הפעילויות האלה משתפות נתונים שנשמרו בדיסק או במקום אחר, חשוב להבין שהפעילות הראשונה לא נעצרת לגמרי לפני שהפעילות השנייה נוצרת. במקום זאת, תהליך ההתחלה של התהליך השני חופף לתהליך העצירה של התהליך הראשון.
הסדר של קריאות חוזרות (callback) של מחזור החיים מוגדר היטב, במיוחד כששתי הפעילויות מתבצעות באותו תהליך – במילים אחרות, באותה אפליקציה – ואחת מתחילה את השנייה. זהו סדר הפעולות שמתרחש כשפעילות א' מתחילה פעילות ב':
- ה-method
onPauseשל פעילות א' מופעל. - השיטות
onCreate,onStartו-onResumeשל פעילות ב' מופעלות ברצף. המיקוד של המשתמש עובר לפעילות ב'. - אם הפעילות A לא גלויה יותר במסך, הפונקציה
onStopשלה מופעלת.
רצף הפונקציות האלה מאפשר לכם לנהל את המעבר של מידע מפעילות אחת לפעילות אחרת.