목록 및 그리드

많은 앱에서 항목의 컬렉션을 표시해야 합니다. 이 문서에서는 Jetpack Compose에서 이 작업을 효율적으로 처리하는 방법을 설명합니다.

스크롤이 필요하지 않은 경우 (방향에 따라) 간단한 Column 또는 Row를 사용하여 다음과 같이 목록을 반복하여 각 항목의 콘텐츠를 내보낼 수 있습니다.

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

verticalScroll() 수정자를 사용하여 Column을 스크롤 가능하게 만들 수 있습니다.

지연 목록

많은 수의 항목이나 길이를 알 수 없는 목록을 표시해야 하는 경우 Column과 같은 레이아웃을 사용하면 모든 항목이 표시 가능 여부와 관계없이 구성되고 배치되므로 성능 문제가 발생할 수 있습니다.

Compose는 구성요소의 표시 영역에 표시되는 항목만 구성하여 배치하는 구성요소 집합을 제공합니다. 이러한 구성요소에는 LazyColumnLazyRow가 포함됩니다.

이름에서 알 수 있듯이 LazyColumnLazyRow의 차이점은 항목을 배치하고 스크롤하는 방향입니다. LazyColumn은 세로로 스크롤되는 목록을 생성하고 LazyRow는 가로로 스크롤되는 목록을 생성합니다.

지연 구성요소는 대부분의 Compose 레이아웃과 다릅니다. 지연 구성요소는 @Composable 콘텐츠 블록 구성요소를 수락하고 앱에서 직접 컴포저블을 내보낼 수 있도록 허용하는 대신 LazyListScope.() 블록을 제공합니다. 이 LazyListScope 블록은 앱에서 항목 콘텐츠를 설명할 수 있는 DSL을 제공합니다. 그런 다음 지연 구성요소가 레이아웃 및 스크롤 위치에 따라 각 항목의 콘텐츠를 추가합니다.

LazyListScope DSL

LazyListScope의 DSL은 레이아웃의 항목을 설명하는 여러 함수를 제공합니다. 가장 기본적인 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)
    }
}

색인을 제공하는 itemsIndexed()라고 하는 items() 확장 함수의 버전도 있습니다. 자세한 내용은 LazyListScope 참조를 확인하세요.

지연 그리드

LazyVerticalGridLazyHorizontalGrid 컴포저블은 그리드로 항목 표시를 지원합니다. 지연 세로 그리드는 여러 열에 걸쳐 세로로 스크롤 가능한 컨테이너에 항목을 표시하는 반면, 지연 가로 그리드는 가로축을 중심으로 동일하게 동작합니다.

그리드는 목록과 동일한 강력한 API 기능을 가지며 콘텐츠를 설명하기 위한 매우 유사한 DSL(LazyGridScope.())을 사용합니다.

사진 그리드를 보여주는 휴대전화의 스크린샷

LazyVerticalGridcolumns 매개변수와 LazyHorizontalGridrows 매개변수는 셀이 열이나 행으로 형성되는 방식을 제어합니다. 다음 예에서는 항목을 그리드로 표시하고 GridCells.Adaptive를 사용하여 각 열의 너비를 128.dp 이상으로 설정합니다.

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 128.dp)
) {
    items(photos) { photo ->
        PhotoItem(photo)
    }
}

LazyVerticalGrid를 사용하면 항목의 너비를 지정할 수 있고 그러면 그리드는 가능한 한 많은 열에 맞습니다. 남은 너비는 열 수가 계산된 후 열 간에 균등하게 분배됩니다. 이러한 적응형 크기 조절 방법은 다양한 화면 크기에서 항목 집합을 표시하는 데 특히 유용합니다.

사용할 열의 정확한 수를 알고 있는 경우 대신 인스턴스 GridCells.Fixed 드림 필수 열의 개수로 포함됩니다.

디자인에서 특정 항목만 비표준 측정기준이 있어야 하는 경우 그리드 지원을 사용하여 항목에 맞춤 열 스팬을 제공할 수 있습니다. LazyGridScope DSL itemitems 메서드의 span 매개변수를 사용하여 열 스팬을 지정합니다. 스팬 범위의 값 중 하나인 maxLineSpan은 적응형 크기 조절을 사용할 때 특히 유용합니다. 열 수가 고정되어 있지 않기 때문입니다. 다음 예는 전체 행 스팬을 제공하는 방법을 보여줍니다.

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 30.dp)
) {
    item(span = {
        // LazyGridItemSpanScope:
        // maxLineSpan
        GridItemSpan(maxLineSpan)
    }) {
        CategoryCard("Fruits")
    }
    // ...
}

지연 시차 그리드

LazyVerticalStaggeredGrid 드림 및 LazyHorizontalStaggeredGrid 는 항목이 지연 로드되고 시차를 두고 이동하는 그리드를 만들 수 있는 컴포저블입니다. 지연 세로 지그재그형 그리드는 여러 열에 걸쳐 세로로 스크롤 가능한 컨테이너에 항목을 표시하고 개별 항목의 높이가 다를 수 있도록 허용합니다. 지연 가로 그리드는 너비가 서로 다른 항목이 있는 가로축입니다.

다음 스니펫은 항목당 200.dp 너비로 LazyVerticalStaggeredGrid를 사용하는 기본 예입니다.

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()
)

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
그림 1. 지연된 비슷한 간격의 세로 그리드의 예

고정된 개수의 열을 설정하려면 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()
)
드림 <ph type="x-smartling-placeholder">
</ph> Compose의 지연 시차 이미지 그리드
그림 2. 고정된 열이 있는 지연된 비슷한 간격의 세로 그리드 예시

콘텐츠 패딩

콘텐츠 가장자리 주변에 패딩을 추가해야 하는 경우가 있습니다. 지연 구성요소를 사용하면 일부 PaddingValuescontentPadding 매개변수에 전달하여 이 작업을 지원할 수 있습니다.

LazyColumn(
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
) {
    // ...
}

이 예에서는 가로 가장자리(왼쪽 및 오른쪽)에 16.dp의 패딩을 추가한 다음 콘텐츠의 상단과 하단에 8.dp의 패딩을 추가합니다.

이 패딩은 LazyColumn 자체가 아니라 콘텐츠에 적용됩니다. 위의 예에서 첫 번째 항목이 상단에 8.dp 패딩을 추가하고 마지막 항목이 하단에 8.dp를 추가하며 모든 항목의 왼쪽과 오른쪽에 16.dp 패딩이 추가됩니다

콘텐츠 간격

항목 사이에 간격을 추가하려면 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)
    }
}

항목 키

기본적으로 각 항목의 상태는 목록이나 그리드에 있는 항목의 위치를 기준으로 키가 지정됩니다. 하지만 이 경우 위치를 효율적으로 변경하는 항목에 상태가 저장되지 않아 데이터 세트가 변경되면 문제가 발생할 수 있습니다. LazyColumnLazyRow 시나리오의 경우 행에서 항목 위치가 변경되면 사용자가 행 내에서 스크롤 위치를 잃게 됩니다.

이 문제를 해결하려면 각 항목에 안정적이고 고유한 키를 제공하여 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()
        }
    }
}

하지만 항목 키로 사용할 수 있는 유형에는 한 가지 제한사항이 있습니다. 키 유형은 Activity가 다시 생성될 때 상태를 유지하는 Android의 메커니즘인 Bundle에서 지원해야 합니다. Bundle은 프리미티브, enum, Parcelable과 같은 유형을 지원합니다.

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

Activity가 다시 생성될 때 또는 이 항목에서 스크롤하여 벗어났다가 다시 스크롤하여 돌아올 때도 항목 컴포저블 내의 rememberSaveable을 복원할 수 있도록 키를 Bundle에서 지원해야 합니다.

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

항목 애니메이션

RecyclerView 위젯을 사용하는 경우 항목 변경사항이 자동으로 애니메이션 처리됩니다. 지연 레이아웃은 항목 재정렬에 동일한 기능을 제공합니다. API는 간단합니다. animateItemPlacement 수정자를 항목 콘텐츠로 설정하기만 하면 됩니다.

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

이동한 요소의 새 위치를 찾을 수 있도록 항목의 키를 제공해야 합니다.

재정렬 외에 추가 및 삭제를 위한 항목 애니메이션은 현재 개발 중입니다. 문제 150812265에서 진행 상황을 추적할 수 있습니다.

고정 헤더(실험용)

'고정 헤더' 패턴은 그룹화된 데이터 목록을 표시할 때 유용합니다. 다음은 각 연락처의 이니셜별로 그룹화된 '연락처 목록'의 예입니다.

연락처 목록을 위아래로 스크롤하는 휴대전화의 동영상

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)
            }
        }
    }
}

스크롤 위치에 반응

많은 앱이 스크롤 위치와 항목 레이아웃 변경사항에 반응하고 이를 수신 대기해야 합니다. 지연 구성요소는 LazyListState를 호이스팅하여 이 사용 사례를 지원합니다.

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

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

간단한 사용 사례의 경우 앱에서 일반적으로 첫 번째로 표시되는 항목에 관한 정보만 알면 됩니다. 이를 위해 LazyListStatefirstVisibleItemIndexfirstVisibleItemScrollOffset 속성을 제공합니다.

사용자가 첫 번째 항목을 지나 스크롤했는지 여부에 따라 버튼을 표시하고 숨기는 예를 사용하는 경우:

@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 컴포저블을 업데이트해야 할 때 유용하지만 동일한 컴퍼지션에서 이벤트를 처리할 필요가 없는 시나리오도 있습니다. 이 시나리오의 일반적인 예는 사용자가 특정 지점을 지나 스크롤한 후 분석 이벤트를 보내는 것입니다. 이 시나리오를 효율적으로 처리하기 위해 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 3.0 이상에서는 androidx.paging:paging-compose 라이브러리를 통해 Compose 지원 기능을 제공합니다.

페이징된 콘텐츠 목록을 표시하려면 collectAsLazyPagingItems() 확장 함수를 사용한 다음 반환된 LazyPagingItemsLazyColumnitems()에 전달하면 됩니다. 뷰의 Paging 지원 기능과 마찬가지로 itemnull인지 확인하여 데이터가 로드되는 동안 자리표시자를 표시할 수 있습니다.

@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()
            }
        }
    }
}

지연 레이아웃 사용 관련 도움말

지연 레이아웃이 의도한 대로 작동하도록 하기 위해 고려할 수 있는 몇 가지 도움말이 있습니다.

크기가 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) }
    // ...
}

지연 레이아웃은 예상대로 이를 처리합니다. 마치 다른 항목인 것처럼 요소를 차례로 배치합니다. 그러나 여기에는 몇 가지 문제가 있습니다.

여러 요소가 한 항목의 일부로 내보내지면 한 항목으로 처리되므로 더 이상 개별적으로 구성할 수 없습니다. 한 요소가 화면에 표시되면 이 항목에 해당하는 모든 요소를 구성하고 측정해야 합니다. 과도하게 사용되면 성능이 저하될 수 있습니다. 모든 요소를 한 항목에 배치하는 극단적인 경우에는 지연 레이아웃 사용의 목적이 완전히 무효화됩니다. 잠재적인 성능 문제 외에도 한 항목에 더 많은 요소를 넣으면 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을 추가하는 것이 좋습니다

Compose 1.2부터 지연 레이아웃의 성능을 극대화하려면 목록 또는 그리드에 contentType을 추가하는 것이 좋습니다. 이를 통해 여러 항목 유형으로 구성된 목록이나 그리드를 구성하는 경우 레이아웃의 각 항목에 콘텐츠 유형을 지정할 수 있습니다.

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

contentType을 제공하면 Compose는 같은 유형의 항목 간에만 컴포지션을 재사용할 수 있습니다. 재사용은 유사한 구조의 항목을 구성할 때 더 효율적이므로 콘텐츠 유형을 제공하면 Compose에서 A 유형의 항목을 B 유형의 완전히 다른 항목 위에 구성하려고 하지 않습니다. 그러면 컴포지션 재사용과 지연 레이아웃 성능의 이점을 극대화할 수 있습니다.

성능 측정

출시 모드에서 그리고 R8 최적화를 사용 설정한 상태에서 실행할 때만 지연 레이아웃의 성능을 안정적으로 측정할 수 있습니다. 디버그 빌드에서는 지연 레이아웃 스크롤이 더 느리게 표시될 수 있습니다. 자세한 내용은 Compose 성능을 참고하세요.