فهرست ها و شبکه ها

بسیاری از برنامه‌ها نیاز به نمایش مجموعه‌ای از آیتم‌ها دارند. این سند توضیح می‌دهد که چگونه می‌توانید این کار را به طور موثر در Jetpack Compose انجام دهید.

اگر می‌دانید که مورد استفاده شما نیازی به پیمایش ندارد، می‌توانید از یک Column یا Row ساده (بسته به جهت) استفاده کنید و محتوای هر آیتم را با پیمایش روی یک لیست به روش زیر منتشر کنید:

@Composable
fun MessageList(messages: List<Message>) {
    Column {
        messages.forEach { message ->
            MessageRow(message)
        }
    }
}

ما می‌توانیم با استفاده از اصلاح‌کننده verticalScroll() Column را قابل پیمایش کنیم.

لیست‌های تنبل

اگر نیاز به نمایش تعداد زیادی آیتم (یا لیستی با طول نامشخص) دارید، استفاده از چیدمانی مانند Column می‌تواند باعث مشکلات عملکردی شود، زیرا همه آیتم‌ها چه قابل مشاهده باشند و چه نباشند، ترکیب و چیدمان می‌شوند.

Compose مجموعه‌ای از کامپوننت‌ها را فراهم می‌کند که فقط مواردی را که در نمای کامپوننت قابل مشاهده هستند، ترکیب و طرح‌بندی می‌کنند. این کامپوننت‌ها شامل LazyColumn و LazyRow هستند.

همانطور که از نامش پیداست، تفاوت بین LazyColumn و LazyRow در جهت قرارگیری آیتم‌ها و اسکرول کردن آنهاست. LazyColumn یک لیست با اسکرول عمودی و LazyRow یک لیست با اسکرول افقی تولید می‌کنند.

کامپوننت‌های Lazy با اکثر طرح‌بندی‌ها در Compose متفاوت هستند. به جای پذیرش پارامتر بلوک محتوای @Composable که به برنامه‌ها اجازه می‌دهد مستقیماً 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 از نمایش آیتم‌ها در یک شبکه پشتیبانی می‌کنند. یک شبکه عمودی Lazy آیتم‌های خود را در یک کانتینر با قابلیت پیمایش عمودی، که در چندین ستون گسترده شده است، نمایش می‌دهد، در حالی که شبکه‌های افقی Lazy رفتار مشابهی در محور افقی خواهند داشت.

گریدها همان قابلیت‌های قدرتمند 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 از Composableهایی هستند که به شما امکان می‌دهند یک شبکه‌ی پلکانی و بارگذاری‌شده با lazy load از آیتم‌ها ایجاد کنید. یک شبکه‌ی پلکانی عمودی lazy آیتم‌های خود را در یک کانتینر با قابلیت پیمایش عمودی نمایش می‌دهد که در چندین ستون قرار می‌گیرد و به آیتم‌های منفرد اجازه می‌دهد تا ارتفاع‌های متفاوتی داشته باشند. شبکه‌های افقی lazy در محور افقی با آیتم‌هایی با عرض‌های مختلف، رفتار یکسانی دارند.

قطعه کد زیر یک مثال ساده از استفاده از 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.Adaptive از StaggeredGridCells.Adaptive StaggeredGridCells.Fixed(columns) استفاده کنید. این تابع عرض موجود را بر تعداد ستون‌ها (یا ردیف‌ها برای یک شبکه افقی) تقسیم می‌کند و هر آیتم آن عرض (یا ارتفاع برای یک شبکه افقی) را اشغال می‌کند:

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 staggered grid of images in Compose
شکل ۲. نمونه‌ای از شبکه عمودی پلکانی تنبل با ستون‌های ثابت

پر کردن محتوا

گاهی اوقات نیاز دارید که اطراف لبه‌های محتوا فاصله (padding) اضافه کنید. کامپوننت‌های 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 کمک می‌کنید تا مرتب‌سازی‌های مجدد را به درستی مدیریت کند. برای مثال، اگر آیتم شما حاوی حالت ذخیره شده باشد، تنظیم کلیدها به Compose اجازه می‌دهد تا این حالت را همراه با آیتم، هنگامی که موقعیت آن تغییر می‌کند، جابجا کند.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = remember {
            Random.nextInt()
        }
    }
}

با این حال، یک محدودیت در مورد نوع داده‌هایی که می‌توانید به عنوان کلید آیتم استفاده کنید وجود دارد. نوع کلید باید توسط Bundle ، مکانیزم اندروید برای نگهداری حالت‌ها هنگام بازسازی Activity، پشتیبانی شود. Bundle از انواع داده‌هایی مانند primitiveها، enumها یا Parcelableها پشتیبانی می‌کند.

LazyColumn {
    items(books, key = {
        // primitives, enums, Parcelable, etc.
    }) {
        // ...
    }
}

این کلید باید توسط Bundle پشتیبانی شود تا rememberSaveable درون آیتم composable بتواند هنگام ایجاد مجدد Activity یا حتی زمانی که از این آیتم خارج می‌شوید و به عقب برمی‌گردید، بازیابی شود.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = rememberSaveable {
            Random.nextInt()
        }
    }
}

انیمیشن‌های آیتم

اگر از ویجت RecyclerView استفاده کرده باشید، می‌دانید که تغییرات آیتم‌ها را به صورت خودکار متحرک می‌کند . Lazy layouts نیز همین عملکرد را برای تغییر ترتیب آیتم‌ها ارائه می‌دهند. 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، می‌توانید تغییرات روی آیتم‌ها در لیست‌های lazy را متحرک‌سازی کنید. قطعه کدهای زیر وقتی با هم استفاده شوند، هنگام اضافه کردن، حذف کردن و مرتب‌سازی مجدد آیتم‌های لیست lazy، انیمیشن‌هایی را پیاده‌سازی می‌کنند.

این قطعه کد لیستی از رشته‌ها را با انتقال‌های انیمیشنی هنگام اضافه شدن، حذف شدن یا تغییر ترتیب آیتم‌ها نمایش می‌دهد:

@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 دکمه‌های مربوط به مرتب‌سازی مجدد لیست را نمایش می‌دهد. این تابع، توابع لامبدا را برای تنظیم مجدد ترتیب و مرتب‌سازی لیست بر اساس طول یا حروف الفبا دریافت می‌کند.
  • ListAnimatedItems ListAnimatedItems composable را فراخوانی می‌کند و لیست 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 لامبداهایی هستند که وقتی کاربر روی دکمه مربوطه کلیک می‌کند، اجرا می‌شوند.

در نهایت، این قطعه کد سه دکمه برای مرتب‌سازی لیست نمایش می‌دهد ( 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 نمایش می‌دهد تا به کاربران اجازه دهد یک روش مرتب‌سازی را در لیست انتخاب کنند یا ترتیب لیست را مجدداً تنظیم کنند. یک کامپوننت SegmentedButton به شما امکان می‌دهد یک گزینه واحد را از لیستی از گزینه‌ها انتخاب کنید.
  • توابع lambda 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()
        }
    }
}

خواندن مستقیم وضعیت در کامپوزیشن زمانی مفید است که نیاز به به‌روزرسانی سایر کامپوزیشن‌های رابط کاربری داشته باشید، اما سناریوهایی نیز وجود دارد که نیازی به مدیریت رویداد در همان کامپوزیشن نیست. یک مثال رایج از این مورد، ارسال یک رویداد تحلیلی پس از عبور کاربر از یک نقطه خاص است. برای مدیریت کارآمد این مورد، می‌توانیم از 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 به برنامه‌ها این امکان را می‌دهد که از لیست‌های بزرگی از آیتم‌ها پشتیبانی کنند و در صورت لزوم، بخش‌های کوچکی از لیست را بارگذاری و نمایش دهند. Paging نسخه ۳.۰ و بالاتر، از طریق کتابخانه androidx.paging:paging-compose پشتیبانی از Compose را ارائه می‌دهد.

برای نمایش لیستی از محتوای صفحه‌بندی شده، می‌توانیم از تابع افزونه collectAsLazyPagingItems() استفاده کنیم و سپس LazyPagingItems برگردانده شده را به items() در LazyColumn خود منتقل کنیم. مشابه پشتیبانی از صفحه‌بندی در نماها، می‌توانید با بررسی null بودن item ، placeholderها را هنگام بارگذاری داده‌ها نمایش دهید:

@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 Lay

چند نکته وجود دارد که می‌توانید برای اطمینان از عملکرد Lazy layouts خود طبق انتظار، در نظر بگیرید.

از استفاده از آیتم‌های با اندازه ۰ پیکسل خودداری کنید

این اتفاق می‌تواند در سناریوهایی رخ دهد که برای مثال، انتظار دارید به صورت ناهمگام برخی داده‌ها مانند تصاویر را بازیابی کنید تا موارد لیست خود را در مرحله بعدی پر کنید. این باعث می‌شود Lazy layout تمام موارد خود را در اولین اندازه‌گیری ترکیب کند، زیرا ارتفاع آنها 0 پیکسل است و می‌تواند همه آنها را در viewport جا دهد. پس از بارگیری موارد و افزایش ارتفاع آنها، Lazy layouts سپس تمام موارد دیگری را که بار اول به طور غیرضروری ترکیب شده‌اند، دور می‌ریزد زیرا در واقع نمی‌توانند در viewport جا شوند. برای جلوگیری از این امر، باید اندازه پیش‌فرض را برای موارد خود تنظیم کنید تا Lazy layout بتواند محاسبه صحیحی از تعداد موارد قابل جایگیری در viewport انجام دهد:

@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 {
        // ...
    }
}

در عوض، می‌توان با قرار دادن تمام composable های خود درون یک 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)
    ) {
        // ...
    }
}

از قرار دادن چندین عنصر در یک آیتم خودداری کنید

در این مثال، آیتم دوم لامبدا، دو آیتم را در یک بلوک منتشر می‌کند:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Item(2)
    }
    item { Item(3) }
    // ...
}

طرح‌بندی‌های تنبل این مشکل را همانطور که انتظار می‌رود، برطرف می‌کنند - آنها عناصر را یکی پس از دیگری طوری قرار می‌دهند که انگار آیتم‌های متفاوتی هستند. با این حال، انجام این کار چند مشکل دارد.

وقتی چندین عنصر به عنوان بخشی از یک آیتم منتشر می‌شوند، به عنوان یک موجودیت واحد مدیریت می‌شوند، به این معنی که دیگر نمی‌توان آنها را به صورت جداگانه ترکیب کرد. اگر یک عنصر روی صفحه قابل مشاهده باشد، تمام عناصر مربوط به آن آیتم باید ترکیب و اندازه‌گیری شوند. این امر در صورت استفاده بیش از حد می‌تواند به عملکرد آسیب برساند. در حالت افراطی، قرار دادن همه عناصر در یک آیتم، هدف استفاده از طرح‌بندی‌های تنبل را کاملاً از بین می‌برد. جدا از مشکلات عملکردی بالقوه، قرار دادن عناصر بیشتر در یک آیتم با scrollToItem() و animateScrollToItem() نیز تداخل خواهد داشت.

با این حال، موارد استفاده معتبری برای قرار دادن چندین عنصر در یک آیتم وجود دارد، مانند داشتن جداکننده‌ها در داخل یک لیست. شما نمی‌خواهید جداکننده‌ها شاخص‌های پیمایش را تغییر دهند، زیرا نباید به عنوان عناصر مستقل در نظر گرفته شوند. همچنین، عملکرد تحت تأثیر قرار نمی‌گیرد زیرا جداکننده‌ها کوچک هستند. احتمالاً یک جداکننده باید زمانی که آیتم قبل از آن قابل مشاهده است، قابل مشاهده باشد، بنابراین می‌تواند بخشی از آیتم قبلی باشد:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Divider()
    }
    item { Item(2) }
    // ...
}

استفاده از تنظیمات سفارشی را در نظر بگیرید

معمولاً لیست‌های تنبل آیتم‌های زیادی دارند و فضای بیشتری از فضای قابل پیمایش را اشغال می‌کنند. با این حال، وقتی لیست شما با آیتم‌های کمی پر شده باشد، طراحی شما می‌تواند الزامات خاص‌تری برای نحوه قرارگیری این آیتم‌ها در نمای دید داشته باشد.

برای دستیابی به این هدف، می‌توانید از Arrangement عمودی سفارشی استفاده کنید و آن را به LazyColumn منتقل کنید. در مثال زیر، شیء TopWithFooter فقط باید متد arrange را پیاده‌سازی کند. اولاً، آیتم‌ها را یکی پس از دیگری قرار می‌دهد. ثانیاً، اگر ارتفاع کل استفاده شده کمتر از ارتفاع viewport باشد، پاورقی را در پایین قرار می‌دهد:

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 layout شما کمک می‌کند.

اندازه‌گیری عملکرد

شما فقط می‌توانید عملکرد یک طرح‌بندی Lazy را هنگام اجرا در حالت انتشار و با فعال بودن بهینه‌سازی R8 به طور قابل اعتمادی اندازه‌گیری کنید. در نسخه‌های اشکال‌زدایی، پیمایش طرح‌بندی Lazy ممکن است کندتر به نظر برسد. برای اطلاعات بیشتر در این مورد، عملکرد Compose را مطالعه کنید.

منابع اضافی

{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}