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

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

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

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

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

איך הופכים את הכיתה לבלתי ניתנת לשינוי

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

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

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

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

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

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

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

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

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

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

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

דרך אפשרית לפתרון בעיות יציבות היא להוסיף הערות לכיתות לא יציבות באמצעות @Stable או @Immutable.

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

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

קטע הקוד הבא הוא דוגמה מינימלית לסיווג נתונים שיצוין בו שהוא לא ניתן לשינוי:

@Immutable
data class Snack(
…
)

בין שאתם משתמשים בהערה @Immutable ובין שאתם משתמשים בהערה @Stable, המהדר של Compose מסמנים את הכיתה Snack כיציבה.

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

נניח שרוצים ליצור רכיב מורכב שכולל פרמטר מסוג List<Snack>:

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

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

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

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

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

קובץ התצורה

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

אוסף בלתי משתנה

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

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

Wrapper

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

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

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

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

הפתרון

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

קובץ תצורת יציבות

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

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

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

מספר מודולים

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

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

הפתרון

כדי לפתור את הבעיה, אפשר להשתמש באחת מהדרכים הבאות:

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

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

לא כל תוכן קומפוזבילי הוא תוכן שניתן לדלג עליו

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

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

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

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