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