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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

דוגמה:

@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.

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

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

@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)
        }
    }
}

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

תרשים שמראה איך הקוד הקודם מורכב מחדש אם מוסיפים רכיב חדש לתחתית הרשימה. המיקום של הפריטים האחרים ברשימה לא משתנה, והם לא מסודרים מחדש.
איור 4. ייצוג של MoviesScreen בקומפוזיציה כשמוסיפים רכיב חדש לתחתית הרשימה. אפשר לעשות שימוש חוזר ברכיבים הקומפוזביליים MovieOverview בקומפוזיציה. אותו צבע ב-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 בקומפוזיציה כשאלמנט חדש נוסף לרשימה. אי אפשר לעשות שימוש חוזר ברכיב הקומפוזבילי של MovieOverview וכל תופעות הלוואי יופעלו מחדש. צבע אחר ב-MovieOverview מציין שהרכיב עבר קומפוזיציה מחדש.

הכי טוב לחשוב על הזהות של מופע MovieOverview כזהות שמקושרת לזהות של movie שמועברת אליו. אם נסדר מחדש את רשימת הסרטים, רצוי שנסדר מחדש באופן דומה את המופעים בעץ הקומפוזיציה במקום להרכיב מחדש כל רכיב MovieOverview עם מופע שונה של סרט. "פיתוח נייטיב" מאפשר לכם לציין בזמן הריצה אילו ערכים ישמשו לזיהוי חלק מסוים בעץ: הרכיב הקומפוזבילי 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)
            }
        }
    }
}

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

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

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

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

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

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

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

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

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