טיפול במחזורי חיים באמצעות רכיבים מודעים למחזור החיים חלק מ-Android Jetpack.
רכיבים מודעים למחזור החיים מבצעים פעולות בתגובה לשינוי בסטטוס מחזור החיים של רכיב אחר, כמו פעילויות וקטעים. הרכיבים האלה עוזרים ליצור קוד מאורגן יותר, ולרוב קל יותר, שקל יותר לתחזק אותו.
דפוס נפוץ הוא להטמיע את הפעולות של הרכיבים התלויים בשיטות מחזור החיים של הפעילויות והקטעים. עם זאת, התבנית הזו מובילה לארגון לקוי של הקוד ולשגיאות רבות. שימוש ברכיבים שמותאמים למחזור חיים מאפשר להעביר את הקוד של רכיבים תלויים מהשיטות של מחזור החיים לרכיבים עצמם.
החבילה androidx.lifecycle
מספקת כיתות וממשקים שמאפשרים ליצור רכיבים מודעים למחזור החיים – רכיבים שיכולים לשנות את ההתנהגות שלהם באופן אוטומטי על סמך המצב הנוכחי של מחזור החיים של פעילות או קטע.
לרוב רכיבי האפליקציה שמוגדרים ב-Android Framework יש מחזור חיים. מחזור החיים מנוהל על ידי מערכת ההפעלה או קוד המסגרת שפועל בתהליך. הן מהותיות לאופן שבו Android פועלת, והאפליקציה שלכם חייבת לפעול בהתאם להן. אם לא תעשו זאת, יכול להיות שיהיו דליפות זיכרון או אפילו קריסות של האפליקציה.
נניח שיש לנו פעילות שמציגה את מיקום המכשיר במסך. הטמעה נפוצה עשויה להיראות כך:
Kotlin
internal class MyLocationListener( private val context: Context, private val callback: (Location) -> Unit ) { fun start() { // connect to system location service } fun stop() { // disconnect from system location service } } class MyActivity : AppCompatActivity() { private lateinit var myLocationListener: MyLocationListener override fun onCreate(...) { myLocationListener = MyLocationListener(this) { location -> // update UI } } public override fun onStart() { super.onStart() myLocationListener.start() // manage other components that need to respond // to the activity lifecycle } public override fun onStop() { super.onStop() myLocationListener.stop() // manage other components that need to respond // to the activity lifecycle } }
Java
class MyLocationListener { public MyLocationListener(Context context, Callback callback) { // ... } void start() { // connect to system location service } void stop() { // disconnect from system location service } } class MyActivity extends AppCompatActivity { private MyLocationListener myLocationListener; @Override public void onCreate(...) { myLocationListener = new MyLocationListener(this, (location) -> { // update UI }); } @Override public void onStart() { super.onStart(); myLocationListener.start(); // manage other components that need to respond // to the activity lifecycle } @Override public void onStop() { super.onStop(); myLocationListener.stop(); // manage other components that need to respond // to the activity lifecycle } }
הדוגמה הזו נראית בסדר, אבל באפליקציה אמיתית, בסופו של דבר יש יותר מדי קריאות שמנהלות את ממשק המשתמש ורכיבים אחרים בתגובה למצב הנוכחי של מחזור החיים. ניהול של כמה רכיבים מחייב להוסיף כמות משמעותית של קוד לשיטות של מחזור החיים, כמו onStart()
ו-onStop()
, ולכן קשה יותר לתחזק אותן.
בנוסף, אין ערובה שהרכיב יתחיל לפעול לפני שהפעילות או החלק יופסקו. זה נכון במיוחד אם אנחנו צריכים לבצע פעולה ממושכת, כמו בדיקת הגדרות מסוימת ב-onStart()
. המצב הזה עלול לגרום לתנאי מרוץ שבו השיטה onStop()
מסתיימת לפני השיטה onStart()
, וכתוצאה מכך הרכיב נשאר פעיל יותר מהנדרש.
Kotlin
class MyActivity : AppCompatActivity() { private lateinit var myLocationListener: MyLocationListener override fun onCreate(...) { myLocationListener = MyLocationListener(this) { location -> // update UI } } public override fun onStart() { super.onStart() Util.checkUserStatus { result -> // what if this callback is invoked AFTER activity is stopped? if (result) { myLocationListener.start() } } } public override fun onStop() { super.onStop() myLocationListener.stop() } }
Java
class MyActivity extends AppCompatActivity { private MyLocationListener myLocationListener; public void onCreate(...) { myLocationListener = new MyLocationListener(this, location -> { // update UI }); } @Override public void onStart() { super.onStart(); Util.checkUserStatus(result -> { // what if this callback is invoked AFTER activity is stopped? if (result) { myLocationListener.start(); } }); } @Override public void onStop() { super.onStop(); myLocationListener.stop(); } }
החבילה androidx.lifecycle
מספקת כיתות וממשקים שעוזרים לטפל בבעיות האלה בצורה עמידת ומבודדת.
מחזור חיים
Lifecycle
היא כיתה שמכילה את המידע על מצב מחזור החיים של רכיב (כמו פעילות או קטע) ומאפשרת לאובייקטים אחרים לצפות במצב הזה.
Lifecycle
משתמש בשתי קבוצות של ערכים ראשיים כדי לעקוב אחרי סטטוס מחזור החיים של הרכיב המשויך:
- אירוע
- אירועי מחזור החיים שנשלחים מהמסגרת ומהקלאס
Lifecycle
. האירועים האלה ממפים לאירועי ה-callback בפעילויות ובקטעים. - מדינה
- המצב הנוכחי של הרכיב שמנוטר על ידי האובייקט
Lifecycle
.
אפשר לחשוב על המצבים בתור צמתים של תרשים ועל האירועים בתור הקצוות בין הצמתים האלה.
כדי למעקב אחרי סטטוס מחזור החיים של רכיב, אפשר להטמיע את DefaultLifecycleObserver
ולשנות את הגדרות ברירת המחדל של השיטות התואמות, כמו onCreate
, onStart
וכו'. לאחר מכן, אפשר להוסיף משתמש מעקב על ידי קריאה לשיטה addObserver()
של המחלקה Lifecycle
והעברת מופע של משתמש המעקב, כפי שמתואר בדוגמה הבאה:
Kotlin
class MyObserver : DefaultLifecycleObserver { override fun onResume(owner: LifecycleOwner) { connect() } override fun onPause(owner: LifecycleOwner) { disconnect() } } myLifecycleOwner.getLifecycle().addObserver(MyObserver())
Java
public class MyObserver implements DefaultLifecycleObserver { @Override public void onResume(LifecycleOwner owner) { connect() } @Override public void onPause(LifecycleOwner owner) { disconnect() } } myLifecycleOwner.getLifecycle().addObserver(new MyObserver());
בדוגמה שלמעלה, האובייקט myLifecycleOwner
מטמיע את הממשק LifecycleOwner
שמוסבר בקטע הבא.
LifecycleOwner
LifecycleOwner
הוא ממשק עם שיטה אחת שמציין שלכיתה יש Lifecycle
. יש לו שיטה אחת, getLifecycle()
, שצריך להטמיע במחלקה.
אם אתם מנסים לנהל את מחזור החיים של תהליך אפליקציה שלם, תוכלו לעיין במאמר ProcessLifecycleOwner
.
הממשק הזה מספק ניתוח סכמתי של הבעלות על Lifecycle
מתוך כיתות נפרדות, כמו Fragment
ו-AppCompatActivity
, ומאפשר לכתוב רכיבים שפועלים איתם. כל מחלקת אפליקציה בהתאמה אישית יכולה להטמיע את הממשק LifecycleOwner
.
רכיבים שמטמיעים את DefaultLifecycleObserver
פועלים בצורה חלקה עם רכיבים שמטמיעים את LifecycleOwner
, כי הבעלים יכול לספק מחזור חיים, והצופה יכול להירשם כדי לצפות בו.
בדוגמה למעקב אחר מיקום, אפשר לגרום לכיתה MyLocationListener
להטמיע את DefaultLifecycleObserver
ואז לאתחל אותה באמצעות Lifecycle
של הפעילות בשיטה onCreate()
. כך אפשר להפוך את הכיתה MyLocationListener
לעצמאית, כלומר ההצהרה על הלוגיקה לתגובה לשינויים בסטטוס מחזור החיים מתבצעת ב-MyLocationListener
במקום בפעילות. כשהרכיבים השונים שומרים את הלוגיקה שלהם, קל יותר לנהל את הלוגיקה של הפעילויות והקטעים.
Kotlin
class MyActivity : AppCompatActivity() { private lateinit var myLocationListener: MyLocationListener override fun onCreate(...) { myLocationListener = MyLocationListener(this, lifecycle) { location -> // update UI } Util.checkUserStatus { result -> if (result) { myLocationListener.enable() } } } }
Java
class MyActivity extends AppCompatActivity { private MyLocationListener myLocationListener; public void onCreate(...) { myLocationListener = new MyLocationListener(this, getLifecycle(), location -> { // update UI }); Util.checkUserStatus(result -> { if (result) { myLocationListener.enable(); } }); } }
תרחיש לדוגמה נפוץ הוא הימנעות מהפעלת קריאות חזרה מסוימות אם Lifecycle
לא נמצא במצב תקין כרגע. לדוגמה, אם פונקציית ה-callback מריצה טרנזקציה של קטע קוד אחרי שמצב הפעילות נשמר, היא תגרום לקריסה, ולכן לעולם לא כדאי להפעיל את פונקציית ה-callback הזו.
כדי להקל על תרחיש לדוגמה הזה, הכיתה Lifecycle
מאפשרת לאובייקטים אחרים לשלוח שאילתות לגבי המצב הנוכחי.
Kotlin
internal class MyLocationListener( private val context: Context, private val lifecycle: Lifecycle, private val callback: (Location) -> Unit ): DefaultLifecycleObserver { private var enabled = false override fun onStart(owner: LifecycleOwner) { if (enabled) { // connect } } fun enable() { enabled = true if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { // connect if not connected } } override fun onStop(owner: LifecycleOwner) { // disconnect if connected } }
Java
class MyLocationListener implements DefaultLifecycleObserver { private boolean enabled = false; public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) { ... } @Override public void onStart(LifecycleOwner owner) { if (enabled) { // connect } } public void enable() { enabled = true; if (lifecycle.getCurrentState().isAtLeast(STARTED)) { // connect if not connected } } @Override public void onStop(LifecycleOwner owner) { // disconnect if connected } }
בעזרת ההטמעה הזו, הכיתה LocationListener
מודעת לחלוטין למחזור החיים. אם אנחנו צריכים להשתמש ב-LocationListener
שלנו מפעילות או מקטע אחרים, אנחנו רק צריכים לאתחל אותו. כל פעולות ההגדרה והפירוק מנוהלות על ידי הכיתה עצמה.
אם ספרייה מספקת כיתות שצריכות לפעול עם מחזור החיים של Android, מומלץ להשתמש ברכיבים שמותאמים למחזור החיים. לקוחות הספרייה יכולים לשלב את הרכיבים האלה בקלות, בלי ניהול ידני של מחזור החיים בצד הלקוח.
הטמעה של LifecycleOwner בהתאמה אישית
קטעי קוד (fragments) ופעילויות ב-Support Library מגרסה 26.1.0 ואילך כבר מיישמים את הממשק LifecycleOwner
.
אם יש לכם כיתה בהתאמה אישית שאתם רוצים ליצור לה LifecycleOwner
, תוכלו להשתמש בכיתה LifecycleRegistry, אבל תצטרכו להעביר אירועים לכיתה הזו, כפי שמתואר בדוגמת הקוד הבאה:
Kotlin
class MyActivity : Activity(), LifecycleOwner { private lateinit var lifecycleRegistry: LifecycleRegistry override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleRegistry = LifecycleRegistry(this) lifecycleRegistry.markState(Lifecycle.State.CREATED) } public override fun onStart() { super.onStart() lifecycleRegistry.markState(Lifecycle.State.STARTED) } override fun getLifecycle(): Lifecycle { return lifecycleRegistry } }
Java
public class MyActivity extends Activity implements LifecycleOwner { private LifecycleRegistry lifecycleRegistry; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); lifecycleRegistry = new LifecycleRegistry(this); lifecycleRegistry.markState(Lifecycle.State.CREATED); } @Override public void onStart() { super.onStart(); lifecycleRegistry.markState(Lifecycle.State.STARTED); } @NonNull @Override public Lifecycle getLifecycle() { return lifecycleRegistry; } }
שיטות מומלצות לרכיבים עם תמיכה במחזור חיים
- חשוב שהפקדים של ממשק המשתמש (פעילויות וקטעי קוד) יהיו יעילים ככל האפשר. הם לא צריכים לנסות לקבל נתונים משלהם, אלא להשתמש ב-
ViewModel
כדי לעשות זאת, ולעקוב אחרי אובייקטLiveData
כדי לשקף את השינויים בחזרה לתצוגות. - נסו לכתוב ממשקי משתמש מבוססי-נתונים שבהם לפקח על ממשק המשתמש תהיה אחריות לעדכן את התצוגות כשהנתונים משתנים, או לדווח על פעולות של משתמשים בחזרה ל-
ViewModel
. - מוסיפים את לוגיק הנתונים לכיתה
ViewModel
.ViewModel
צריך לשמש כמחבר בין הבקר של ממשק המשתמש לשאר האפליקציה. עם זאת, חשוב לזכור ש-ViewModel
לא אחראי לאחזור נתונים (למשל, מרשת). במקום זאת,ViewModel
צריך להפעיל את הרכיב המתאים כדי לאחזר את הנתונים, ואז לספק את התוצאה בחזרה למסוף הבקרה של ממשק המשתמש. - משתמשים בקישור נתונים כדי לשמור על ממשק נקי בין התצוגות לבין בקר ממשק המשתמש. כך תוכלו לשפר את האופן שבו התצוגות מפורטות, ולצמצם את כמות הקוד לעדכון שצריך לכתוב בפעילויות ובקטעים. אם אתם מעדיפים לעשות זאת בשפת התכנות Java, תוכלו להשתמש בספרייה כמו Butter Knife כדי להימנע מקוד סטנדרטי ולקבל הפשטה טובה יותר.
- אם ממשק המשתמש שלכם מורכב, כדאי ליצור את הכיתה presenter כדי לטפל בשינויים בממשק המשתמש. זו עשויה להיות משימה מפרכת, אבל היא יכולה להקל על בדיקת רכיבי ממשק המשתמש.
- הימנעו מהפניה להקשר של
View
אוActivity
ב-ViewModel
. אם ה-ViewModel
ימשיך להתקיים אחרי הפעילות (במקרה של שינויים בהגדרות), הפעילות תדלוף ולא תימחק כראוי על ידי מנהל האשפה. - שימוש בקורוטינים של Kotlin לניהול משימות ממושכות ופעולות אחרות שיכולות לפעול באופן אסינכרוני.
תרחישים לדוגמה לרכיבים עם תמיכה במחזור חיים
רכיבים עם תמיכה במחזור חיים יכולים להקל מאוד על ניהול מחזורי החיים במגוון מקרים. הנה כמה דוגמאות:
- מעבר בין עדכוני מיקום כלליים לעדכוני מיקום מפורטים. כדאי להשתמש ברכיבים שמותאמים למחזור החיים כדי לאפשר עדכוני מיקום מפורטים בזמן שאפליקציית המיקום גלויה, ולעבור לעדכונים פחות מפורטים כשהאפליקציה פועלת ברקע.
LiveData
הוא רכיב שמתעדכן לפי מחזור החיים, ומאפשר לאפליקציה לעדכן את ממשק המשתמש באופן אוטומטי כשהמשתמש משנה מיקומים. - עצירה והפעלה של האחסון במטמון של סרטונים. כדאי להשתמש ברכיבים שמזהים את מחזור החיים כדי להתחיל את האחסון במטמון של הסרטון בהקדם האפשרי, אבל לדחות את ההפעלה עד שהאפליקציה מופעלת במלואה. אפשר גם להשתמש ברכיבים מודעים למחזור החיים כדי לסיים את האחסון במטמון כשהאפליקציה נהרסת.
- הפעלה והפסקה של קישוריות לרשת. שימוש ברכיבים שמותאמים למחזור החיים כדי לאפשר עדכון בזמן אמת (סטרימינג) של נתוני הרשת בזמן שהאפליקציה נמצאת בחזית, וגם להשהות אותם באופן אוטומטי כשהאפליקציה עוברת לרקע.
- השהיה והפעלה מחדש של רכיבי drawable עם אנימציה. משתמשים ברכיבים שמותאמים למחזור החיים כדי להשהות רכיבי drawable מונפשים כשהאפליקציה ברקע, ולהמשיך את ההפעלה שלהם כשהאפליקציה בחזית.
טיפול באירועי עצירה
כש-Lifecycle
שייך ל-AppCompatActivity
או ל-Fragment
, המצב של Lifecycle
משתנה ל-CREATED
והאירוע ON_STOP
נשלח כשמתבצעת קריאה ל-onSaveInstanceState()
של AppCompatActivity
או של Fragment
.
כשמצב של Fragment
או AppCompatActivity
נשמר באמצעות onSaveInstanceState()
, ממשק המשתמש שלו נחשב לבלתי ניתן לשינוי עד שמפעילים את ON_START
. ניסיון לשנות את ממשק המשתמש אחרי שמירת המצב עלול לגרום לאי-עקביות במצב הניווט של האפליקציה. לכן, FragmentManager
יוצר חריגה אם האפליקציה מפעילה FragmentTransaction
אחרי שמירת המצב. פרטים נוספים מופיעים בכתובת commit()
.
LiveData
מונע את מקרה הקצה הזה כברירת מחדל, על ידי הימנעות מהפעלת הצופה שלו אם הערך המשויך של Lifecycle
של הצופה הוא פחות מ-STARTED
.
מאחורי הקלעים, הוא קורא ל-isAtLeast()
לפני שהוא מחליט להפעיל את המתבונן שלו.
לצערנו, השיטה onStop()
של AppCompatActivity
נקראת אחרי
onSaveInstanceState()
, מה שמשאיר פער שבו לא ניתן לשנות את מצב ממשק המשתמש, אבל Lifecycle
עדיין לא הועבר למצב CREATED
.
כדי למנוע את הבעיה הזו, הכיתה Lifecycle
בגרסה beta2
ואילך מסמנת את המצב בתור CREATED
בלי לשלוח את האירוע, כך שכל קוד שבודק את המצב הנוכחי מקבל את הערך האמיתי, למרות שהאירוע לא נשלח עד שהמערכת קוראת ל-onStop()
.
לצערי, לפתרון הזה יש שתי בעיות עיקריות:
- ברמת API 23 ומטה, מערכת Android שומרת את המצב של פעילות גם אם היא מכוסה חלקית על ידי פעילות אחרת. במילים אחרות, מערכת Android קוראת ל-
onSaveInstanceState()
אבל לא בהכרח קוראת ל-onStop()
. כתוצאה מכך, יכול להיות שייווצר מרווח זמן ארוך שבו המתבונן עדיין יחשוב שמחזור החיים פעיל, למרות שאי אפשר לשנות את מצב ממשק המשתמש שלו. - כל מחלקה שרוצים לחשוף התנהגות דומה למחלקה
LiveData
צריכה להטמיע את הפתרון החלופי שזמין ב-Lifecycle
בגרסהbeta 2
ומטה.
מקורות מידע נוספים
למידע נוסף על טיפול במחזורי חיים באמצעות רכיבים מודעים למחזור חיים, תוכלו לעיין במקורות המידע הנוספים הבאים.
דוגמיות
- Sunflower, אפליקציית הדגמה שממחישה שיטות מומלצות לשימוש ברכיבי ארכיטקטורה
Codelabs
בלוגים
מומלץ עבורך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- סקירה כללית על LiveData
- שימוש ב-coroutines של Kotlin עם רכיבים מודעים למחזור חיים
- מודול של מצב ששמור ל-ViewModel