טיפול במחזורי חיים באמצעות רכיבים שמודעים למחזור החיים (תצוגות)

מושגים ויישום ב-Jetpack פיתוח נייטיב

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

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

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

מדינה

המצב הנוכחי של הרכיב שנמצא במעקב על ידי אובייקט 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 יש. יש לה method אחת, ‏ getLifecycle, שצריך להטמיע במחלקה. אם אתם מנסים לנהל את מחזור החיים של תהליך שלם של אפליקציה, כדאי לעיין במאמר ProcessLifecycleOwner.

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

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

בדוגמה של מעקב אחר מיקום, אפשר להגדיר את המחלקה MyLocationListener כך שתטמיע את DefaultLifecycleObserver ואז לאתחל אותה באמצעות Lifecycle של הפעילות ב-method‏ 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 לא במצב טוב כרגע. לדוגמה, אם הקריאה החוזרת מפעילה טרנזקציה של קטע אחרי שמצב הפעילות נשמר, היא תגרום לקריסה, ולכן לא נרצה להפעיל את הקריאה החוזרת הזו.

כדי להקל על תרחיש השימוש הזה, המחלקה 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 ו-Activities בספריית התמיכה מגרסה 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 כדי להימנע מקוד שחוזר על עצמו (boilerplate) ולקבל הפשטה טובה יותר.
  • אם ממשק המשתמש שלכם מורכב, כדאי ליצור מחלקה של שכבת Presentation כדי לטפל בשינויים בממשק המשתמש. יכול להיות שזו משימה מייגעת, אבל היא יכולה להקל על הבדיקה של רכיבי ממשק המשתמש.
  • אל תפנו להקשר של View או Activity בViewModel. אם ViewModel ממשיך להתקיים אחרי שהפעילות מסתיימת (במקרה של שינויים בהגדרות), הפעילות שלכם נחשפת ולא נמחקת כמו שצריך על ידי איסוף האשפה.
  • אפשר להשתמש ב-Kotlin coroutines כדי לנהל משימות ארוכות טווח ופעולות אחרות שיכולות לפעול באופן אסינכרוני.

תרחישי שימוש ברכיבים שמודעים למחזור החיים

רכיבים שמודעים למחזור החיים יכולים להקל מאוד על ניהול מחזורי החיים במגוון תרחישים. הנה כמה דוגמאות:

  • מעבר בין עדכוני מיקום גסים לבין עדכוני מיקום פרטניים. כדאי להשתמש ברכיבים שמודעים למחזור החיים כדי לאפשר עדכוני מיקום מדויקים בזמן שאפליקציית המיקום גלויה, ולעבור לעדכוני מיקום גסים כשהאפליקציה פועלת ברקע. ‫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 צריכה להטמיע את הפתרון העקיף שסופק על ידי גרסה beta 2 ומטה של Lifecycle.

מקורות מידע נוספים

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

דוגמיות

  • Sunflower, אפליקציית הדגמה שמציגה שיטות מומלצות לשימוש ברכיבי ארכיטקטורה

Codelabs

בלוגים