יכול להיות שתיתקלו בבעיות נפוצות בכתיבה. יכול להיות שהטעויות האלה יניבו קוד שנראה כאילו הוא פועל בצורה טובה, אבל הן עלולות לפגוע בביצועים של ממשק המשתמש. כדי לבצע אופטימיזציה של האפליקציה ב-Compose, מומלץ לפעול לפי השיטות המומלצות.
שימוש ב-remember כדי לצמצם חישובים יקרים
פונקציות שאפשר להרכיב מהן ממשקים יכולות לפעול בתדירות גבוהה מאוד, אפילו בכל פריים של אנימציה. לכן, מומלץ לבצע כמה שפחות חישובים בגוף של הפונקציה הניתנת להרכבה.
טכניקה חשובה היא שמירת תוצאות החישובים באמצעות remember. כך החישוב יפעל פעם אחת, ותוכלו לאחזר את התוצאות מתי שצריך.
לדוגמה, הנה קוד שמציג רשימה ממוינת של שמות, אבל המיון מתבצע בצורה יקרה מאוד:
@Composable fun ContactList( contacts: List<Contact>, comparator: Comparator<Contact>, modifier: Modifier = Modifier ) { LazyColumn(modifier) { // DON’T DO THIS items(contacts.sortedWith(comparator)) { contact -> // ... } } }
בכל פעם שמתבצעת קומפוזיציה מחדש של ContactsList, כל רשימת אנשי הקשר ממוינת מחדש, גם אם הרשימה לא השתנתה. אם המשתמש מגלגל את הרשימה,
הקומפוזיציה מורכבת מחדש בכל פעם שמופיעה שורה חדשה.
כדי לפתור את הבעיה, צריך למיין את הרשימה מחוץ ל-LazyColumn ולאחסן את הרשימה הממוינת באמצעות remember:
@Composable fun ContactList( contacts: List<Contact>, comparator: Comparator<Contact>, modifier: Modifier = Modifier ) { val sortedContacts = remember(contacts, comparator) { contacts.sortedWith(comparator) } LazyColumn(modifier) { items(sortedContacts) { // ... } } }
עכשיו, הרשימה ממוינת פעם אחת, כשיוצרים את ContactList בפעם הראשונה. אם אנשי הקשר או פונקציית ההשוואה משתנים, הרשימה הממוינת נוצרת מחדש. אחרת, אפשר להמשיך להשתמש ברשימה הממוינת שנשמרה במטמון.
שימוש במקשי פריסה עצלה
פריסות עצלות מאפשרות שימוש חוזר יעיל בפריטים, ויוצרות אותם מחדש רק כשצריך. עם זאת, אתם יכולים לעזור באופטימיזציה של פריסות עצלות לרה-קומפוזיציה.
נניח שפעולת משתמש גורמת להזזת פריט ברשימה. לדוגמה, נניח שאתם מציגים רשימה של פתקים שממוינים לפי זמן השינוי, והפתק ששוּנה לאחרונה מופיע בראש הרשימה.
@Composable fun NotesList(notes: List<Note>) { LazyColumn { items( items = notes ) { note -> NoteRow(note) } } }
אבל יש בעיה בקוד הזה. נניח שההערה התחתונה משתנה. היא עכשיו הרשומה האחרונה ששונתה, ולכן היא עוברת לראש הרשימה, וכל שאר הרשומות יורדות מקום אחד למטה.
בלי העזרה שלכם, כלי הכתיבה לא מבין שפריטים שלא השתנו פשוט מוזזים ברשימה. במקום זאת, התכונה 'יצירה' חושבת ש'פריט 2' הישן נמחק ונוצר פריט חדש בשביל פריט 3, פריט 4 וכל שאר הפריטים. התוצאה היא ש-Compose מרכיב מחדש כל פריט ברשימה, גם אם רק אחד מהם השתנה בפועל.
הפתרון הוא לספק מפתחות פריטים. אם מספקים מפתח יציב לכל פריט, אפשר למנוע ב-Compose פעולות מיותרות של יצירה מחדש. במקרה הזה, Compose יכול לקבוע שהפריט שנמצא עכשיו במיקום 3 הוא אותו פריט שהיה במיקום 2. מכיוון שאף אחד מהנתונים של הפריט לא השתנה, Compose לא צריך ליצור אותו מחדש.
@Composable fun NotesList(notes: List<Note>) { LazyColumn { items( items = notes, key = { note -> // Return a stable, unique key for the note note.id } ) { note -> NoteRow(note) } } }
שימוש ב-derivedStateOf כדי להגביל את ההרכבות מחדש
אחד הסיכונים בשימוש במצב בהרכבות הוא שאם המצב משתנה במהירות, יכול להיות שממשק המשתמש יורכב מחדש יותר פעמים ממה שצריך. לדוגמה, נניח שאתם מציגים רשימה עם אפשרות גלילה. בודקים את מצב הרשימה כדי לראות איזה פריט הוא הפריט הראשון שמוצג ברשימה:
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } val showButton = listState.firstVisibleItemIndex > 0 AnimatedVisibility(visible = showButton) { ScrollToTopButton() }
הבעיה כאן היא שאם המשתמש גולל את הרשימה, הערך של listState משתנה כל הזמן כשהמשתמש גורר את האצבע. כלומר, הרשימה מורכבת מחדש כל הזמן. עם זאת, לא צריך להרכיב אותו מחדש כל כך הרבה פעמים – רק כשפריט חדש מופיע בתחתית. לכן,
מדובר בהרבה חישובים נוספים, שגורמים לביצועים של ממשק המשתמש להיות גרועים.
הפתרון הוא להשתמש במצב נגזר. מצב נגזר מאפשר לכם לציין ל-Compose אילו שינויים בערך הדינמי צריכים להפעיל רה-קומפוזיציה. במקרה כזה, צריך לציין שחשוב לכם לדעת מתי הפריט הגלוי הראשון משתנה. כשערך המצב הזה משתנה, ממשק המשתמש צריך להרכיב מחדש את עצמו, אבל אם המשתמש עדיין לא גלל מספיק כדי להציג פריט חדש בחלק העליון, אין צורך בהרכבה מחדש.
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() }
דחיית קריאות כמה שיותר
אם זיהיתם בעיה בביצועים, יכול להיות שדחיית קריאות של מצב תעזור לכם. דחיית קריאות של ערך דינמי תבטיח ש-Compose יפעיל מחדש את הקוד המינימלי האפשרי במהלך רה-קומפוזיציה. לדוגמה, אם לממשק המשתמש יש מצב שמועבר למעלה בעץ של הקומפוזיציה ואתם קוראים את המצב בקומפוזיציה צאצאית, אתם יכולים לעטוף את קריאת המצב בפונקציית למדה. כך הקריאה מתבצעת רק כשבאמת צריך אותה. לעיון, אפשר לראות את ההטמעה באפליקציה לדוגמה Jetsnack. באפליקציה Jetsnack מוטמע אפקט של סרגל כלים מתקפל במסך הפרטים. כדי להבין למה הטכניקה הזו עובדת, אפשר לעיין בפוסט בבלוג בנושא Jetpack Compose: Debugging Recomposition.
כדי להשיג את האפקט הזה, לרכיב ה-Composable Title צריך להיות ערך של היסט הגלילה כדי להגדיר היסט בעצמו באמצעות Modifier. זו גרסה פשוטה של קוד Jetsnack לפני האופטימיזציה:
@Composable fun SnackDetail() { // ... Box(Modifier.fillMaxSize()) { // Recomposition Scope Start val scroll = rememberScrollState(0) // ... Title(snack, scroll.value) // ... } // Recomposition Scope End } @Composable private fun Title(snack: Snack, scroll: Int) { // ... val offset = with(LocalDensity.current) { scroll.toDp() } Column( modifier = Modifier .offset(y = offset) ) { // ... } }
כשמצב הגלילה משתנה, Compose מבטל את התוקף של היקף הרה-קומפוזיציה של ההורה הקרוב ביותר. במקרה הזה, ההיקף הקרוב ביותר הוא SnackDetail. הערה: Box היא פונקציה מוטבעת, ולכן היא לא תחום של קומפוזיציה מחדש. לכן, הפונקציה Compose מרכיבה מחדש את SnackDetail ואת כל הפונקציות הניתנות להרכבה בתוך SnackDetail. אם תשנו את הקוד כך שהוא יקרא רק את המצב שבו אתם משתמשים בו בפועל, תוכלו להקטין את מספר הרכיבים שצריך להרכיב מחדש.
@Composable fun SnackDetail() { // ... Box(Modifier.fillMaxSize()) { // Recomposition Scope Start val scroll = rememberScrollState(0) // ... Title(snack) { scroll.value } // ... } // Recomposition Scope End } @Composable private fun Title(snack: Snack, scrollProvider: () -> Int) { // ... val offset = with(LocalDensity.current) { scrollProvider().toDp() } Column( modifier = Modifier .offset(y = offset) ) { // ... } }
פרמטר הגלילה הוא עכשיו lambda. כלומר, Title עדיין יכול להתייחס למצב המועבר, אבל הערך נקרא רק בתוך Title, במקום שבו הוא נחוץ בפועל. כתוצאה מכך, כשערך הגלילה משתנה, היקף הרה-קומפוזיציה הקרוב ביותר הוא עכשיו Title composable – Compose כבר לא צריך לבצע רה-קומפוזיציה של כל Box.
זה שיפור טוב, אבל אפשר להשיג תוצאות טובות יותר. כדאי לחשוד אם אתם גורמים לרה-קומפוזיציה רק כדי לשנות את הפריסה או לצייר מחדש רכיב Composable. במקרה הזה, כל מה שעושים הוא לשנות את ההיסט של רכיב Title composable, שאפשר לעשות את זה בשלב הפריסה.
@Composable private fun Title(snack: Snack, scrollProvider: () -> Int) { // ... Column( modifier = Modifier .offset { IntOffset(x = 0, y = scrollProvider()) } ) { // ... } }
בעבר, הקוד השתמש ב-Modifier.offset(x: Dp, y: Dp), שמקבל את ההיסט כפרמטר. אם עוברים לגרסת ה-lambda של ה-modifier, אפשר לוודא שהפונקציה קוראת את מצב הגלילה בשלב הפריסה. כתוצאה מכך, כשמצב הגלילה משתנה, Compose יכול לדלג על שלב הקומפוזיציה לחלוטין ולעבור ישירות לשלב הפריסה. כשמעבירים משתני מצב שמשתנים לעיתים קרובות לשינויים, כדאי להשתמש בגרסאות ה-lambda של השינויים, אם אפשר.
הנה עוד דוגמה לגישה הזו. הקוד הזה עדיין לא עבר אופטימיזציה:
// Here, assume animateColorBetween() is a function that swaps between // two colors val color by animateColorBetween(Color.Cyan, Color.Magenta) Box( Modifier .fillMaxSize() .background(color) )
כאן, צבע הרקע של התיבה משתנה במהירות בין שני צבעים. לכן, המצב הזה משתנה לעיתים קרובות מאוד. אחר כך הרכיב הקומפוזבילי קורא את המצב הזה במגדיר הרקע. כתוצאה מכך, התיבה צריכה להרכיב את עצמה מחדש בכל פריים, כי הצבע משתנה בכל פריים.
כדי לשפר את הביצועים, אפשר להשתמש במחרוזת לשינוי שמבוססת על lambda – במקרה הזה, drawBehind.
כלומר, מצב הצבע נקרא רק במהלך שלב הציור. כתוצאה מכך, Compose יכול לדלג על שלבי הקומפוזיציה והפריסה לחלוטין – כשהצבע משתנה, Compose עובר ישירות לשלב הציור.
val color by animateColorBetween(Color.Cyan, Color.Magenta) Box( Modifier .fillMaxSize() .drawBehind { drawRect(color) } )
איך להימנע מכתיבה לאחור
ההנחה הבסיסית של התכונה 'כתיבת הודעה' היא שלעולם לא תכתבו למצב שכבר נקרא. הפעולה הזו נקראת כתיבה לאחור, והיא עלולה לגרום לרה-קומפוזיציה בכל פריים, ללא סוף.
בדוגמה הבאה של קומפוזבל אפשר לראות טעות כזו.
@Composable fun BadComposable() { var count by remember { mutableIntStateOf(0) } // Causes recomposition on click Button(onClick = { count++ }, Modifier.wrapContentSize()) { Text("Recompose") } Text("$count") count++ // Backwards write, writing to state after it has been read</b> }
הקוד הזה מעדכן את הספירה בסוף הקומפוזיציה אחרי הקריאה שלה בשורה הקודמת. אם מריצים את הקוד הזה, אפשר לראות שאחרי שלוחצים על הלחצן, שגורם להרכבה מחדש, הדלפק עולה במהירות בלולאה אינסופית, כי Compose מרכיב מחדש את הרכיב הזה, רואה קריאת מצב שהיא לא עדכנית, ולכן מתזמן הרכבה מחדש נוספת.
כדי להימנע לחלוטין מכתיבה לאחור, אל תכתבו אף פעם למצב ב-Composition. אם אפשר, תמיד כדאי לכתוב למצב בתגובה לאירוע ובפונקציית lambda כמו בדוגמה onClick הקודמת.
לקבלת מדריך מפורט על כתיבה לאחור, לולאות של רה-קומפוזיציה ותיאום של שלבים, אפשר לעיין במאמר כתיבה לאחור ב-Compose.
מקורות מידע נוספים
- מדריך לביצועי אפליקציות: כאן תמצאו שיטות מומלצות, ספריות וכלים לשיפור הביצועים ב-Android.
- בדיקת הביצועים: בדיקת ביצועי האפליקציה.
- השוואה לשוק: השוואה של ביצועי האפליקציה לשוק.
- הפעלת האפליקציה: אופטימיזציה של הפעלת האפליקציה.
- פרופילים של Baseline: הסבר על פרופילים של Baseline.
מומלץ בשבילכם
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- מצב ו-Jetpack פיתוח נייטיב
- משני גרפיקה
- חשיבה ב-Compose