טיפול במחזורי חיים באמצעות רכיבים מודעים למחזור החיים   חלק מ-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.
תרשים של מצבי מחזור החיים
איור 1. מצבים ואירועים שמרכיבים את מחזור החיים של הפעילות ב-Android

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

כדי למעקב אחרי סטטוס מחזור החיים של רכיב, אפשר להטמיע את 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

בלוגים