באפליקציות רבות צריך להציג אוספים של פריטים. במאמר הזה מוסבר איך לעשות את זה ביעילות ב-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.
רשתות עם טעינה מדורגת
הפונקציות הקומפוזביליות
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 של השיטות LazyGridScope DSL
item ו-items.
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 משמאל ומימין.
דוגמה נוספת: אפשר להעביר את PaddingValues של Scaffold אל contentPadding של LazyColumn. למדריך בנושא פריסה מקצה לקצה
מרווחים בין התכנים
כדי להוסיף רווח בין פריטים, אפשר להשתמש ב-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 לטפל בסידור מחדש בצורה נכונה. לדוגמה, אם הפריט מכיל מצב שנשמר, הגדרת מפתחות תאפשר למערכת פיתוח נייטיב להעביר את המצב הזה יחד עם הפריט כשהמיקום שלו משתנה.
LazyColumn { items(books, key = { it.id }) { val rememberedValue = remember { Random.nextInt() } } }
עם זאת, יש מגבלה אחת על הסוגים שבהם אפשר להשתמש כמפתחות של פריטים.
סוג המפתח חייב להיות נתמך על ידי Bundle, המנגנון של Android לשמירת המצבים כשה-Activity נוצר מחדש. Bundle תומך בסוגים כמו פרימיטיבים, טיפוסי enum או Parcelables.
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מציג את הלחצנים לסידור מחדש של הרשימה. היא מקבלת את פונקציות הלמבדה לאיפוס הסדר ולמיון הרשימה לפי אורך או לפי סדר אלפביתי.-
ListAnimatedItemsקורא לרכיב הקומפוזביליListAnimatedItemsומעביר את הרשימהdataכדי להציג את הרשימה המונפשת של המחרוזות. הערךdataמוגדר במקום אחר.
קטע הקוד הזה יוצר ממשק משתמש עם הלחצנים הוספת פריט ומחיקת פריט:
@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הם פונקציות למבדה שמופעלות כשהמשתמש לוחץ על הכפתור המתאים.
בסוף קטע הקוד הזה מוצגים שלושה כפתורים למיון הרשימה (איפוס, סדר אלפביתי ואורך):
@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כדי לאפשר למשתמשים לבחור שיטת מיון ברשימה או לאפס את סדר הרשימה. רכיבSegmentedButtonמאפשר לבחור אפשרות אחת מתוך רשימה של אפשרויות.-
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 מאפשרת לאפליקציות לתמוך ברשימות גדולות של פריטים, ולטעון ולהציג חלקים קטנים מהרשימה לפי הצורך. בגרסה 3.0 ואילך של Paging יש תמיכה ב-Compose דרך הספרייה androidx.paging:paging-compose.
כדי להציג רשימה של תוכן עם דפים, אנחנו יכולים להשתמש בפונקציית ההרחבה collectAsLazyPagingItems(), ואז להעביר את הערך המוחזר LazyPagingItems אל items() ב-LazyColumn. בדומה לתמיכה ב-Paging בתצוגות, אפשר להציג פלייסהולדרים בזמן שהנתונים נטענים. כדי לעשות את זה, בודקים אם 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() } } } }
טיפים לשימוש בפריסות Lazy
יש כמה טיפים שכדאי להביא בחשבון כדי לוודא שהפריסות עם טעינה מדורגת פועלות כמצופה.
הימנעו משימוש בפריטים בגודל 0 פיקסלים
זה יכול לקרות בתרחישים שבהם, לדוגמה, אתם מצפים לאחזר באופן אסינכרוני נתונים מסוימים כמו תמונות, כדי למלא את הפריטים ברשימה בשלב מאוחר יותר. זה יגרום לפריסה ה"עצלה" ליצור את כל הפריטים שלה במדידה הראשונה, כי הגובה שלהם הוא 0 פיקסלים והיא יכולה להתאים את כולם לאזור התצוגה. אחרי שהפריטים נטענים והגובה שלהם מתרחב, פריסות עם טעינה מדורגת ימחקו את כל הפריטים האחרים שנוצרו שלא לצורך בפעם הראשונה, כי הם לא יכולים להתאים לאזור התצוגה. כדי למנוע את זה, צריך להגדיר גודל ברירת מחדל לפריטים, כדי שהפריסה הדינמית תוכל לחשב בצורה נכונה כמה פריטים יכולים להיכנס לאזור התצוגה:
@Composable fun Item(imageUrl: String) { AsyncImage( model = rememberAsyncImagePainter(model = imageUrl), modifier = Modifier.size(30.dp), contentDescription = null // ... ) }
אם אתם יודעים מה הגודל המשוער של הפריטים אחרי שהנתונים נטענים באופן אסינכרוני, מומלץ לוודא שגודל הפריטים נשאר זהה לפני הטעינה ואחריה, למשל על ידי הוספת כמה פלייסהולדרים. כך אפשר לשמור על מיקום הגלילה הנכון.
הימנעו מהטמעה של רכיבים שניתן לגלול באותו כיוון
ההגדרה הזו רלוונטית רק למקרים שבהם רכיבי צאצא ניתנים לגלילה ללא גודל מוגדר מראש מקוננים בתוך רכיב הורה אחר שניתן לגלילה באותו כיוון. לדוגמה, ניסיון להטמיע רכיב צאצא LazyColumn בלי גובה קבוע בתוך רכיב הורה Column עם גלילה אנכית:
// throws IllegalStateException Column( modifier = Modifier.verticalScroll(state) ) { LazyColumn { // ... } }
במקום זאת, אפשר להשיג את אותה תוצאה על ידי גלישת כל ה-composables בתוך הורה אחד 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) ) { // ... } }
חשוב להיזהר כשמכניסים כמה רכיבים לפריט אחד
בדוגמה הזו, פונקציית הלמבדה של הפריט השני פולטת 2 פריטים בבלוק אחד:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Item(2) } item { Item(3) } // ... }
פריסות עם טעינה מדורגת יטפלו בזה כצפוי – הן יפרסו את הרכיבים אחד אחרי השני כאילו היו פריטים שונים. אבל יש כמה בעיות עם זה.
כשכמה רכיבים נפלטים כחלק מפריט אחד, הם מטופלים כישות אחת, כלומר אי אפשר יותר ליצור אותם בנפרד. אם רכיב אחד הופך לגלוי במסך, צריך להרכיב ולמדוד את כל הרכיבים שתואמים לפריט. שימוש מוגזם ב-CSS יכול לפגוע בביצועים. במקרה קיצוני שבו כל הרכיבים נמצאים בפריט אחד, אין שום טעם להשתמש בפריסות עם טעינה מדורגת. מלבד בעיות פוטנציאליות בביצועים, הוספת אלמנטים נוספים לפריט אחד תפריע גם ל-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
החל מגרסה 1.2 של Compose, כדי למקסם את הביצועים של פריסת Lazy, כדאי להוסיף contentType לרשימות או לרשתות. כך אפשר לציין את סוג התוכן של כל פריט בפריסה, במקרים שבהם אתם יוצרים רשימה או רשת שמורכבת מכמה סוגים שונים של פריטים:
LazyColumn { items(elements, contentType = { it.type }) { // ... } }
כשמציינים את contentType, אפשר לעשות שימוש חוזר בקומפוזיציות רק בין פריטים מאותו סוג. שימוש חוזר יעיל יותר כשיוצרים פריטים עם מבנה דומה, ולכן ציון סוגי התוכן מבטיח ש-Compose לא ינסה ליצור פריט מסוג א' על גבי פריט שונה לחלוטין מסוג ב'. כך אפשר למקסם את היתרונות של שימוש חוזר בקומפוזיציה ולשפר את הביצועים של פריסת Lazy.
מדידת הביצועים
אפשר למדוד באופן מהימן את הביצועים של פריסה עם טעינה מדורגת רק כשמפעילים את מצב ההפצה ואת האופטימיזציה של R8. בגרסאות build לניפוי באגים, יכול להיות שהגלילה בפריסה עם טעינה מדורגת תהיה איטית יותר. מידע נוסף זמין במאמר ביצועים של Compose.
מקורות מידע נוספים
- יצירת רשימה סופית שאפשר לגלול בה
- יצירת רשת עם גלילה
- הצגת פריטים בגלילה מקוננת ברשימה
- סינון רשימה תוך כדי הקלדה
- טעינה מדורגת של נתונים באמצעות רשימות ו-Paging
- יצירת רשימה עם כמה סוגי פריטים
- סרטון: רשימות ב-Compose
מומלץ בשבילכם
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- העברת
RecyclerViewלרשימה בטעינה מדורגת - שמירת מצב ממשק המשתמש בפיתוח נייטיב
- Kotlin ל-Jetpack Compose