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

بسیاری‌از برنامه‌ها باید مجموعه‌هایی از موارد را نمایش دهند. این سند توضیح می‌دهد که چگونه می‌توانید این کار را به‌طور مؤثر در 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، که به برنامه‌ها اجازه می‌دهد مستقیماً ترکیب‌پذیرها را منتشر کنند، مؤلفه‌های 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 vertical grid موارد خود را در یک محفظه قابل‌پیمایش عمودی نمایش می‌دهد که در چندین ستون گسترده شده است، درحالی‌که Lazy horizontal grids همان رفتار را در محور افقی خواهند داشت.

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

حاشیه داخلی محتوا

گاهی اوقات لازم است در اطراف لبه‌های محتوا حاشیه اضافه کنید. مؤلفه‌های تنبل به شما امکان می‌دهند تا برای پشتیبانی از این مورد، مقداری 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 پشتیبانی شود، مکانیزم 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 استفاده کرده باشید، می‌دانید که تغییرات مورد را به‌طور خودکار پویانمایی می‌کند. چیدمان‌های Lazy همین عملکرد را برای مرتب‌سازی مجدد موارد ارائه می‌دهند. این میانای برنامه‌سازی کاربردی ساده است - فقط باید اصلاح‌کننده 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 دکمه‌های تغییر ترتیب فهرست را نمایش می‌دهد. این تابع توابع لامبدا را برای بازنشانی ترتیب و مرتب‌سازی فهرست براساس طول یا حروف الفبا دریافت می‌کند.
  • ‫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 را نمایش می‌دهد تا کاربران بتوانند روش مرتب‌سازی را در فهرست انتخاب کنند یا ترتیب فهرست را بازنشانی کنند. مؤلفه SegmentedButton به شما امکان می‌دهد یک گزینه را از فهرست گزینه‌ها انتخاب کنید.
  • ‫resetOrder، orderAlphabetically، و orderByLength توابع لامبدا هستند که وقتی دکمه مربوطه انتخاب می‌شود اجرا می‌شوند.
  • متغیر حالت selectedIndex گزینه انتخاب‌شده را ردیابی می‌کند.

نتیجه

این ویدیو نتیجه قطعه‌های قبلی را هنگام مرتب‌سازی مجدد موارد نشان می‌دهد:

شکل ۱. فهرستی که هنگام اضافه شدن، برداشته شدن، یا مرتب شدن موارد، انتقال‌های مورد را پویانمایی می‌کند.

سرصفحه‌های چسبان

الگوی «سرصفحه چسبان» هنگام نمایش فهرست‌های داده‌های گروه‌بندی‌شده مفید است. در زیر می‌توانید نمونه‌ای از «فهرست مخاطبین» را ببینید که براساس حرف اول هر مخاطب گروه‌بندی شده است:

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

برای دستیابی به سرایند چسبان با LazyColumn، می‌توانید از stickyHeader() استفاده کنید

تابع، ارائه محتوای سرایند:

@Composable
fun ListWithHeader(items: List<Item>) {
    LazyColumn {
        stickyHeader {
            Header()
        }

        items(items) { item ->
            ItemRow(item)
        }
    }
}

برای دستیابی به فهرستی با چند سرایند، مانند مثال «فهرست مخاطبین» در بالا، می‌توانید این کار را انجام دهید:

// This ideally would be done in the ViewModel
val grouped = contacts.groupBy { it.firstName[0] }

@Composable
fun ContactsList(grouped: Map<Char, List<Contact>>) {
    LazyColumn {
        grouped.forEach { (initial, contactsForInitial) ->
            stickyHeader {
                CharacterHeader(initial)
            }

            items(contactsForInitial) { contact ->
                ContactListItem(contact)
            }
        }
    }
}

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

بسیاری از برنامه‌ها باید به تغییرات موقعیت پیمایش و چیدمان موارد واکنش نشان دهند و آن‌ها را گوش دهند. مؤلفه‌های Lazy با بالا بردن LazyListState از این مورد استفاده پشتیبانی می‌کنند:

@Composable
fun MessageList(messages: List<Message>) {
    // Remember our own LazyListState
    val listState = rememberLazyListState()

    // Provide it to LazyColumn
    LazyColumn(state = listState) {
        // ...
    }
}

برای موارد استفاده ساده، برنامه‌ها معمولاً فقط باید اطلاعات مربوط به اولین مورد نمایان را بدانند. برای این LazyListState ویژگی‌های firstVisibleItemIndex و firstVisibleItemScrollOffset را ارائه می‌دهد.

اگر از مثال نشان دادن و پنهان کردن دکمه براساس اینکه کاربر از اولین مورد رد شده است یا نه استفاده کنیم:

@Composable
fun MessageList(messages: List<Message>) {
    Box {
        val listState = rememberLazyListState()

        LazyColumn(state = listState) {
            // ...
        }

        // Show the button if the first visible item is past
        // the first item. We use a remembered derived state to
        // minimize unnecessary compositions
        val showButton by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex > 0
            }
        }

        AnimatedVisibility(visible = showButton) {
            ScrollToTopButton()
        }
    }
}

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

کنترل کردن موقعیت پیمایش

As well as reacting to scroll position, it’s also useful for apps to be able to control the scroll position too. 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 و نسخه‌های جدیدتر ازطریق کتابخانه androidx.paging:paging-compose از 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

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

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

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

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

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

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

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

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

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

سنجش عملکرد

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

منابع بیشتر