באפליקציות רבות צריך להציג אוספים של פריטים. במאמר הזה מוסבר איך לעשות את זה ביעילות ב-Jetpack Compose.
אם אתם יודעים שתרחיש השימוש שלכם לא דורש גלילה, אתם יכולים להשתמש ב-Column או ב-Row פשוטים (בהתאם לכיוון), ולהציג את התוכן של כל פריט על ידי חזרה על רשימה באופן הבא:
@Composable fun MessageList(messages: List<Message>) { Column { messages.forEach { message -> MessageRow(message) } } }
כדי לאפשר גלילה ב-Column, אפשר להשתמש במשנה verticalScroll().
רשימות בטעינה מדורגת (lazy loading)
אם אתם צריכים להציג מספר גדול של פריטים (או רשימה באורך לא ידוע), שימוש בפריסה כמו 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() )
שיפור ביצועי הגלילה באמצעות LazyLayoutCacheWindow
LazyLayoutCacheWindow הוא API שנועד לשפר את ביצועי הגלילה ברשימות, ברשתות וברשתות מדורגות. ב-CacheWindow, אתם יכולים להגדיר מרחב וירטואלי שבו אפשר לאחזר מראש פריטים (חלון קדימה) או להשאיר אותם פעילים (חלון אחורה) במהלך אירועי גלילה. זה אומר שהקומפוזיציה והמדידה של הפריטים מפעילות מטמון פנימי – כשהפריטים האלה נדרשים בפועל (כשהם הופכים לגלויים), הצגתם נמשכת הרבה פחות זמן מאשר אם הם היו נוצרים מאפס.
כל האוספים בטעינה מדורגת משתמשים ב-CacheWindow כשיטת ברירת המחדל לאחזור מראש. בחלון ברירת המחדל, מרחק השליפה מראש מוגבל לטווח שבין 10% ל-50% מאזור התצוגה. היא מגדירה את החלון שמאחורי התוכן ל-0 פיקסלים ומשביתה את השמירה במטמון של תוכן שלא נגלל.
הגדרת החלון
אפשר להגדיר חלון מטמון באמצעות חלקי חלון תצוגה או מרחקים קבועים ב-Dp:
// Fraction-based cache window: prefetch 50% of viewport ahead, keep 20% behind LazyColumn( cacheWindow = LazyLayoutCacheWindow(aheadFraction = 0.5f, behindFraction = 0.2f) ) { items(itemsList) { item -> Text("Item $item") } } // Dp-based cache window: prefetch 150.dp ahead, keep 50.dp behind LazyVerticalGrid( columns = GridCells.Fixed(3), cacheWindow = LazyLayoutCacheWindow( ahead = 150.dp, behind = 50.dp, isNonScrollCachingEnabled = true ) ) { items(itemsList) { item -> Text("Item $item") } }
חלון המטמון מנהל שני אזורים נפרדים ביחס לכיוון הגלילה:
- חלון קדימה: המסגרת יוצרת ומודדת מראש את הפריטים הבאים לפני שהם נכנסים לאזור התצוגה הגלוי. ברירת המחדל היא
0dpאו0f. - מאחורי החלון: המסגרת שומרת פריטים שנגללו לאחרונה בלי למחוק אותם. כשהמשתמשים משנים את כיוון הגלילה, הפריטים שנשמרו מוצגים מיד בלי ליצור אותם מחדש או למדוד אותם מחדש. ערך ברירת המחדל הוא
0dpאו0f.
בנוסף, כשמגדירים את isNonScrollCachingEnabled = true (ערך ברירת המחדל), הפריסה מאכלסת את החלון הבא בתקופות של חוסר פעילות, מחוץ לגלילות הפעילות של המשתמש. הטריגרים האלה כוללים את שלב הפריסה הראשוני, עדכונים של קבוצת הנתונים ושינוי הסדר של הפריטים. כשהמדיניות מוגדרת לערך false, השמירה במטמון מתרחשת רק במהלך אירועי גלילה.
CacheWindow בהתאמה אישית
אם מרחקים קבועים או חלקי אזור תצוגה לא תואמים לדפוסי הפריסה שלכם, אתם יכולים להטמיע את הממשק LazyLayoutCacheWindow ישירות:
interface LazyLayoutCacheWindow { fun Density.calculateAheadWindow(viewport: Int): Int = 0 fun Density.calculateBehindWindow(viewport: Int): Int = 0 val isNonScrollCachingEnabled: Boolean get() = false }
מטמיעים את שיטות החישוב כדי להחזיר את גודל החלון המועדף בפיקסלים לגודל נתון של אזור התצוגה:
class ScaledCacheWindow( val someLayoutState: LayoutState ) : LazyLayoutCacheWindow { override fun Density.calculateAheadWindow(viewport: Int): Int { return someLayoutState.specialDynamicCalculation().coerceAtMost(viewport * 5) } override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is ScaledCacheWindow) return false return someLayoutState == other.someLayoutState } override fun hashCode(): Int { return someLayoutState.hashCode() } } @Composable fun ScaledCacheWindowSample() { val state = rememberLazyListState() val newCacheWindow = remember { ScaledCacheWindow(state) } LazyColumn( state = state, cacheWindow = newCacheWindow, ) { items(100) { index -> Text( text = "Item #$index", modifier = Modifier .fillMaxWidth() .padding(16.dp), ) } } }
מדריך להעברת נתונים (מיגרציה)
ממשקי ה-API הראשוניים להגדרת השליפה מראש (PrefetchStrategy) והגרסה הראשונה של LazyLayoutCacheWindow אפשרו להגדיר את השליפה מראש במצבים, אבל הפונקציונליות הזו הוצאה משימוש. בקטע הזה מוסבר איך לעבור מממשקי ה-API ומהפרמטרים הבאים שהוצאו משימוש:
- הפרמטרים של
cacheWindowשמבוססים על המצב ב-rememberLazyListStateוב-rememberLazyGridState. -
LazyListPrefetchStrategyו-LazyGridPrefetchStrategyעםLazyLayoutCacheWindow.
אפשרות 1: העברת cacheWindow ארגומנטים
כדי לעבור לממשקי ה-Recommender API המומלצים, צריך להעביר את הארגומנטים של cacheWindow מ-rememberLazyListState ומ-rememberLazyGridState כדי להשתמש בארגומנט של cacheWindow ב-LazyColumn, ב-LazyRow, ב-LazyVerticalGrid וב-LazyHorizontalGrid. ב-LazyVerticalStaggeredGrid וב-LazyHorizontalStaggeredGrid כבר מוטמעת הפרדיגמה החדשה ב-composable של הפריסה.
// Before (Experimental in Compose 1.9 to 1.12, now Deprecated) val state = rememberLazyListState( cacheWindow = LazyLayoutCacheWindow(aheadFraction = 0.5f, behindFraction = 0.2f) ) LazyColumn(state = state) { items(itemsList) { item -> Text("Item $item") } }
// Now in Compose 1.13 (Stable) val state = rememberLazyListState() LazyColumn( state = state, cacheWindow = LazyLayoutCacheWindow(aheadFraction = 0.5f, behindFraction = 0.2f) ) { items(itemsList) { item -> Text("Item $item") } }
אפשרות 2: החלפה של LazyListPrefetchStrategy ו-LazyGridPrefetchStrategy
מחליפים את השימושים בממשקי ה-API שהוצאו משימוש, LazyListPrefetchStrategy ו-LazyGridPrefetchStrategy, ב-LazyLayoutCacheWindow. באופן פנימי, כל התנהגות הטעינה מראש מסתמכת עכשיו על cacheWindow. השינוי הזה משנה את אופן הפעולה של האחזור מראש בתרחישים הבאים:
- פריטים קטנים: כשפריטים תופסים פחות מ-10% מאזור התצוגה, Compose מאחזר מראש כמה פריטים. כך אפשר לצמצם את הקפיצות בגלילה כשגוללים במהירות.
- פריטים גדולים: כשפריטים בודדים תופסים יותר מ-50% מאזור התצוגה, החלון המוגבל לא יכול לכסות פריט שלם שמופיע בהמשך. אם יש לכם פריטים גבוהים, כמו פידים של מדיה או שרשורי הודעות, יכול להיות שתבחינו בטריגרים מאוחרים של אחזור מראש, ותצטרכו לכוונן את חלון ה-ahead.
- אם האסטרטגיה המותאמת אישית שלכם התבססה על תזמון של כל פריט בנפרד, העבירו את הלוגיקה אל
LazyLayoutCacheWindow.calculateAheadWindow. כדי לשכפל התנהגות דומה של שליפה מראש של יצירה ומדידה של פריט אחד קדימה בכיוון הגלילה, צריך להטמיע חלון בהתאמה אישית שמבוסס על מדדי פריטים גלויים. שימו לב: למרות שההתנהגות הזו לא זהה, היא הכי קרובה שאפשר להגיע אליה כשמשתמשים באחזור מראש מבוסס-חלון:
val state = rememberLazyListState() val legacyCacheWindow = remember(state) { object : LazyLayoutCacheWindow { override fun Density.calculateAheadWindow(viewport: Int): Int { val visibleItems = state.layoutInfo.visibleItemsInfo if (visibleItems.isEmpty()) return 0 val averageItemHeight = visibleItems.sumOf { it.size } / visibleItems.size return averageItemHeight } } } LazyColumn( state = state, cacheWindow = legacyCacheWindow ) { items(itemsList) { item -> Text("Item $item") } }
ריפוד תוכן
לפעמים צריך להוסיף ריפוד מסביב לקצוות של התוכן. הרכיבים ה-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 לשמירת המצבים כשהפעילות נוצרת מחדש. Bundle תומך בסוגים כמו פרימיטיביים,
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מקצה מפתח ייחודי לכל פריט ברשימה. פיתוח נייטיב משתמש במפתחות כדי לעקוב אחרי הפריטים ולזהות שינויים במיקומים שלהם. 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הן פונקציות lambda שמופעלות כשבוחרים את הכפתור המתאים. - משתנה המצב
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 ואילך יש תמיכה ב-Compose דרך הספרייה androidx.paging:paging-compose.
כדי להציג רשימה של תוכן עם דפים, אנחנו יכולים להשתמש בפונקציית התוסף 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 פיקסלים והיא יכולה להתאים את כולם לאזור התצוגה. אחרי שהפריטים נטענים והגובה שלהם מתרחב, פריסות עם טעינה מדורגת מבטלות את כל הפריטים האחרים שנוצרו שלא לצורך בפעם הראשונה, כי הם לא יכולים להתאים לאזור התצוגה. כדי למנוע את זה, צריך להגדיר גודל ברירת מחדל לפריטים, כדי שהפריסה הדינמית תוכל לחשב בצורה נכונה כמה פריטים יכולים להיכנס לאזור התצוגה:
@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 { // ... } }
במקום זאת, אפשר להשיג את אותה תוצאה על ידי גלישת כל הרכיבים הניתנים ליצירה בתוך הורה אחד 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) } // ... }
פריסות עם טעינה מדורגת יטפלו בזה כצפוי – הן יפרסו את הרכיבים אחד אחרי השני כאילו היו פריטים שונים. אבל יש כמה בעיות עם זה.
כשכמה רכיבים נפלטים כחלק מפריט אחד, הם מטופלים כישות אחת, כלומר אי אפשר יותר להרכיב אותם בנפרד. אם רכיב אחד מופיע במסך, צריך ליצור ולמדוד את כל הרכיבים שתואמים לפריט. שימוש מוגזם בשיטה הזו עלול לפגוע בביצועים. במקרה קיצוני שבו כל הרכיבים נמצאים בפריט אחד, אין שום טעם להשתמש בפריסות Lazy. מלבד בעיות פוטנציאליות בביצועים, הוספת אלמנטים נוספים לפריט אחד תפריע גם ל-scrollToItem() ול-animateScrollToItem().
עם זאת, יש מקרים שבהם הגיוני להציב כמה רכיבים בפריט אחד, למשל כשרוצים להוסיף קווי הפרדה בתוך רשימה. אתם לא רוצים שהמפרידים ישנו את אינדקסי הגלילה, כי הם לא אמורים להיחשב כרכיבים עצמאיים. בנוסף, הביצועים לא מושפעים כי המחיצות קטנות. סביר להניח שצריך להציג קו מפריד כשהפריט שלפניו מוצג, כך שהוא יכול להיות חלק מהפריט הקודם:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Divider() } item { Item(2) } // ... }
כדאי להשתמש בסידורים בהתאמה אישית
בדרך כלל, רשימות בטעינה מדורגת מכילות פריטים רבים, והן תופסות יותר מהגודל של מאגר הגלילה. עם זאת, כשהרשימה מאוכלסת בכמה פריטים, העיצוב יכול לכלול דרישות ספציפיות יותר לגבי המיקום שלהם באזור התצוגה.
כדי לעשות את זה, אתם יכולים להשתמש בענף בהתאמה אישית
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 יכול לעשות שימוש חוזר בקומפוזיציות רק בין פריטים מאותו סוג. שימוש חוזר יעיל יותר כשיוצרים פריטים עם מבנה דומה, ולכן כשמציינים את סוגי התוכן, Compose לא מנסה ליצור פריט מסוג א' על גבי פריט שונה לחלוטין מסוג ב'. כך אפשר למקסם את היתרונות של שימוש חוזר בקומפוזיציה ולשפר את הביצועים של פריסת Lazy.
מדידת הביצועים
אפשר למדוד באופן מהימן את הביצועים של פריסה עם טעינה מדורגת רק כשמפעילים את מצב ההפצה ואת האופטימיזציה של R8. בגרסאות build לניפוי באגים, יכול להיות שהגלילה בפריסה עם טעינה מדורגת תהיה איטית יותר. מידע נוסף זמין במאמר ביצועים של Compose.
מקורות מידע נוספים
- יצירת רשימה סופית שאפשר לגלול בה
- יצירת רשת עם גלילה
- הצגת פריטים בגלילה מקוננת ברשימה
- סינון רשימה תוך כדי הקלדה
- טעינה מדורגת של נתונים באמצעות רשימות ועימוד
- יצירת רשימה עם כמה סוגי פריטים
- סרטון: רשימות ב-Compose
מומלץ בשבילכם
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- העברת
RecyclerViewלרשימה עם טעינה מדורגת - שמירת מצב ממשק המשתמש בפיתוח נייטיב
- Kotlin ל-Jetpack Compose