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

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

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

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

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

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

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

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

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

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

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

תרשים שמראה את הסידור ההיררכי של הרכיבים בקטע הקוד הקודם

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

המבנה של פונקציה שאפשר להרכיב ב-Compose

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

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

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

דוגמה:

@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 תפעיל את הפונקציה LoginError בהתאם לתנאי, ותמיד תפעיל את הפונקציה LoginInput. לכל קריאה יש מיקום ייחודי באתר הקריאה ובמקור, והקומפיילר ישתמש במיקום הזה כדי לזהות אותה באופן ייחודי.

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

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

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

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

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

@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, כי המיקום שלהם ברשימה לא השתנה, ולכן הקלט של 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 שמועברת אליו. אם נסדר מחדש את רשימת הסרטים, רצוי שנסדר מחדש באופן דומה את המופעים בעץ ה-Composition במקום להרכיב מחדש כל רכיב MovieOverview עם מופע שונה של סרט. ‫Compose מאפשרת לכם לציין בזמן הריצה אילו ערכים ישמשו לזיהוי חלק מסוים בעץ: ה-composable‏ key.

אם עוטפים בלוק קוד בקריאה לפונקציה הניתנת להרכבה של המפתח עם ערך אחד או יותר שמועברים, הערכים האלה ישולבו כדי לזהות את המופע הזה בקומפוזיציה. הערך של key לא צריך להיות ייחודי באופן גלובלי, אלא רק ייחודי בין הקריאות של פונקציות Composable באתר הקריאה. לכן, בדוגמה הזו, לכל 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)
            }
        }
    }
}

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

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

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

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

@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 וכו'.
  • מיתרים
  • כל סוגי הפונקציות (פונקציות למבדה)

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

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

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

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