יציבות בכתיבה

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

‫Compose משתמש ביציבות של הפרמטרים של פונקציה קומפוזיבילית כדי לקבוע איך להשוות בין קלט ולהחליט אם אפשר לדלג על הפונקציה הקומפוזיבילית במהלך קומפוזיציה מחדש (עם מצב דילוג חזק שמופעל כברירת מחדל החל מ-Kotlin 2.0.20):

  • פרמטרים יציבים:‏ Compose משווה פרמטרים יציבים באמצעות שוויון מבני (Object.equals()). אם הפרמטרים היציבים של רכיב שאפשר להרכיב שווים לערכים הקודמים שלהם, המערכת מדלגת עליו.
  • פרמטרים לא יציבים: אם מפעילים את מצב דילוג חזק (ברירת המחדל ב-Kotlin 2.0.20 ומעלה), ‏ Compose משווה פרמטרים לא יציבים באמצעות שוויון מופעים (===) ומדלג על הפונקציה הניתנת להרכבה אם מועברים אותם מופעי אובייקט. בגרסאות קודמות ל-Kotlin 2.0.20 (או כשמצב דילוג חזק מושבת), Compose תמיד מבצע רה-קומפוזיציה של פונקציה קומפוזבילית עם פרמטרים לא יציבים, כשמתבצעת רה-קומפוזיציה של הפונקציה הראשית שלה.

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

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

אובייקטים שלא ניתן לשנות

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

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

data class Contact(val name: String, val number: String)

ל-ContactRow composable יש פרמטר מסוג Contact.

@Composable
fun ContactRow(contact: Contact, modifier: Modifier = Modifier) {
   var selected by remember { mutableStateOf(false) }

   Row(modifier) {
      ContactDetails(contact)
      ToggleButton(selected, onToggled = { selected = !selected })
   }
}

כדאי לחשוב מה קורה כשמשתמש לוחץ על לחצן ההפעלה והמצב של selected משתנה:

  1. התכונה 'כתיבת קוד' בודקת אם צריך ליצור מחדש את הקוד בתוך ContactRow.
  2. המערכת רואה שהארגומנט היחיד של ContactDetails הוא מסוג Contact.
  3. מכיוון ש-Contact הוא מחלקת נתונים שלא ניתן לשנות, Compose בטוח שאף אחד מהארגומנטים של ContactDetails לא השתנה.
  4. לכן, התכונה 'יצירה' מדלגת על ContactDetails ולא יוצרת אותו מחדש.
  5. לעומת זאת, הארגומנטים של ToggleButton השתנו, ולכן Compose מבצע קומפוזיציה מחדש של הרכיב הזה.

אובייקטים שניתנים לשינוי

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

data class Contact(var name: String, var number: String)

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

‫Compose מחשיב מחלקה כזו כלא יציבה. במצב דילוג חזק (Kotlin 2.0.20 ואילך), ‏ Compose משווה את Contact באמצעות שוויון מופעים (===): שינוי של contact.name במקום לא יפעיל קומפוזיציה מחדש, אבל העברה של מופע Contact שהוקצה לו זיכרון חדש עם ערכים זהים עדיין תגרום ל-ContactDetails לבצע קומפוזיציה מחדש (ובלי מצב דילוג חזק, ContactDetails מבצע קומפוזיציה מחדש בכל פעם ש-selected משתנה).

הטמעה ב-Compose

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

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

פונקציות

‫Compose יכול לסמן פונקציות כ-skippable או כ-restartable. שימו לב: יכול להיות שהמערכת תסמן פונקציה כאחת מהאפשרויות הבאות, כשתיהן או אף אחת מהן:

  • ניתן לדילוג: אם הקומפיילר מסמן פונקציה composable כניתנת לדילוג, Compose יכול לדלג עליה במהלך ההרכבה מחדש אם כל הארגומנטים שלה שווים לערכים הקודמים שלהם. (ב-Kotlin 2.0.20 ואילך, עם מצב דילוג חזק, כל הפונקציות הניתנות להפעלה מחדש ניתנות לדילוג באופן אוטומטי).
  • ‫Restartable: רכיב קומפוזבילי שאפשר להפעיל מחדש משמש כ'היקף' שבו אפשר להתחיל קומפוזיציה מחדש. במילים אחרות, הפונקציה יכולה להיות נקודת כניסה שבה Compose יכול להתחיל להריץ מחדש קוד לצורך קומפוזיציה מחדש אחרי שינויים במצב.

סוגים

‫Compose מסמן סוגים כבלתי ניתנים לשינוי או כיציבים. כל סוג הוא אחד מהשניים:

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

ניפוי באגים של יציבות

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

מידע מפורט על אבחון בעיות מורכבות ביציבות ב-Compose זמין במדריך ניפוי באגים ביציבות.

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

מידע על שיפור היציבות של הטמעת Compose זמין במדריך פתרון בעיות שקשורות ליציבות.

סיכום

באופן כללי, חשוב לשים לב לנקודות הבאות:

  • פרמטרים: הפונקציה Compose קובעת את היציבות של כל פרמטר של הרכיבים הניתנים להרכבה כדי להחליט אם להשוות אותם באמצעות שוויון מבני (.equals()) או שוויון מופעים (===) במהלך ההרכבה מחדש.
  • תיקונים מיידיים: אם אתם מבחינים שהפונקציה הקומפוזבילית שלכם לא נדלגת ושהיא גורמת לבעיה בביצועים, בדקו אם מופעים חדשים של פרמטרים לא יציבים נוצרים מחדש בכל מעבר או אם נעשה שימוש במאפייני var במקום ב-State.
  • דוחות של קומפיילר: אפשר להשתמש בדוחות של קומפיילר כדי להבין מהי רמת היציבות של הכיתות.
  • אוספים: ממשקי אוספים רגילים (List,‏ Set ו-Map) נחשבים ב-Compose ללא יציבים, כי יכול להיות שההטמעות הבסיסיות שלהם ניתנות לשינוי. במצב דילוג חזק (מופעל כברירת מחדל ב-Kotlin 2.0.20 ואילך), אוספים לא יציבים עדיין מדלגים על קומפוזיציה מחדש באמצעות השוואה מהירה של O(1)מופעים (===) כשאין שינוי בהפניה לאוסף. מומלץ להימנע מהמרה של אוספים גדולים ל-Kotlinx immutable collections או מהוספת הערות (annotation) למחלקות שמכילות אוספים באמצעות @Immutable או @Stable, אלא אם נדרשת במפורש שוויון מבני (O(N) .equals()), כי השוואה של כל רכיב בכל קומפוזיציה מחדש יכולה להיות יקרה יותר מהקומפוזיציה מחדש עצמה.
  • מודולים אחרים: ‏ Compose מחשיב מחלקות ממודולים שבהם קומפיילר Compose לא פועל כלא יציבות (בהשוואה לשימוש ב-=== במצב דילוג חזק). אם מקור נתונים יוצר מחדש לעיתים קרובות מודלים קטנים ושטוחים ממודולים שאינם מודולי Compose עם ערכים זהים, אפשר להגדיר קובץ תצורה של יציבות או להשתמש ב-@Stable או ב-@Immutable במקרים שבהם .equals() זול.

קריאה נוספת