פתרון של בעיות ביציבות

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

הפעלת דילוג חזק

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

מידע נוסף זמין במאמר בנושא דילוגים חזקים.

הפיכת הכיתה לבלתי ניתנת לשינוי

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

  • בלתי משתנה: מציין סוג שבו הערך של כל המאפיינים לא יכול להשתנות אחרי שנוצר מופע של הסוג הזה, וכל השיטות שקופות מבחינת הפניה.
    • מוודאים שכל המאפיינים של המחלקה הם גם val ולא var, וגם מסוגים שלא ניתן לשנות.
    • סוגים פרימיטיביים כמו String, Int ו-Float הם תמיד קבועים.
    • אם זה לא אפשרי, צריך להשתמש במצב Compose לכל מאפיין שניתן לשינוי.
  • יציב: מציין סוג שניתן לשינוי. זמן הריצה של Compose לא יודע אם ומתי מאפיינים ציבוריים או התנהגות של שיטה מהסוג הזה יניבו תוצאות שונות מהפעלה קודמת.

אוספים שלא ניתן לשנות

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

כדי לפתור את הבעיה, אפשר להשתמש באוספים שלא ניתן לשנות. הקומפיילר של Compose כולל תמיכה ב-Kotlinx Immutable Collections. הקולקציות האלה לא ניתנות לשינוי, והקומפיילר של Compose מתייחס אליהן כאל קולקציות כאלה. הספרייה הזו עדיין בשלב אלפא, לכן צפויים שינויים בממשק ה-API שלה.

כדאי לעיין שוב בכיתה הלא יציבה הזו מתוך המדריך אבחון בעיות יציבות:

unstable class Snack {
  
  unstable val tags: Set<String>
  
}

אפשר להפוך את tags ליציב באמצעות אוסף שלא ניתן לשינוי. בכיתה, משנים את הסוג של tags לImmutableSet<String>:

data class Snack{
    
    val tags: ImmutableSet<String> = persistentSetOf()
    
}

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

הוספת הערות באמצעות Stable או Immutable

אחת הדרכים לפתור בעיות יציבות היא להוסיף הערות לכיתות לא יציבות עם @Stable או @Immutable.

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

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

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

@Immutable
data class Snack(

)

בין אם משתמשים באנוטציה @Immutable או באנוטציה @Stable, קומפיילר Compose מסמן את המחלקה Snack כמחלקה יציבה.

כיתות עם הערות באוספים

כדאי לשקול שימוש בקומפוזבל שכולל פרמטר מסוג List<Snack>:

restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  
  unstable snacks: List<Snack>
  
)

גם אם מוסיפים את ההערה @Immutable לפרמטר Snack, הקומפיילר של Compose עדיין מסמן את הפרמטר snacks בפונקציה HighlightedSnacks כלא יציב.

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

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

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

קובץ תצורה

אם אתם רוצים להשתמש ב-Kotlin collections כספריות יציבות, אתם יכולים להוסיף את kotlin.collections.* לקובץ הגדרות היציבות.

אוסף שלא ניתן לשינוי

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

@Composable
private fun HighlightedSnacks(
    
    snacks: ImmutableList<Snack>,
    
)

Wrapper

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

@Immutable
data class SnackCollection(
   val snacks: List<Snack>
)

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

@Composable
private fun HighlightedSnacks(
    index: Int,
    snacks: SnackCollection,
    onSnackClick: (Long) -> Unit,
    modifier: Modifier = Modifier
)

הפתרון

אחרי שמיישמים את אחת מהגישות האלה, הקומפיילר של Compose מסמן את הפונקציה HighlightedSnacks Composable כ-skippable וגם כ-restartable.

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  stable index: Int
  stable snacks: ImmutableList<Snack>
  stable onSnackClick: Function1<Long, Unit>
  stable modifier: Modifier? = @static Companion
)

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

קובץ תצורה של יציבות

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

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

הגדרה לדוגמה:

// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider my datalayer stable
com.datalayer.*
// Consider my datalayer and all submodules stable
com.datalayer.**
// Consider my generic type stable based off it's first type parameter only
com.example.GenericClass<*,_>

כדי להפעיל את התכונה הזו, מעבירים את הנתיב של קובץ התצורה לבלוק האפשרויות composeCompiler של ההגדרה Compose compiler Gradle plugin.

composeCompiler {
  stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
}

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

מודולים מרובים

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

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

הפתרון

כדי לפתור את הבעיה הזו, אפשר לנסות אחת מהגישות הבאות:

  1. מוסיפים את המחלקות לקובץ התצורה של הקומפיילר.
  2. מפעילים את מהדר ה-Compose במודולים של שכבת הנתונים, או מתייגים את המחלקות באמצעות @Stable או @Immutable במקומות המתאימים.
    • הפעולה הזו כוללת הוספה של תלות ב-Compose לשכבת הנתונים. עם זאת, היא רק התלות בזמן הריצה של Compose ולא ב-Compose-UI.
  3. במודול ממשק המשתמש, עוטפים את המחלקות של שכבת הנתונים במחלקות עוטפות שספציפיות לממשק המשתמש.

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

לא כל רכיב שאפשר להרכיב צריך להיות ניתן לדילוג

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

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

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

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