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