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

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

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

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

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

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

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

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

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

נניח שיש לנו פונקציית Composable שכוללת פרמטר מסוג List<Snack>:

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

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

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

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

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

קובץ תצורה

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

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

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