באפליקציות רבות צריך להציג אוספים של פריטים. במאמר הזה נסביר איך לעשות את זה ביעילות ב-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
שפת התצורה של 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
של השיטות
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
padding בחלק העליון, הפריט האחרון יקבל 8.dp
padding בחלק התחתון, וכל הפריטים יקבלו 16.dp
padding בצד ימין ובצד שמאל.
דוגמה נוספת: אפשר להעביר את 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
תומך בסוגים כמו primitives, enums או 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
מקצה מפתח ייחודי לכל פריט ברשימה. הכלי Compose משתמש במקשים כדי לעקוב אחרי הפריטים ולזהות שינויים במיקומים שלהם. -
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()
ולספק את תוכן הכותרת:
@OptIn(ExperimentalFoundationApi::class) @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] } @OptIn(ExperimentalFoundationApi::class) @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() } } } }
טיפים לשימוש בפריסות עצלניות
ריכזנו כמה טיפים שיעזרו לכם לוודא שפריסות Lazy פועלות כמו שצריך.
הימנעו משימוש בפריטים בגודל 0 פיקסלים
זה יכול לקרות בתרחישים שבהם, לדוגמה, אתם מצפים לאחזר באופן אסינכרוני נתונים מסוימים כמו תמונות, כדי למלא את הפריטים ברשימה בשלב מאוחר יותר. במקרה כזה, הפריסה העצלנית תרכיב את כל הפריטים שלה במדידה הראשונה, כי הגובה שלהם הוא 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 לא ינסה ליצור פריט מסוג A על גבי פריט שונה לחלוטין מסוג B. כך אפשר למקסם את היתרונות של שימוש חוזר בקומפוזיציה ואת הביצועים של פריסת Lazy.
מדידת ביצועים
אפשר למדוד את הביצועים של פריסת Lazy בצורה מהימנה רק כשמריצים אותה במצב release ועם אופטימיזציה של R8 מופעלת. בגרסאות build לניפוי באגים, יכול להיות שהגלילה בפריסה של Lazy תהיה איטית יותר. מידע נוסף בנושא זמין במאמר ביצועים של כתיבת תוכן.
מקורות מידע נוספים
- יצירת רשימה סופית שאפשר לגלול בה
- יצירת רשת עם אפשרות גלילה
- הצגת פריטים ברשימה עם גלילה מקוננת
- סינון רשימה תוך כדי הקלדה
- טעינת נתונים עצלה באמצעות רשימות וחלוקה לדפים
- יצירת רשימה באמצעות כמה סוגים של פריטים
- סרטון: רשימות בכתיבת אימייל
מומלץ עבורך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- העברה של
RecyclerView
לרשימה 'העברה' - שמירת מצב ממשק המשתמש בכתיבה
- Kotlin ל-Jetpack פיתוח נייטיב