ב-Jetpack Compose, אובייקט יכול להטמיע את RememberObserver כדי לקבל קריאות חוזרות (callback) כשמשתמשים בו עם remember, וכך לדעת מתי הוא מתחיל להיות חלק מהיררכיית הקומפוזיציה ומתי הוא מפסיק להיות חלק ממנה. באופן דומה, אפשר להשתמש ב-RetainObserver כדי לקבל מידע על מצב של אובייקט שנעשה בו שימוש עם retain.
לגבי אובייקטים שמשתמשים במידע הזה על מחזור החיים מהיררכיית הקומפוזיציה, מומלץ לפעול לפי כמה שיטות מומלצות כדי לוודא שהאובייקטים מתנהגים בצורה תקינה בפלטפורמה ומספקים הגנה מפני שימוש לרעה. באופן ספציפי, צריך להשתמש בקריאות החוזרות (callback) onRemembered (או onRetained) כדי להפעיל עבודה במקום בבונה (constructor), לבטל את כל העבודה כשאובייקטים מפסיקים להיות בזיכרון או נשמרים, ולהימנע מחשיפת יישומים של RememberObserver ו-RetainObserver כדי למנוע קריאות לא מכוונות. בקטע הבא מוסבר על ההמלצות האלה בפירוט רב יותר.
אתחול וניקוי באמצעות RememberObserver ו-RetainObserver
במדריך Thinking in Compose מתואר המודל המנטלי שמאחורי הקומפוזיציה. כשעובדים עם RememberObserver ו-RetainObserver, חשוב לזכור שני אופנים של יצירת מוזיקה:
- הסידור מחדש הוא אופטימי ויכול להיות שהוא יבוטל
- לכל הפונקציות שניתנות להרכבה לא צריכות להיות תופעות לוואי
הפעלת תופעות לוואי של אתחול במהלך onRemembered או onRetained, ולא במהלך בנייה
כשזוכרים או שומרים אובייקטים, פונקציית ה-lambda של החישוב פועלת כחלק מההרכבה. מאותן סיבות שבגללן לא כדאי לבצע פעולות עם תופעות לוואי או להפעיל קורוטינה במהלך הקומפוזיציה, לא כדאי גם לבצע פעולות עם תופעות לוואי בביטוי הלמדה של החישוב שמועבר אל remember, אל retain ואל הווריאציות שלהם.
זה כולל כחלק מהבונה של האובייקטים שנשמרו או שנשמרו בזיכרון.
במקום זאת, כשמטמיעים את RememberObserver או את RetainObserver, צריך לוודא שכל האפקטים והמשימות שהופעלו נשלחים ב-callback של onRemembered.
התזמון של העדכונים זהה לזה של ממשקי ה-API של SideEffect. היא גם מבטיחה שהאפקטים האלה יופעלו רק כשהקומפוזיציה מוחלת, וכך מונעת משימות יתומות ודליפות זיכרון אם קומפוזיציה מחדש ננטשת או נדחית.
class MyComposeObject : RememberObserver { private val job = Job() private val coroutineScope = CoroutineScope(Dispatchers.Main + job) init { // Not recommended: This will cause work to begin during composition instead of // with other effects. Move this into onRemembered(). coroutineScope.launch { loadData() } } override fun onRemembered() { // Recommended: Move any cancellable or effect-driven work into the onRemembered // callback. If implementing RetainObserver, this should go in onRetained. coroutineScope.launch { loadData() } } private suspend fun loadData() { /* ... */ } // ... }
הסרת מכשיר שנשכח, יצא משימוש או ננטש
כדי למנוע דליפת משאבים או השארת עבודות ברקע ללא בעלים, צריך גם להשמיד אובייקטים שנשמרו. באובייקטים שמטמיעים את RememberObserver, המשמעות היא שכל מה שמאותחל ב-onRemembered חייב להיות תואם לקריאה ל-release ב-onForgotten.
מכיוון שאפשר לבטל הרכבה, אובייקטים שמטמיעים את RememberObserver צריכים גם לנקות אחרי עצמם אם הם ננטשים בהרכבות. אובייקט נחשב לנטוש אם הוא מוחזר על ידי remember בקומפוזיציה שמבוטלת או נכשלת. (המצב הזה קורה בדרך כלל כשמשתמשים ב-PausableComposition, ויכול לקרות גם כשמשתמשים בטעינה מחדש מהירה עם כלי התצוגה המקדימה של Android Studio.)
כשמוותרים על אובייקט שנשמר בזיכרון, הוא מקבל רק את הקריאה ל-onAbandoned (ולא קריאה ל-onRemembered). כדי להטמיע את שיטת הוויתור, צריך להשמיד את כל מה שנוצר בין אתחול האובייקט לבין הרגע שבו האובייקט היה אמור לקבל את הקריאה החוזרת onRemembered.
class MyComposeObject : RememberObserver { private val job = Job() private val coroutineScope = CoroutineScope(Dispatchers.Main + job) // ... override fun onForgotten() { // Cancel work launched from onRemembered. If implementing RetainObserver, onRetired // should cancel work launched from onRetained. job.cancel() } override fun onAbandoned() { // If any work was launched by the constructor as part of remembering the object, // you must cancel that work in this callback. For work done as part of the construction // during retain, this code should will appear in onUnused. job.cancel() } }
שמירה על הפרטיות של ההטמעות של RememberObserver ו-RetainObserver
כשכותבים ממשקי API ציבוריים, צריך להיזהר כשמרחיבים את RememberObserver ואת RetainObserver ביצירת מחלקות שמוחזרות באופן ציבורי. יכול להיות שמשתמש לא יזכור את האובייקט שלכם כשאתם מצפים שהוא יזכור אותו, או שהוא יזכור אותו בצורה שונה מזו שהתכוונתם. לכן, לא מומלץ לחשוף קונסטרוקטורים או פונקציות factory לאובייקטים שמטמיעים את RememberObserver או RetainObserver. שימו לב: זה תלוי בסוג זמן הריצה של מחלקה, ולא בסוג המוצהר. אם זוכרים אובייקט שמטמיע את RememberObserver או RetainObserver אבל מועבר אל Any, האובייקט עדיין יקבל קריאות חוזרות.
לא מומלץ:
abstract class MyManager
// Not Recommended: Exposing a public constructor (even implicitly) for an object implementing
// RememberObserver can cause unexpected invocations if it is remembered multiple times.
class MyComposeManager : MyManager(), RememberObserver { ... }
// Not Recommended: The return type may be an implementation of RememberObserver and should be
// remembered explicitly.
fun createFoo(): MyManager = MyComposeManager()
המלצה:
abstract class MyManager class MyComposeManager : MyManager() { // Callers that construct this object must manually call initialize and teardown fun initialize() { /*...*/ } fun teardown() { /*...*/ } } @Composable fun rememberMyManager(): MyManager { // Protect the RememberObserver implementation by never exposing it outside the library return remember { object : RememberObserver { val manager = MyComposeManager() override fun onRemembered() = manager.initialize() override fun onForgotten() = manager.teardown() override fun onAbandoned() { /* Nothing to do if manager hasn't initialized */ } } }.manager }
שיקולים כשזוכרים אובייקטים
בנוסף להמלצות הקודמות לגבי RememberObserver ו-RetainObserver, מומלץ גם להיזהר מלהפעיל בטעות שוב את הפונקציה לזכירת אובייקטים, ולנסות להימנע מכך, כדי לשמור על הביצועים ועל הדיוק. בקטעים הבאים מוסבר יותר לעומק על תרחישים ספציפיים של שינוי זיכרון, ולמה כדאי להימנע מהם.
לזכור אובייקטים רק פעם אחת
יכול להיות שיהיה מסוכן לזכור מחדש אובייקט. במקרה הטוב, יכול להיות שתבזבזו משאבים כדי לזכור ערך שכבר נזכר. אבל אם אובייקט מטמיע את RememberObserver ונשמר פעמיים באופן לא צפוי, הוא יקבל יותר קריאות חוזרות ממה שהוא מצפה. הדבר עלול לגרום לבעיות, כי הלוגיקה של onRemembered ושל onForgotten תופעל פעמיים, ורוב ההטמעות של RememberObserver לא תומכות במקרה הזה. אם מתבצעת קריאה שנייה של remember בהיקף אחר עם משך חיים שונה מזה של remember המקורי, הרבה הטמעות של RememberObserver.onForgotten משחררות את האובייקט לפני שהשימוש בו מסתיים.
val first: RememberObserver = rememberFoo()
// Not Recommended: Re-remembered `Foo` now gets double callbacks
val second = remember { first }
ההמלצה הזו לא חלה על אובייקטים שנשמרים שוב באופן טרנזיטיבי (כלומר, אובייקטים שנשמרו שצורכים אובייקט אחר שנשמר). מקובל לכתוב קוד שנראה כך, וזה מותר כי נשמר אובייקט שונה ולכן לא מתרחש כפילות לא צפויה של קריאה חוזרת (callback).
val foo: Foo = rememberFoo() // Acceptable: val bar: Bar = remember { Bar(foo) } // Recommended key usage: val barWithKey: Bar = remember(foo) { Bar(foo) }
הנחה שארגומנטים של פונקציות כבר נשמרו
פונקציה לא צריכה לזכור אף אחד מהפרמטרים שלה, כי זה עלול לגרום להפעלות כפולות של קריאה חוזרת עבור RememberObserver, וגם כי זה לא נחוץ. אם צריך לזכור פרמטר קלט, צריך לוודא שהוא לא מטמיע את RememberObserver, או לדרוש מהמתקשרים לזכור את הארגומנט שלהם.
@Composable
fun MyComposable(
parameter: Foo
) {
// Not Recommended: Input should be remembered by the caller.
val rememberedParameter = remember { parameter }
}
הכלל הזה לא חל על אובייקטים שזוכרים באופן טרנזיטיבי. כשרוצים לזכור אובייקט שנגזר מהארגומנטים של פונקציה, כדאי לציין אותו כאחד מהמפתחות של remember:
@Composable fun MyComposable( parameter: Foo ) { // Acceptable: val derivedValue = remember { Bar(parameter) } // Also Acceptable: val derivedValueWithKey = remember(parameter) { Bar(parameter) } }
לא לשמור אובייקט שכבר נשמר בזיכרון
בדומה לזיכרון מחדש של אובייקט, צריך להימנע משמירה של אובייקט שנשמר כדי לנסות להאריך את משך החיים שלו. ההמלצה הזו היא תוצאה של ההמלצה בנושא משך החיים של מצבים: אסור להשתמש ב-retain עם אובייקטים שמשך החיים שלהם לא תואם למשך החיים של שמירת מבצעים. מכיוון של-remembered אובייקטים יש משך חיים קצר יותר מאשר ל-retained אובייקטים, לא כדאי לשמור אובייקט שנזכר. במקום זאת, עדיף לשמור את האובייקט באתר המקורי במקום לזכור אותו.