فهرست‌های تنبل و جدول‌های تنبل

بسیاری از برنامه‌ها باید مجموعه‌هایی از موارد را نمایش دهند. این سند توضیح می‌دهد که چگونه می‌توانید این کار را به‌طور کارآمد در 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، که به برنامه‌ها اجازه می‌دهد مستقیماً عناصر ترکیبی را منتشر کنند، عناصر «تنبل» بلوک LazyListScope.() را ارائه می‌دهند. این LazyListScope بلوک «زبان توصیفی داده» (DSL) ارائه می‌دهد که به برنامه‌ها اجازه می‌دهد محتوای مورد را توصیف کنند. سپس عنصر «تنبل» مسئول افزودن محتوای هر مورد براساس الزامات طرح‌بندی و موقعیت پیمایش است.

‫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 مرجع مراجعه کنید.

جدول‌های تنبل

ترکیب‌پذیرهای 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.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()
)
جدول شطرنجی تنبل تصویرها در «نوشتن»
شکل ۲. مثالی از شبکه عمودی با ستون‌های ثابت

حاشیه محتوا

گاهی اوقات باید در اطراف لبه‌های محتوا حاشیه اضافه کنید. عناصر تنبل به شما اجازه می‌دهند برخی‌از 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 از انواع مختلفی مثل ابتدایی‌ها، شمارش‌ها، یا Parcelable پشتیبانی می‌کند.

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

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

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

پویانمایی‌های مورد

اگر از ابزارک RecyclerView استفاده کرده باشید، می‌دانید که به‌طور خودکار تغییرات مورد را پویانمایی می‌کند. چیدمان‌های تنبل همان عملکرد را برای تغییر ترتیب موارد ارائه می‌دهند. این API ساده است - فقط باید animateItem اصلاح‌کننده را روی محتوای مورد تنظیم کنید:

LazyColumn {
    // It is important to provide a key to each item to ensure animateItem() works as expected.
    items(books, key = { it.id }) {
        Row(Modifier.animateItem()) {
            // ...
        }
    }
}

درصورت نیاز، حتی می‌توانید مشخصات پویانمایی سفارشی ارائه دهید:

LazyColumn {
    items(books, key = { it.id }) {
        Row(
            Modifier.animateItem(
                fadeInSpec = tween(durationMillis = 250),
                fadeOutSpec = tween(durationMillis = 100),
                placementSpec = spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)
            )
        ) {
            // ...
        }
    }
}

مطمئن شوید که کلیدهایی برای عناصرتان ارائه می‌کنید تا بتوان موقعیت جدید عنصر منتقل‌شده را پیدا کرد.

مثال: پویانمایی موارد در فهرست‌های تنبل

با «ترکیب»، می‌توانید تغییرات عناصر در فهرست‌های تنبل را پویانمایی کنید. وقتی با هم استفاده شوند، گلچین‌های زیر هنگام افزودن، برداشتن، و تغییر ترتیب عناصر فهرست تنبل، پویانمایی‌ها را پیاده‌سازی می‌کنند.

این گلچین فهرستی از رشته‌ها را با گذارهای پویانمایی‌شده هنگام افزوده شدن، برداشته شدن، یا تغییر ترتیب موارد نمایش می‌دهد:

@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 نادرست باشد، دکمه مربوطه غیرفعال می‌شود.
  • پارامترهای 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 را نمایش می‌دهد تا به کاربران امکان دهد روش مرتب‌سازی را در فهرست انتخاب کنند یا ترتیب فهرست را بازنشانی کنند. عنصر A 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)
            }
        }
    }
}

واکنش به موقعیت پیمایش

بسیاری از برنامه‌ها باید به تغییرات موقعیت پیمایش و چیدمان مورد واکنش نشان دهند و به آن‌ها گوش دهند. عناصر «تنبل» با بالا بردن 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()
        }
    }
}

خواندن مستقیم وضعیت در ترکیب زمانی مفید است که نیاز دارید ترکیب‌های دیگر واسط کاربر را به‌روز کنید، اما سناریوهایی هم وجود دارد که رویداد نیازی به مدیریت شدن در همان ترکیب ندارد. نمونه رایج این مورد ارسال رویداد 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)
            }
        }
    )
}

مجموعه‌های داده بزرگ (صفحه‌بندی)

کتابخانه صفحه‌بندی به برنامه‌ها امکان می‌دهد از فهرست‌های بزرگ موارد پشتیبانی کنند و بخش‌های کوچک فهرست را درصورت نیاز بار و نمایش دهند. «صفحه‌بندی ۳.۰» و نسخه‌های جدیدتر ازطریق کتابخانه 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()
            }
        }
    }
}

نکته‌هایی درباره استفاده از چیدمان‌های تنبل

چند نکته وجود دارد که می‌توانید درنظر بگیرید تا مطمئن شوید چیدمان‌های «تنبل» شما همان‌طور که درنظر گرفته شده است کار می‌کنند.

از استفاده از عناصر با اندازه ۰ پیکسل خودداری کنید

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

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

مراقب باشید که چند عنصر را در یک مورد قرار ندهید

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

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 را پیاده‌سازی کند. اولاً، موارد را یکی پس‌از دیگری قرار می‌دهد. دوم، اگر ارتفاع کل استفاده‌شده کمتر از ارتفاع درگاه دید باشد، پانوشت را در پایین قرار می‌دهد:

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 اضافه کنید

از «نوشتن» نسخه ۱.۲، برای بیشینه کردن عملکرد چیدمان Lazy، contentType را به فهرست‌ها یا شبکه‌هایتان اضافه کنید. این امکان را به شما می‌دهد تا نوع محتوا را برای هر مورد از چیدمان مشخص کنید، در مواردی که فهرستی یا شبکه‌ای متشکل از چندین نوع مختلف از موارد را می‌سازید:

LazyColumn {
    items(elements, contentType = { it.type }) {
        // ...
    }
}

وقتی contentType را ارائه می‌دهید، «نگارش» فقط می‌تواند از قطعه‌های موسیقی بین موارد از یک نوع استفاده مجدد کند. ازآنجایی‌که وقتی عناصر با ساختار مشابه را می‌نویسید، استفاده مجدد کارآمدتر است، ارائه انواع محتوا تضمین می‌کند «نوشتن» سعی نمی‌کند عنصر نوع A را روی عنصر کاملاً متفاوتی از نوع B بنویسد. این کار به حداکثر رساندن مزایای ترکیب استفاده مجدد و عملکرد چیدمان تنبل شما کمک می‌کند.

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

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

منابع بیشتر