시배's Android

Compose Docs | Lists and grids 본문

Android/Compose Docs

Compose Docs | Lists and grids

si8ae 2023. 9. 17. 12:04
 

목록 및 그리드  |  Jetpack Compose  |  Android Developers

목록 및 그리드 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 많은 앱에서 항목의 컬렉션을 표시해야 합니다. 이 문서에서는 Jetpack Compose에서 이 작업을 효

developer.android.com

Lazy lists

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

Compose는 컴포넌트의 뷰포트에 표시되는 항목만 컴포넌트를 작성하고 레이아웃하는 컴포넌트 세트를 제공합니다. 이러한 컴포넌트에는 LazyColumn과 LazyRow가 포함됩니다.

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

Lazy 컴포넌트는 Compose의 대부분의 레이아웃과 다릅니다. 컴포저블 콘텐츠 블록 매개변수를 허용하여 앱이 컴포저블을 직접 방출할 수 있도록 허용하는 대신, Lazy 컴포넌트는 LazyListScope.() 블록을 제공합니다. 이 LazyListScope 블록은 앱이 항목 내용을 설명할 수 있는 DSL을 제공합니다. 그런 다음 Lazy 컴포넌트는 레이아웃과 스크롤 위치에 따라 각 항목의 콘텐츠를 추가합니다.

LazyListScope DSL

LazyListScope의 DSL은 레이아웃의 항목을 설명하기 위한 여러 함수를 제공합니다. 가장 기본적인 item()은 단일 항목을 추가하고 item(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")
    }
}
목록과 같은 항목 컬렉션을 추가할 수 있는 여러 확장 기능도 있습니다. 이러한 확장 함수를 사용하면 위의 Column 예제를 쉽게 마이그레이션할 수 있습니다:
/**
 * import androidx.compose.foundation.lazy.items
 */
LazyColumn {
    items(messages) { message ->
        MessageRow(message)
    }
}
색인을 제공하는 items() 확장 함수의 변형인 itemsIndexed()라는 함수도 있습니다. 자세한 내용은 LazyListScope 참조를 참조하세요.

Lazy grids

LazyVerticalGrid 및 LazyHorizontalGrid 컴포저블은 그리드에 항목을 표시하는 기능을 지원합니다. Lazy 세로 그리드는 여러 열에 걸쳐 세로로 스크롤 가능한 컨테이너에 항목을 표시하고, Lazy 가로 그리드는 가로 축에서 동일한 동작을 수행합니다.

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

LazyVerticalGrid의 열 매개변수와 LazyHorizontalGrid의 행 매개변수는 셀이 열 또는 행으로 형성되는 방식을 제어합니다. 다음 예제에서는 GridCells.Adaptive를 사용하여 각 열의 너비를 최소 128.dp로 설정하여 그리드에 항목을 표시합니다:
LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 128.dp)
) {
    items(photos) { photo ->
        PhotoItem(photo)
    }
}

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

사용할 열의 정확한 양을 알고 있는 경우 대신 필요한 열 수를 포함하는 GridCells.Fixed 인스턴스를 제공할 수 있습니다.

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

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

Lazy staggered grid

LazyVerticalStaggeredGrid와 LazyHorizontalStaggeredGrid는 항목의 지연 로드, 엇갈린 그리드를 만들 수 있는 컴포저블입니다. 지연 수직 엇갈림 그리드는 여러 열에 걸쳐 세로로 스크롤 가능한 컨테이너에 항목을 표시하고 개별 항목의 높이를 다르게 설정할 수 있습니다. 지연 가로 격자는 가로 축에서 너비가 다른 항목에 대해 동일한 동작을 수행합니다.

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

Item keys

기본적으로 각 항목의 상태는 목록 또는 그리드에서 항목의 위치에 대해 키가 지정됩니다. 그러나 데이터 세트가 변경되면 위치가 변경된 항목은 기억된 상태를 사실상 잃게 되므로 문제가 발생할 수 있습니다. LazyColumn 내의 LazyRow 시나리오를 상상해 보면, 행이 항목 위치를 변경하면 사용자는 행 내에서 스크롤 위치를 잃게 됩니다.

이 문제를 방지하기 위해 각 항목에 대해 안정적이고 고유한 키를 제공하여 키 매개변수에 블록을 제공할 수 있습니다. 안정적인 키를 제공하면 데이터 집합 변경 시에도 항목 상태가 일관되게 유지됩니다:

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은 프리미티브, 열거형 또는 패커블과 같은 유형을 지원합니다.

LazyColumn {
    items(books, key = {
        // primitives, enums, Parcelable, etc.
    }) {
        // ...
    }
}
활동이 다시 생성될 때 또는 이 항목에서 스크롤했다가 다시 스크롤할 때도 컴포저블 항목 내부의 기억저장가능이 복원될 수 있도록 이 키는 번들에서 지원되어야 합니다.
LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = rememberSaveable {
            Random.nextInt()
        }
    }
}

Item animations

RecyclerView 위젯을 사용해 본 적이 있다면 항목 변경이 자동으로 애니메이션으로 표시된다는 것을 알고 있을 것입니다. 레이지 레이아웃은 항목 재정렬에도 동일한 기능을 제공합니다. API는 간단합니다. 항목 콘텐츠에 animateItemPlacement 수정자를 설정하기만 하면 됩니다:
LazyColumn {
    items(books, key = { it.id }) {
        Row(Modifier.animateItemPlacement()) {
            // ...
        }
    }
}

필요한 경우 사용자 지정 애니메이션 사양을 제공할 수도 있습니다:

LazyColumn {
    items(books, key = { it.id }) {
        Row(
            Modifier.animateItemPlacement(
                tween(durationMillis = 250)
            )
        ) {
            // ...
        }
    }
}

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

Tips on using Lazy layouts

Avoid using 0-pixel sized items

예를 들어 이미지와 같은 일부 데이터를 비동기적으로 검색하여 나중에 목록의 항목을 채우려는 시나리오에서 이러한 문제가 발생할 수 있습니다. 이 경우 높이가 0픽셀이고 뷰포트에 모두 맞을 수 있기 때문에 지연 레이아웃이 첫 번째 측정에서 모든 항목을 구성하게 됩니다. 항목이 로드되고 높이가 확장되면 지연 레이아웃은 뷰포트에 실제로 맞을 수 없으므로 처음에 불필요하게 구성된 다른 모든 항목을 버립니다. 이를 방지하려면 항목에 기본 크기를 설정하여 레이지 레이아웃이 뷰포트에 실제로 들어갈 수 있는 항목 수를 올바르게 계산할 수 있도록 해야 합니다:
@Composable
fun Item(imageUrl: String) {
    AsyncImage(
        model = rememberAsyncImagePainter(model = imageUrl),
        modifier = Modifier.size(30.dp),
        contentDescription = null
        // ...
    )
}
데이터가 비동기식으로 로드된 후 항목의 대략적인 크기를 알고 있는 경우, 플레이스홀더를 추가하는 등 로드 전후에 항목의 크기가 동일하게 유지되도록 하는 것이 좋습니다. 이렇게 하면 올바른 스크롤 위치를 유지하는 데 도움이 됩니다.

Avoid nesting components scrollable in the same direction

이는 같은 방향으로 스크롤 가능한 다른 부모 안에 미리 정의된 크기가 없는 스크롤 가능한 자식을 중첩하는 경우에만 적용됩니다. 예를 들어 세로로 스크롤 가능한 Column 부모 안에 높이가 고정되지 않은 자식 LazyColumn을 중첩하려고 할 때를 들 수 있습니다:

// throws IllegalStateException
Column(
    modifier = Modifier.verticalScroll(state)
) {
    LazyColumn {
        // ...
    }
}
 
대신, 모든 컴포저블을 하나의 부모 LazyColumn 안에 래핑하고 해당 DSL을 사용하여 다양한 유형의 콘텐츠를 전달하면 동일한 결과를 얻을 수 있습니다. 이렇게 하면 단일 항목뿐만 아니라 여러 목록 항목을 모두 한 곳에서 전송할 수 있습니다:
LazyColumn {
    item {
        Header()
    }
    items(data) { item ->
        PhotoItem(item)
    }
    item {
        Footer()
    }
}
 
스크롤 가능한 부모 행과 자식 레이지 컬럼과 같이 서로 다른 방향의 레이아웃을 중첩하는 경우는 허용된다는 점에 유의하세요:
Row(
    modifier = Modifier.horizontalScroll(scrollState)
) {
    LazyColumn {
        // ...
    }
}
또한 동일한 방향 레이아웃을 사용하지만 중첩된 자식에 고정 크기를 설정하는 경우도 있습니다:
Column(
    modifier = Modifier.verticalScroll(scrollState)
) {
    LazyColumn(
        modifier = Modifier.height(200.dp)
    ) {
        // ...
    }
}

Beware of putting multiple elements in one item

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

Consider using custom arrangements

일반적으로 레이지 목록에는 많은 항목이 있으며 스크롤 컨테이너 크기보다 더 많이 차지합니다. 그러나 목록에 항목이 적은 경우 뷰포트에서 항목을 배치하는 방법에 대해 보다 구체적인 요구 사항을 디자인에 반영할 수 있습니다.

이를 위해 사용자 지정 세로 배열을 사용하여 LazyColumn에 전달할 수 있습니다. 다음 예제에서 TopWithFooter 객체는 배열 메서드만 구현하면 됩니다. 첫째, 항목을 차례로 배치합니다. 둘째, 사용된 총 높이가 뷰포트 높이보다 낮으면 바닥글을 하단에 배치합니다:

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

Consider adding contentType

작성 1.2부터 Lazy 레이아웃의 성능을 최대화하려면 목록이나 그리드에 contentType을 추가하는 것이 좋습니다. 이렇게 하면 여러 가지 유형의 항목으로 구성된 목록이나 그리드를 작성하는 경우 레이아웃의 각 항목에 대한 콘텐츠 유형을 지정할 수 있습니다:
LazyColumn {
    items(elements, contentType = { it.type }) {
        // ...
    }
}
contentType을 제공하면 작성 기능은 같은 유형의 항목 간에만 작성물을 재사용할 수 있습니다. 비슷한 구조의 항목을 작성할 때 재사용이 더 효율적이므로 콘텐츠 유형을 제공하면 Compose가 완전히 다른 유형 B의 항목 위에 유형 A의 항목을 작성하려고 시도하지 않으므로 컴포지션 재사용의 이점과 Lazy 레이아웃 성능을 극대화할 수 있습니다.