מחזור החיים של תכנים קומפוזביליים

בדף הזה נסביר על מחזור החיים של קומפוזיט ונסביר איך Compose מחליט אם צריך לבצע קומפוזיציה מחדש של קומפוזיט.

סקירה כללית על מחזור החיים

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

כש-Jetpack Compose מפעיל את ה-composables בפעם הראשונה, במהלך הרכבת הקוד הראשונית, הוא עוקב אחרי ה-composables שאתם קוראים להם כדי לתאר את ממשק המשתמש ב-Composition. לאחר מכן, כשמצב האפליקציה ישתנה, Jetpack Compose יתזמן יצירה מחדש. 'הרכבה מחדש' היא תהליך שבו מערכת Jetpack Compose מפעילה מחדש את הרכיבים הניתנים ליצירה (composables) שעשויים להשתנות בתגובה לשינויים במצב, ולאחר מכן מעדכנת את ההרכבה כך שתשקף את השינויים.

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

תרשים שבו מוצג מחזור החיים של רכיב מורכב

איור 1. מחזור החיים של רכיב ה-Composable ב-Composition. הוא נכנס ליצירה, עובר עיבוד מחדש אפס פעמים או יותר ויוצא מהיצירה.

בדרך כלל, יצירת קומפוזיציה מחדש מופעלת בעקבות שינוי באובייקט State<T>. Compose עוקב אחרי האירועים האלה ומריץ את כל הרכיבים הניתנים לקישור ב-Composition שקוראים את State<T> הספציפי הזה, ואת כל הרכיבים הניתנים לקישור שהם קוראים אליהם ולא ניתן לדלג עליהם.

אם קוראים ל-composable כמה פעמים, המערכת תוסיף כמה מכונות ל-Composition. לכל קריאה יש מחזור חיים משלה ב-Composition.

@Composable
fun MyComposable() {
    Column {
        Text("Hello")
        Text("World")
    }
}

תרשים שבו מוצגת ההיררכיה של הרכיבים בקטע הקוד הקודם

איור 2. הייצוג של MyComposable בקומפוזיציה. אם קוראים ל-composable כמה פעמים, המערכת תוסיף כמה מכונות ל-Composition. אם לאלמנט יש צבע שונה, סימן שהוא מכיל מופע נפרד.

המבנה של רכיב שאפשר לשלב ב-Composition

המופע של רכיב ה-Composable ב-Composition מזוהה לפי אתר הקריאה שלו. המהדר של Compose מתייחס לכל אתר קריאה כאתר נפרד. קריאה לרכיבים מורכבים ממספר מוקדי קריאה תיצור כמה מופעים של הרכיב המורכב ב-Composition.

אם במהלך יצירת קומפוזיציה מחדש, רכיב מורכב קורא לרכיבים מורכבים שונים מאלה שקרא להם במהלך היצירה הקודמת, Compose יזהה אילו רכיבים מורכבים הוזמנו או לא הוזמנו. לגבי הרכיבים המורכבים שהוזמנו בשתי הקומפוזיציות, Compose ימנע יצירת קומפוזיציה מחדש שלהם אם הקלט שלהם לא השתנה.

חשוב לשמור על הזהות כדי לשייך את תופעות הלוואי לרכיב ה-composable שלהן, כדי שהן יוכלו להסתיים בהצלחה במקום להתחיל מחדש בכל פעם שמבצעים יצירת מחדש.

דוגמה:

@Composable
fun LoginScreen(showError: Boolean) {
    if (showError) {
        LoginError()
    }
    LoginInput() // This call site affects where LoginInput is placed in Composition
}

@Composable
fun LoginInput() { /* ... */ }

@Composable
fun LoginError() { /* ... */ }

בקטע הקוד שלמעלה, LoginScreen יקרא ל-composable של LoginError באופן מותנה, ותמיד יקרא ל-composable של LoginInput. לכל קריאה יש מיקום מקור ומיקום קריאה ייחודיים, שבהם המהדר ישתמש כדי לזהות אותה באופן ייחודי.

תרשים שבו מוצג איך הקוד הקודם מורכב מחדש אם הדגל showError משתנה ל-true. הרכיב ה-composable LoginError מתווסף, אבל הרכיבים האחרים לא מורכבים מחדש.

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

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

הוספת מידע נוסף כדי לשפר את הרכבות החכמות מחדש

קריאה ל-composable מספר פעמים תוסיף אותו ל-Composition מספר פעמים גם כן. כשקוראים ל-composable כמה פעמים מאותו אתר קריאה, ל-Compose אין מידע שאפשר להשתמש בו כדי לזהות באופן ייחודי כל קריאה ל-composable הזה. לכן, כדי להבדיל בין המופעים, נעשה שימוש בסדר הביצוע בנוסף לאתר הקריאה. לפעמים זה כל מה שצריך, אבל במקרים מסוימים זה עלול לגרום להתנהגות לא רצויה.

@Composable
fun MoviesScreen(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            // MovieOverview composables are placed in Composition given its
            // index position in the for loop
            MovieOverview(movie)
        }
    }
}

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

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

איור 4. ייצוג של MoviesScreen בהרכבה כשמוסיפים רכיב חדש לתחתית הרשימה. אפשר לעשות שימוש חוזר ברכיבי MovieOverview ב-Composition. אם הצבע ב-MovieOverview זהה, המשמעות היא שהרכיב המודילרי לא עובר קומפוזיציה מחדש.

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

@Composable
fun MovieOverview(movie: Movie) {
    Column {
        // Side effect explained later in the docs. If MovieOverview
        // recomposes, while fetching the image is in progress,
        // it is cancelled and restarted.
        val image = loadNetworkImage(movie.url)
        MovieHeader(image)

        /* ... */
    }
}

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

איור 5. ייצוג של MoviesScreen בהרכבה כשמוסיפים רכיב חדש לרשימה. אי אפשר לעשות שימוש חוזר ב-composables של MovieOverview, וכל תופעות הלוואי יתחילו מחדש. צבע שונה ב-MovieOverview מציין שהרכיב המודולרי עוצב מחדש.

באופן אידיאלי, הזהות של מכונה MovieOverview מקושרת לזהות של movie שמועברת אליה. אם נשנה את הסדר של רשימת הסרטים, מומלץ לשנות את הסדר של המופעים בעץ הקומפוזיציה באופן דומה, במקום ליצור מחדש כל רכיב MovieOverview שאפשר ליצור ממנו קומפוזיציה עם מופע אחר של סרט. Compose מאפשר לכם לציין בסביבת זמן הריצה באילו ערכים אתם רוצים להשתמש כדי לזהות חלק נתון בעץ: ה-composable‏ key.

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

@Composable
fun MoviesScreenWithKey(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            key(movie.id) { // Unique ID for this movie
                MovieOverview(movie)
            }
        }
    }
}

בעזרת הקוד שלמעלה, גם אם הרכיבים ברשימה ישתנו, Compose יזהה קריאות נפרדות ל-MovieOverview ויוכל לעשות בהן שימוש חוזר.

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

איור 6. ייצוג של MoviesScreen בהרכבה כשמוסיפים רכיב חדש לרשימה. מכיוון שלרכיבי ה-Compose של MovieOverview יש מפתחות ייחודיים, Compose מזהה אילו מכונות MovieOverview לא השתנו ויכול לעשות בהן שימוש חוזר. ההשפעות הצדדיות שלהן ימשיכו לפעול.

לחלק מהרכיבים הניתנים לשילוב יש תמיכה מובנית ברכיב key הניתן לשילוב. לדוגמה, אפשר לציין key בהתאמה אישית ב-DSL של items.LazyColumn

@Composable
fun MoviesScreenLazy(movies: List<Movie>) {
    LazyColumn {
        items(movies, key = { movie -> movie.id }) { movie ->
            MovieOverview(movie)
        }
    }
}

דילוג אם הקלט לא השתנה

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

פונקציה הניתנת להגדרה עומדת בדרישות לאפשרות דילוג אלא אם:

  • לסוג ההחזרה של הפונקציה יש ערך שאינו Unit
  • הפונקציה מסומנת ב-@NonRestartableComposable או ב-@NonSkippableComposable
  • פרמטר חובה הוא מסוג לא יציב

יש מצב מעבד ניסיוני, Strong Skipping, שמקל על הדרישה האחרונה.

כדי שסוג ייחשב כיציב, הוא צריך לעמוד בהסכם הבא:

  • התוצאה של equals בשתי מכונות תהיה תמיד זהה לשתי המכונות.
  • אם נכס ציבורי מהסוג הזה ישתנה, תישלח הודעה ל-Composition.
  • גם כל סוגי הנכסים הציבוריים יציבים.

יש כמה סוגים נפוצים חשובים שנכללים בהסכם הזה, והמְהַדר של Compose יתייחס אליהם כאל יציבים, גם אם הם לא מסומנים במפורש כיציבים באמצעות ההערה @Stable:

  • כל סוגי הערכים הפרימיטיביים: Boolean,‏ Int,‏ Long,‏ Float,‏ Char וכו'.
  • מיתרים
  • כל סוגי הפונקציות (lambdas)

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

סוג אחד ראוי לציון שהוא יציב אבל ניתן לשינוי הוא הסוג MutableState של Compose. אם ערך נשמר ב-MutableState, אובייקט המצב נחשב יציב באופן כללי כי Compose יקבל הודעה על כל שינוי במאפיין .value של State.

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

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

אם Compose לא יכול להסיק שסוג מסוים יציב, אבל אתם רוצים לאלץ את Compose להתייחס אליו כאל יציב, תוכלו לסמן אותו באמצעות ההערה @Stable.

// Marking the type as stable to favor skipping and smart recompositions.
@Stable
interface UiState<T : Result<T>> {
    val value: T?
    val exception: Throwable?

    val hasError: Boolean
        get() = exception != null
}

בקטע הקוד שלמעלה, מכיוון ש-UiState הוא ממשק, Compose יכול בדרך כלל להתייחס לסוג הזה כאל סוג לא יציב. הוספת ההערה @Stable מאפשרת ל-Compose להעדיף קומפוזיציות מחדש חכמות, כי היא מסמנת שהסוג הזה יציב. המשמעות היא גם ש-Compose יתייחס לכל ההטמעות שלו כיציבות אם הממשק ישמש כסוג הפרמטר.