באפליקציות רבות צריך להציג אוספים של פריטים. במאמר הזה נסביר איך לעשות את זה ביעילות ב-Jetpack Compose.
אם אתם יודעים שתרחיש השימוש שלכם לא דורש גלילה, תוכלו להשתמש ב-Column או ב-Row פשוטים (בהתאם לכיוון), ולשדר את התוכן של כל פריט על ידי איטרציה ברשימה באופן הבא:
@Composable fun MessageList(messages: List<Message>) { Column { messages.forEach { message -> MessageRow(message) } } }
אפשר להוסיף גלילה ל-Column באמצעות שינוי המאפיין verticalScroll().
רשימות עצלניות
אם אתם צריכים להציג מספר גדול של פריטים (או רשימה באורך לא ידוע),
שימוש בפריסה כמו Column עלול לגרום לבעיות בביצועים, כי כל הפריטים יורכבו ויוצבו בפריסה בין אם הם גלויים ובין אם לא.
Compose מספקת קבוצה של רכיבים שמרכיבים ומסדרים רק פריטים שגלויים באזור התצוגה של הרכיב. הרכיבים האלה כוללים את
LazyColumn
ואת
LazyRow.
כפי שהשם מרמז, ההבדל בין LazyColumn לבין LazyRow הוא הכיוון שבו הפריטים מוצגים והגלילה מתבצעת. LazyColumn
יוצר רשימה עם גלילה אנכית, ו-LazyRow יוצר רשימה עם גלילה אופקית.
הרכיבים Lazy שונים מרוב הפריסות ב-Compose. במקום לקבל פרמטר של @Composable חסימת תוכן, שמאפשר לאפליקציות להפיק ישירות רכיבים שניתנים להרכבה, הרכיבים Lazy מספקים חסימה של LazyListScope.(). בלוק LazyListScope הזה מוצעת שפת תיאור תחום (DSL) שמאפשרת לאפליקציות לתאר את תוכן הפריט. לאחר מכן, רכיב Lazy אחראי להוספת התוכן של כל פריט בהתאם לפריסה ולמיקום הגלילה.
LazyListScope DSL
שפת התחום הספציפית (DSL) של LazyListScope מספקת מספר פונקציות לתיאור פריטים בפריסה. בצורה הכי בסיסית,
item()
מוסיף פריט אחד, ו-
items(Int)
מוסיף כמה פריטים:
LazyColumn { // Add a single item item { Text(text = "First item") } // Add 5 items items(5) { index -> Text(text = "Item: $index") } // Add another single item item { Text(text = "Last item") } }
יש גם מספר פונקציות של תוספים שמאפשרות להוסיף אוספים של פריטים, כמו List. התוספים האלה מאפשרים לנו להעביר בקלות את הדוגמה של Column שצוינה למעלה:
/** * import androidx.compose.foundation.lazy.items */ LazyColumn { items(messages) { message -> MessageRow(message) } }
יש גם גרסה של פונקציית התוסף items() שנקראת itemsIndexed(), שמספקת את האינדקס. לפרטים נוספים, אפשר לעיין במאמר LazyListScope.
רשתות מדורגות
רכיבי ה-Composable LazyVerticalGrid ו-LazyHorizontalGrid מספקים תמיכה בהצגת פריטים ברשת. רשת אנכית עצלה תציג את הפריטים שלה במאגר שניתן לגלול בו אנכית, והפריטים יתפרסו על פני כמה עמודות. רשתות אופקיות עצלות יתנהגו באופן דומה בציר האופקי.
לרשתות יש את אותן יכולות API עוצמתיות כמו לרשימות, והן גם משתמשות ב-DSL דומה מאוד – LazyGridScope.() – כדי לתאר את התוכן.
הפרמטר columns ב-LazyVerticalGrid והפרמטר rows ב-LazyHorizontalGrid קובעים איך התאים מסודרים בעמודות או בשורות. בדוגמה הבאה מוצגים פריטים בפריסה של רשת, באמצעות GridCells.Adaptive כדי להגדיר שכל עמודה תהיה ברוחב של 128.dp לפחות:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 128.dp) ) { items(photos) { photo -> PhotoItem(photo) } }
LazyVerticalGrid מאפשרת לציין רוחב לפריטים, ואז הרשת תתאים כמה שיותר עמודות. כל הרוחב שנותר יחולק באופן שווה בין העמודות, אחרי שמספר העמודות יחושב. שיטת התאמה הזו שימושית במיוחד להצגת קבוצות של פריטים בגדלים שונים של מסכים.
אם אתם יודעים את המספר המדויק של העמודות שבהן אתם רוצים להשתמש, אתם יכולים במקום זאת לספק מופע של GridCells.Fixed שמכיל את מספר העמודות הנדרש.
אם העיצוב שלכם מחייב שרק לפריטים מסוימים יהיו מידות לא סטנדרטיות,
אתם יכולים להשתמש בתמיכה בפריסת רשת כדי לספק טווחים מותאמים אישית של עמודות לפריטים.
כדי לציין את טווח העמודות, משתמשים בפרמטר span של השיטות item ו-items של LazyGridScope DSL.
maxLineSpan, אחד מהערכים של היקף טווח העמודות, שימושי במיוחד כשמשתמשים בשינוי גודל דינמי, כי מספר העמודות לא קבוע.
בדוגמה הזו מוצג איך לספק טווח של שורה מלאה:
LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 30.dp) ) { item(span = { // LazyGridItemSpanScope: // maxLineSpan GridItemSpan(maxLineSpan) }) { CategoryCard("Fruits") } // ... }
רשת מדורגת עצלה
LazyVerticalStaggeredGrid
ו-LazyHorizontalStaggeredGrid
הם רכיבים שאפשר להשתמש בהם כדי ליצור רשת של פריטים שנטענים בהדרגה, עם פריסה מדורגת.
רשת אנכית מדורגת שנטענת בהדרגה מציגה את הפריטים שלה במאגר שאפשר לגלול בו אנכית, שמשתרע על פני כמה עמודות ומאפשר לפריטים שונים להיות בגבהים שונים. לרשתות אופקיות שנטענות בהדרגה יש את אותו אופן פעולה בציר האופקי, עם פריטים ברוחבים שונים.
קטע הקוד הבא הוא דוגמה בסיסית לשימוש ב-LazyVerticalStaggeredGrid עם רוחב של 200.dp לכל פריט:
LazyVerticalStaggeredGrid( columns = StaggeredGridCells.Adaptive(200.dp), verticalItemSpacing = 4.dp, horizontalArrangement = Arrangement.spacedBy(4.dp), content = { items(randomSizedPhotos) { photo -> AsyncImage( model = photo, contentScale = ContentScale.Crop, contentDescription = null, modifier = Modifier .fillMaxWidth() .wrapContentHeight() ) } }, modifier = Modifier.fillMaxSize() )
כדי להגדיר מספר קבוע של עמודות, אפשר להשתמש ב-StaggeredGridCells.Fixed(columns) במקום ב-StaggeredGridCells.Adaptive.
הרוחב הזמין מחולק במספר העמודות (או השורות ברשת אופקית), וכל פריט תופס את הרוחב הזה (או הגובה ברשת אופקית):
LazyVerticalStaggeredGrid( columns = StaggeredGridCells.Fixed(3), verticalItemSpacing = 4.dp, horizontalArrangement = Arrangement.spacedBy(4.dp), content = { items(randomSizedPhotos) { photo -> AsyncImage( model = photo, contentScale = ContentScale.Crop, contentDescription = null, modifier = Modifier .fillMaxWidth() .wrapContentHeight() ) } }, modifier = Modifier.fillMaxSize() )
מרווחים פנימיים של תוכן
לפעמים צריך להוסיף שוליים מסביב לקצוות של התוכן. הקומפוננטות lazy מאפשרות להעביר חלק מהפרמטרים PaddingValues לפרמטר contentPadding כדי לתמוך בכך:
LazyColumn( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), ) { // ... }
בדוגמה הזו, אנחנו מוסיפים 16.dp של ריווח פנימי לקצוות האופקיים (ימין ושמאל), ואז 8.dp לחלק העליון והתחתון של התוכן.
חשוב לשים לב שהריווח הפנימי הזה מתווסף לתוכן, ולא ל-LazyColumn עצמו. בדוגמה שלמעלה, לפריט הראשון יתווסף ריווח פנימי של 8.dp בחלק העליון, לפריט האחרון יתווסף ריווח פנימי של 8.dp בחלק התחתון, ולכל הפריטים יתווסף ריווח פנימי של 16.dp בצד ימין ובצד שמאל.
דוגמה נוספת: אפשר להעביר את Scaffold's PaddingValues אל LazyColumn's contentPadding. למידע נוסף, קראו את המדריך בנושא תצוגה מקצה לקצה.
ריווח התוכן
כדי להוסיף רווחים בין הפריטים, אפשר להשתמש ב-Arrangement.spacedBy().
בדוגמה הבאה מתווסף רווח של 4.dp בין כל פריט:
LazyColumn( verticalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
באופן דומה, לגבי LazyRow:
LazyRow( horizontalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
עם זאת, אפשר להשתמש בגרידים גם בפריסה אנכית וגם בפריסה אופקית:
LazyVerticalGrid( columns = GridCells.Fixed(2), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { items(photos) { item -> PhotoItem(item) } }
מפתחות פריטים
כברירת מחדל, המצב של כל פריט מוגדר לפי המיקום של הפריט ברשימה או ברשת. עם זאת, זה עלול לגרום לבעיות אם מערך הנתונים משתנה, כי פריטים שמשנים את המיקום שלהם מאבדים למעשה את המצב שנשמר. אם נדמיין את התרחיש של LazyRow בתוך LazyColumn, אם המיקום של הפריט בשורה ישתנה, המשתמש יאבד את מיקום הגלילה שלו בתוך השורה.
כדי לפתור את הבעיה הזו, אפשר לספק מפתח יציב וייחודי לכל פריט, ולספק בלוק לפרמטר key. הוספת מפתח יציב מאפשרת לשמור על עקביות של מצב הפריט גם כשמבצעים שינויים במערך הנתונים:
LazyColumn { items( items = messages, key = { message -> // Return a stable + unique key for the item message.id } ) { message -> MessageRow(message) } }
הוספת מפתחות עוזרת ל-Compose לטפל נכון בשינוי הסדר. לדוגמה, אם הפריט מכיל מצב שנשמר, הגדרת מקשים תאפשר ל-Compose להעביר את המצב הזה יחד עם הפריט, כשהמיקום שלו משתנה.
LazyColumn { items(books, key = { it.id }) { val rememberedValue = remember { Random.nextInt() } } }
עם זאת, יש מגבלה אחת על הסוגים שבהם אפשר להשתמש כמפתחות פריטים.
הסוג של המפתח חייב להיות נתמך על ידי Bundle, המנגנון של Android לשמירת המצבים כשהפעילות נוצרת מחדש. Bundle תומך בסוגים כמו פרימיטיבים, enum או Parcelable.
LazyColumn { items(books, key = { // primitives, enums, Parcelable, etc. }) { // ... } }
המפתח צריך להיות נתמך על ידי Bundle כדי שאפשר יהיה לשחזר את rememberSaveable בתוך רכיב קומפוזבילי של פריט כשהפעילות נוצרת מחדש, או אפילו כשגוללים מהפריט הזה וחוזרים אליו.
LazyColumn { items(books, key = { it.id }) { val rememberedValue = rememberSaveable { Random.nextInt() } } }
אנימציות של פריטים
אם השתמשתם בווידג'ט RecyclerView, אתם יודעים שהוא מנפיש שינויים בפריטים באופן אוטומטי.
פריסות עצלות מספקות את אותה פונקציונליות לשינוי הסדר של פריטים.
ה-API פשוט – צריך רק להגדיר את משנה המאפיין animateItem בתוכן הפריט:
LazyColumn { // It is important to provide a key to each item to ensure animateItem() works as expected. items(books, key = { it.id }) { Row(Modifier.animateItem()) { // ... } } }
אם צריך, אפשר אפילו לספק הגדרות מותאמות אישית לאנימציה:
LazyColumn { items(books, key = { it.id }) { Row( Modifier.animateItem( fadeInSpec = tween(durationMillis = 250), fadeOutSpec = tween(durationMillis = 100), placementSpec = spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy) ) ) { // ... } } }
חשוב לספק מפתחות לפריטים כדי שיהיה אפשר למצוא את המיקום החדש של הרכיב שהועבר.
דוגמה: אנימציה של פריטים ברשימות עצלות
בעזרת Compose, אפשר להוסיף אנימציות לשינויים בפריטים ברשימות עצלות. כשמשתמשים בקטעי הקוד הבאים ביחד, הם מוסיפים אנימציות כשמוסיפים, מסירים ומסדרים מחדש פריטים ברשימה עצלה.
בקטע הקוד הזה מוצגת רשימה של מחרוזות עם מעברים מונפשים כשמוסיפים פריטים, מסירים פריטים או משנים את הסדר שלהם:
@Composable fun ListAnimatedItems( items: List<String>, modifier: Modifier = Modifier ) { LazyColumn(modifier) { // Use a unique key per item, so that animations work as expected. items(items, key = { it }) { ListItem( headlineContent = { Text(it) }, modifier = Modifier .animateItem( // Optionally add custom animation specs ) .fillParentMaxWidth() .padding(horizontal = 8.dp, vertical = 0.dp), ) } } }
מידע חשוב על הקוד
-
ListAnimatedItemsמציג רשימה של מחרוזות ב-LazyColumnעם מעברים מונפשים כשפריטים משתנים. - הפונקציה
itemsמקצה מפתח ייחודי לכל פריט ברשימה. פיתוח נייטיב משתמש במפתחות כדי לעקוב אחרי הפריטים ולזהות שינויים במיקומים שלהם. -
ListItemמגדיר את הפריסה של כל פריט ברשימה. הוא מקבל פרמטרheadlineContentשמגדיר את התוכן העיקרי של הפריט. - המשנה
animateItemמוסיף אנימציות שמוגדרות כברירת מחדל לפריטים שנוספו, הוסרו או הועברו.
בקטע הקוד הבא מוצג מסך שכולל אמצעי בקרה להוספה ולהסרה של פריטים, וגם למיון של רשימה מוגדרת מראש:
@Composable private fun ListAnimatedItemsExample( data: List<String>, modifier: Modifier = Modifier, onAddItem: () -> Unit = {}, onRemoveItem: () -> Unit = {}, resetOrder: () -> Unit = {}, onSortAlphabetically: () -> Unit = {}, onSortByLength: () -> Unit = {}, ) { val canAddItem = data.size < 10 val canRemoveItem = data.isNotEmpty() Scaffold(modifier) { paddingValues -> Column( modifier = Modifier .padding(paddingValues) .fillMaxSize() ) { // Buttons that change the value of displayedItems. AddRemoveButtons(canAddItem, canRemoveItem, onAddItem, onRemoveItem) OrderButtons(resetOrder, onSortAlphabetically, onSortByLength) // List that displays the values of displayedItems. ListAnimatedItems(data) } } }
מידע חשוב על הקוד
ListAnimatedItemsExampleמציג מסך שכולל אמצעי בקרה להוספה, להסרה ולמיון של פריטים.-
onAddItemו-onRemoveItemהם ביטויי למבדה שמועברים אלAddRemoveButtonsכדי להוסיף פריטים לרשימה ולהסיר פריטים ממנה. -
resetOrder,onSortAlphabeticallyו-onSortByLengthהם ביטויי למדה שמועברים אלOrderButtonsכדי לשנות את סדר הפריטים ברשימה.
-
AddRemoveButtonsמוצגים הלחצנים 'הוספה' ו'הסרה'. הוא מפעיל או משבית את הכפתורים ומטפל בלחיצות על הכפתורים.OrderButtonsמציג את הלחצנים לסידור מחדש של הרשימה. הוא מקבל את פונקציות ה-lambda לאיפוס הסדר ולמיון הרשימה לפי אורך או לפי סדר אלפביתי.-
ListAnimatedItemsשולח קריאה לרכיב הקומפוזביליListAnimatedItems, ומעביר את רשימתdataכדי להציג את רשימת המחרוזות המונפשת. הערךdataמוגדר במקום אחר.
קטע הקוד הזה יוצר ממשק משתמש עם הלחצנים Add Item (הוספת פריט) ו-Delete Item (מחיקת פריט):
@Composable private fun AddRemoveButtons( canAddItem: Boolean, canRemoveItem: Boolean, onAddItem: () -> Unit, onRemoveItem: () -> Unit ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center ) { Button(enabled = canAddItem, onClick = onAddItem) { Text("Add Item") } Spacer(modifier = Modifier.padding(25.dp)) Button(enabled = canRemoveItem, onClick = onRemoveItem) { Text("Delete Item") } } }
מידע חשוב על הקוד
AddRemoveButtonsמוצגת שורה של לחצנים לביצוע פעולות של הוספה והסרה ברשימה.- הפרמטרים
canAddItemו-canRemoveItemקובעים את מצב ההפעלה של הלחצנים. אם הערך שלcanAddItemאוcanRemoveItemהוא False, הלחצן המתאים מושבת. - הפרמטרים
onAddItemו-onRemoveItemהם פונקציות אנונימיות שמופעלות כשהמשתמש לוחץ על הכפתור המתאים.
לבסוף, בקטע הקוד הזה מוצגים שלושה לחצנים למיון הרשימה (Reset, Alphabetical ו-Length):
@Composable private fun OrderButtons( resetOrder: () -> Unit, orderAlphabetically: () -> Unit, orderByLength: () -> Unit ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center ) { var selectedIndex by remember { mutableIntStateOf(0) } val options = listOf("Reset", "Alphabetical", "Length") SingleChoiceSegmentedButtonRow { options.forEachIndexed { index, label -> SegmentedButton( shape = SegmentedButtonDefaults.itemShape( index = index, count = options.size ), onClick = { Log.d("AnimatedOrderedList", "selectedIndex: $selectedIndex") selectedIndex = index when (options[selectedIndex]) { "Reset" -> resetOrder() "Alphabetical" -> orderAlphabetically() "Length" -> orderByLength() } }, selected = index == selectedIndex ) { Text(label) } } } } }
מידע חשוב על הקוד
- ב-
OrderButtonsמוצגSingleChoiceSegmentedButtonRowכדי לאפשר למשתמשים לבחור שיטת מיון ברשימה או לאפס את סדר הרשימה. רכיב ASegmentedButtonמאפשר לבחור אפשרות אחת מתוך רשימת אפשרויות. -
resetOrder,orderAlphabeticallyו-orderByLengthהן פונקציות למבדה שמופעלות כשבוחרים את הלחצן המתאים. - משתנה המצב
selectedIndexעוקב אחרי האפשרות שנבחרה.
תוצאה
בסרטון הזה מוצגת התוצאה של קטעי הקוד הקודמים כשמשנים את הסדר של הפריטים:
כותרות קבועות
התבנית 'כותרת קבועה' שימושית כשמציגים רשימות של נתונים מקובצים. בהמשך מוצגת דוגמה של רשימת אנשי קשר, שמקובצת לפי האות הראשונה של כל איש קשר:

כדי ליצור כותרת קבועה באמצעות LazyColumn, אפשר להשתמש בפונקציה הניסיונית stickyHeader() ולספק את תוכן הכותרת:
@Composable fun ListWithHeader(items: List<Item>) { LazyColumn { stickyHeader { Header() } items(items) { item -> ItemRow(item) } } }
כדי ליצור רשימה עם כמה כותרות, כמו בדוגמה של 'רשימת אנשי קשר' שלמעלה, אפשר לעשות את הפעולות הבאות:
// This ideally would be done in the ViewModel val grouped = contacts.groupBy { it.firstName[0] } @Composable fun ContactsList(grouped: Map<Char, List<Contact>>) { LazyColumn { grouped.forEach { (initial, contactsForInitial) -> stickyHeader { CharacterHeader(initial) } items(contactsForInitial) { contact -> ContactListItem(contact) } } } }
תגובה למיקום הגלילה
הרבה אפליקציות צריכות להגיב לשינויים במיקום הגלילה ובפריסת הפריטים, ולהאזין להם.
רכיבי Lazy תומכים בתרחיש השימוש הזה באמצעות העלאה של LazyListState:
@Composable fun MessageList(messages: List<Message>) { // Remember our own LazyListState val listState = rememberLazyListState() // Provide it to LazyColumn LazyColumn(state = listState) { // ... } }
במקרים פשוטים, האפליקציות צריכות לדעת רק את המידע על הפריט הראשון שמוצג. במקרה הזה, LazyListState מספק את המאפיינים firstVisibleItemIndex ו-firstVisibleItemScrollOffset.
אם נשתמש בדוגמה של הצגה והסתרה של לחצן בהתאם לכך שהמשתמש גלל מעבר לפריט הראשון:
@Composable fun MessageList(messages: List<Message>) { Box { val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } // Show the button if the first visible item is past // the first item. We use a remembered derived state to // minimize unnecessary compositions val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() } } }
קריאת הסטטוס ישירות בקומפוזיציה שימושית כשצריך לעדכן רכיבי UI אחרים שניתנים להרכבה, אבל יש גם תרחישים שבהם אין צורך לטפל באירוע באותה קומפוזיציה. דוגמה נפוצה לכך היא שליחת אירוע Analytics אחרי שהמשתמש גלל מעבר לנקודה מסוימת. כדי לטפל בזה בצורה יעילה, אפשר להשתמש ב-snapshotFlow():
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .map { index -> index > 0 } .distinctUntilChanged() .filter { it } .collect { MyAnalyticsService.sendScrolledPastFirstItemEvent() } }
בנוסף, LazyListState מספק מידע על כל הפריטים שמוצגים כרגע ועל הגבולות שלהם במסך, באמצעות המאפיין layoutInfo. מידע נוסף זמין במחלקה LazyListLayoutInfo.
שליטה במיקום הגלילה
בנוסף לתגובה למיקום הגלילה, כדאי שאפליקציות יוכלו גם לשלוט במיקום הגלילה.
LazyListState תומך בזה באמצעות הפונקציה scrollToItem(), שמעבירה את מיקום הגלילה 'באופן מיידי', והפונקציה animateScrollToItem(), שמעבירה את מיקום הגלילה באמצעות אנימציה (נקראת גם גלילה חלקה):
@Composable fun MessageList(messages: List<Message>) { val listState = rememberLazyListState() // Remember a CoroutineScope to be able to launch val coroutineScope = rememberCoroutineScope() LazyColumn(state = listState) { // ... } ScrollToTopButton( onClick = { coroutineScope.launch { // Animate scroll to the first item listState.animateScrollToItem(index = 0) } } ) }
קבוצות נתונים גדולות (חלוקה לדפים)
ספריית Paging מאפשרת לאפליקציות לתמוך ברשימות גדולות של פריטים, לטעון ולהציג חלקים קטנים מהרשימה לפי הצורך. הספרייה androidx.paging:paging-compose מספקת תמיכה ב-Compose ב-Paging 3.0 ואילך.
כדי להציג רשימה של תוכן עם מספור עמודים, אפשר להשתמש בפונקציית התוסף collectAsLazyPagingItems() ואז להעביר את הערך המוחזר LazyPagingItems אל items() ב-LazyColumn. בדומה לתמיכה בהחלפת דפים בתצוגות, אתם יכולים להציג מצייני מיקום בזמן שהנתונים נטענים. לשם כך, צריך לבדוק אם הערך של item הוא null:
@Composable fun MessageList(pager: Pager<Int, Message>) { val lazyPagingItems = pager.flow.collectAsLazyPagingItems() LazyColumn { items( lazyPagingItems.itemCount, key = lazyPagingItems.itemKey { it.id } ) { index -> val message = lazyPagingItems[index] if (message != null) { MessageRow(message) } else { MessagePlaceholder() } } } }
טיפים לשימוש בפריסות עצלניות
כדי לוודא שהפריסות העצלניות פועלות כמו שצריך, כדאי לקחת בחשבון את הטיפים הבאים.
הימנעו משימוש בפריטים בגודל 0 פיקסלים
זה יכול לקרות בתרחישים שבהם, למשל, אתם מצפים לאחזר באופן אסינכרוני נתונים מסוימים כמו תמונות, כדי למלא את הפריטים ברשימה בשלב מאוחר יותר. במקרה כזה, הפריסה Lazy תרכיב את כל הפריטים שלה במדידה הראשונה, כי הגובה שלהם הוא 0 פיקסלים והיא יכולה להתאים את כולם לאזור התצוגה. אחרי שהפריטים נטענים והגובה שלהם מתרחב, פריסות Lazy יבטלו את כל הפריטים האחרים שהורכבו שלא לצורך בפעם הראשונה, כי הם לא יכולים להתאים לאזור התצוגה. כדי להימנע מכך, צריך להגדיר גודל ברירת מחדל לפריטים, כדי שהפריסה Lazy תוכל לבצע את החישוב הנכון של מספר הפריטים שיכולים להתאים לאזור התצוגה:
@Composable fun Item(imageUrl: String) { AsyncImage( model = rememberAsyncImagePainter(model = imageUrl), modifier = Modifier.size(30.dp), contentDescription = null // ... ) }
אם אתם יודעים מה הגודל המשוער של הפריטים אחרי שהנתונים נטענים באופן אסינכרוני, מומלץ לוודא שהגודל של הפריטים נשאר זהה לפני ואחרי הטעינה. למשל, אפשר להוסיף כמה placeholder. כך תוכלו לשמור על מיקום הגלילה הנכון.
הימנעו מהוספת רכיבים שניתן לגלול באותו כיוון
ההגדרה הזו חלה רק על מקרים של הטמעת צאצאים שניתנים לגלילה ללא גודל מוגדר מראש בתוך רכיב אב שניתן לגלילה באותו כיוון. לדוגמה, ניסיון להטמיע צאצא LazyColumn ללא גובה קבוע בתוך רכיב אב Column שניתן לגלילה אנכית:
// throws IllegalStateException Column( modifier = Modifier.verticalScroll(state) ) { LazyColumn { // ... } }
במקום זאת, אפשר להשיג את אותה תוצאה על ידי הוספת כל הרכיבים הניתנים להרכבה בתוך רכיב אב אחד LazyColumn ושימוש ב-DSL שלו כדי להעביר סוגים שונים של תוכן. כך אפשר להוציא פריטים בודדים, וגם כמה פריטים ברשימה, והכול במקום אחד:
LazyColumn { item { Header() } items(data) { item -> PhotoItem(item) } item { Footer() } }
חשוב לזכור שמותר להשתמש במקרים שבהם יוצרים היררכיה של פריסות בכיוונים שונים, למשל, רכיב הורה עם אפשרות גלילה Row ורכיב צאצא LazyColumn:
Row( modifier = Modifier.horizontalScroll(scrollState) ) { LazyColumn { // ... } }
בנוסף, במקרים שבהם עדיין משתמשים באותם פריסות של כיוונים, אבל גם מגדירים גודל קבוע לרכיבי הצאצא שמוטמעים:
Column( modifier = Modifier.verticalScroll(scrollState) ) { LazyColumn( modifier = Modifier.height(200.dp) ) { // ... } }
היזהרו מלהוסיף כמה רכיבים לפריט אחד
בדוגמה הזו, פונקציית ה-lambda של הפריט השני פולטת 2 פריטים בבלוק אחד:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Item(2) } item { Item(3) } // ... }
פריסות עצלות יטפלו בזה כצפוי – הן יציגו את הרכיבים אחד אחרי השני כאילו הם פריטים שונים. עם זאת, יש כמה בעיות בשיטה הזו.
כשכמה רכיבים מופקים כחלק מפריט אחד, המערכת מתייחסת אליהם כישות אחת, כלומר אי אפשר יותר להרכיב אותם בנפרד. אם רכיב אחד הופך לגלוי על המסך, כל הרכיבים שמתאימים לפריט צריכים להיות מורכבים ונמדדים. שימוש מוגזם עלול לפגוע בביצועים. במקרה הקיצוני של הוספת כל הרכיבים לפריט אחד, אין טעם להשתמש בפריסות Lazy. בנוסף לבעיות פוטנציאליות בביצועים, הוספה של יותר מדי רכיבים לפריט אחד תשפיע גם על scrollToItem() ועל animateScrollToItem().
עם זאת, יש תרחישי שימוש לגיטימיים להצבת כמה רכיבים בפריט אחד, כמו הוספת קווי הפרדה בתוך רשימה. לא רוצים שקווי ההפרדה ישנו את אינדקס הגלילה, כי הם לא נחשבים לרכיבים עצמאיים. בנוסף, הביצועים לא מושפעים כי קווי ההפרדה קטנים. סביר להניח שקו הפרדה צריך להיות גלוי כשהפריט שלפניו גלוי, ולכן הוא יכול להיות חלק מהפריט הקודם:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Divider() } item { Item(2) } // ... }
כדאי להשתמש בפריסות מותאמות אישית
בדרך כלל רשימות Lazy מכילות הרבה פריטים, והן תופסות יותר מקום מהגודל של מאגר הגלילה. עם זאת, כשהרשימה כוללת רק כמה פריטים, יכול להיות שיהיו דרישות ספציפיות יותר לגבי המיקום שלהם באזור התצוגה.
כדי לעשות את זה, אפשר להשתמש בArrangement מותאם אישית לפי ענף ולהעביר אותו אל LazyColumn. בדוגמה הבאה, האובייקט TopWithFooter צריך להטמיע רק את המתודה arrange. קודם כול, הפריטים יוצבו אחד אחרי השני. שנית, אם הגובה הכולל בשימוש נמוך מגובה אזור התצוגה, הכותרת התחתונה תמוקם בתחתית:
object TopWithFooter : Arrangement.Vertical { override fun Density.arrange( totalSize: Int, sizes: IntArray, outPositions: IntArray ) { var y = 0 sizes.forEachIndexed { index, size -> outPositions[index] = y y += size } if (y < totalSize) { val lastIndex = outPositions.lastIndex outPositions[lastIndex] = totalSize - sizes.last() } } }
כדאי להוסיף contentType
החל מ-Compose 1.2, כדי למקסם את הביצועים של Lazy
layout, מומלץ להוסיף contentType לרשימות או לרשתות. כך אפשר לציין את סוג התוכן של כל פריט בפריסת הדף, במקרים שבהם יוצרים רשימה או רשת שמורכבת מכמה סוגים שונים של פריטים:
LazyColumn { items(elements, contentType = { it.type }) { // ... } }
כשמספקים את contentType, אפשר להשתמש מחדש בפריטים ב-Compose רק בין פריטים מאותו סוג. השימוש מחדש יעיל יותר כשיוצרים פריטים עם מבנה דומה, ולכן כשמספקים את סוגי התוכן, Compose לא מנסה ליצור פריט מסוג A על גבי פריט שונה לחלוטין מסוג B. כך אפשר למקסם את היתרונות של שימוש חוזר בפריטים ואת הביצועים של פריסת Lazy.
מדידת ביצועים
אפשר למדוד באופן מהימן את הביצועים של פריסת Lazy רק כשמריצים אותה במצב release ועם אופטימיזציה של R8 מופעלת. בגרסאות debug, יכול להיות שהגלילה של פריסת Lazy תיראה איטית יותר. מידע נוסף בנושא זמין במאמר ביצועים של Compose.
מקורות מידע נוספים
- יצירת רשימה סופית שאפשר לגלול בה
- יצירת רשת עם אפשרות גלילה
- הצגת פריטים ברשימה עם גלילה מוטמעת
- סינון רשימה תוך כדי הקלדה
- טעינת נתונים עצלה באמצעות רשימות וחלוקה לדפים
- יצירת רשימה באמצעות כמה סוגים של פריטים
- סרטון: רשימות בכלי הכתיבה
מומלץ בשבילכם
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- העברה של
RecyclerViewל-Lazy list - שמירת מצב ממשק המשתמש בפיתוח נייטיב
- Kotlin ל-Jetpack Compose