[Android/Kotlin] Compose Lazy lists
Lazy lists
많은 수의 항목이나 길이를 알 수 없는 목록을 표시해야 하는 경우 Column과 Row 같은 레이아웃을 사용하면 모든 항목이 표시 가능 여부와 관계없이 구성되고 배치되므로 성능 문제가 발생할 수 있습니다.
Compose는 구성요소의 표시 영역에 표시되는 항목만 구성하여 배치하는 구성요소 집합을 제공합니다. 이러한 구성요소에는 LazyColumn 및 LazyRow가 포함됩니다.
* 이를 XML에서 Recyclerview를 통해서 표현하였습니다.
- LazyColumn : 세로 스크롤되는 목록
- LazyRow : 가로 스크롤되는 목록
- LazyVerticalGrid
- LazyHorizontalGrid
- LazyStaggeredGrid
다음은 LazyColumn과 LazyRow에 대한 함수입니다.
@Composable
fun LazyColumn(
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
content: LazyListScope.() -> Unit
){
LazyList(
modifier = modifier,
state = state,
contentPadding = contentPadding,
verticalAlignment = verticalAlignment,
horizontalArrangement = horizontalArrangement,
isVertical = false,
flingBehavior = flingBehavior,
reverseLayout = reverseLayout,
userScrollEnabled = userScrollEnabled,
content = content
)
}
LazyListScope의 DSL은 레이아웃의 항목을 설명하는 여러 함수를 제공합니다. item() 함수는 단일 항목을 추가하고 item(Int)는 여러 항목을 추가합니다.
다음은 LazyColumn에 관한 예시입니다. LazyRow에서도 같은 방식으로 구현합니다.
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")
}
}
LazyColumn {
items(messages) { message ->
CustomView(message)
}
}
open fun items(
count: Int,
key: ((index: Int) -> Any)? = null,
contentType: (index: Int) -> Any = { null },
itemContent: @Composable LazyItemScope.(index: Int) -> Unit
): Unit
LazyVerticalGrid를 사용하면 항목의 너비를 지정할 수 있고 그러면 그리드는 가능한 한 많은 열에 맞습니다. 남은 너비는 열 수가 계산된 후 열 간에 균등하게 분배됩니다. 이러한 적응형 크기 조절 방법은 다양한 화면 크기에서 항목 집합을 표시하는 데 특히 유용합니다.
사용할 열의 정확한 수를 알고 있으면 필요한 수의 열이 포함된 GridCells.Fixed의 인스턴스를 대신 제공할 수 있습니다.
디자인에서 특정 항목만 비표준 측정기준이 있어야 하는 경우 그리드 지원을 사용하여 항목에 맞춤 열 스팬을 제공할 수 있습니다. LazyGridScope DSL item 및 items 메서드의 span 매개변수를 사용하여 열 스팬을 지정합니다. 스팬 범위의 값 중 하나인 maxLineSpan은 적응형 크기 조절을 사용할 때 특히 유용합니다. 열 수가 고정되어 있지 않기 때문입니다. 다음 예는 전체 행 스팬을 제공하는 방법을 보여줍니다.
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 30.dp) //columns = GridCells.Fixed(3)
) {
item(span = {
// LazyGridItemSpanScope:
// maxLineSpan
GridItemSpan(maxLineSpan)
}) {
CategoryCard("Fruits")
}
// ...
}
@Composable
fun LazyVerticalGrid(
columns: GridCells,
modifier: Modifier = Modifier,
state: LazyGridState = rememberLazyGridState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
content: LazyGridScope.() -> Unit
) {
LazyGrid(
slots = rememberColumnWidthSums(columns, horizontalArrangement, contentPadding),
modifier = modifier,
state = state,
contentPadding = contentPadding,
reverseLayout = reverseLayout,
isVertical = true,
horizontalArrangement = horizontalArrangement,
verticalArrangement = verticalArrangement,
flingBehavior = flingBehavior,
userScrollEnabled = userScrollEnabled,
content = content
)
}
LazyVerticalStaggeredGrid 및 LazyHorizontalStaggeredGrid는 지연 로드된 비슷한 간격의 항목 그리드를 만들 수 있는 컴포저블입니다.
지연 세로 지그재그형 그리드는 여러 열에 걸쳐 세로로 스크롤 가능한 컨테이너에 항목을 표시하며, 개별 항목의 높이를 다르게 지정할 수 있습니다. 지연된 가로 그리드는 너비가 다른 항목이 있는 가로축에서 동일한 동작을 보입니다.
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 매개변수에 전달하여 이 작업을 지원할 수 있습니다.
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
콘텐츠 간격 : 항목 사이에 간격을 추가하려면 Arrangement.spacedBy()를 사용하세요. 아래 예에서는 각 항목 사이에 4.dp의 간격을 추가합니다. 각각의 LazyColumn, LazyRow에 해당합니다.
verticalArrangement = Arrangement.spacedBy(4.dp) // LazyColumn
horizontalArrangement = Arrangement.spacedBy(4.dp) // LazyRow
* 그리드일 경우 모두 적용이 가능합니다.
항목 키 : 기본적으로 각 항목의 상태는 목록이나 그리드에 있는 항목의 위치를 기준으로 키가 지정됩니다. 하지만 이 경우 위치를 효율적으로 변경하는 항목에 상태가 저장되지 않아 데이터 세트가 변경되면 문제가 발생할 수 있습니다. LazyColumn 내 LazyRow 시나리오의 경우 행에서 항목 위치가 변경되면 사용자가 행 내에서 스크롤 위치를 잃게 됩니다.
이 문제를 해결하려면 각 항목에 안정적이고 고유한 키를 제공하여 key 매개변수에 블록을 제공하세요. 안정적인 키를 제공하면 데이터 세트가 변경되어도 항목 상태의 일관성이 유지됩니다.
LazyColumn {
items(books, key = { it.id }) {
val rememberedValue = rememberSaveable {
Random.nextInt()
}
Row(
Modifier.animateItem(
fadeInSpec = tween(durationMillis = 250),
fadeOutSpec = tween(durationMillis = 100),
placementSpec = spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)
)
) {
// ...
}
}
}
키를 제공하면 Compose가 재정렬을 올바르게 처리할 수 있습니다. 예를 들어 항목에 저장된 상태가 포함되어 있는 경우 키를 설정하면 위치가 변경될 때 Compose가 항목과 함께 이 상태를 옮길 수 있습니다.
하지만 항목 키로 사용할 수 있는 유형에는 한 가지 제한사항이 있습니다. 키 유형은 Activity가 다시 생성될 때 상태를 유지하는 Android의 메커니즘인 Bundle에서 지원해야 합니다. Bundle은 프리미티브, enum, Parcelable과 같은 유형을 지원합니다.
Activity가 다시 생성될 때 또는 이 항목에서 스크롤하여 벗어났다가 다시 스크롤하여 돌아올 때도 항목 컴포저블 내의 rememberSaveable을 복원할 수 있도록 키를 Bundle에서 지원해야 합니다.
고정 헤더 : 스크롤하면서 헤더 콘텐츠를 고정시킬때 사용합니다. 하지만 현재 실험용 API로 향후 변경될 수 있습니다.
LazyColumn {
stickyHeader {
Header()
}
}
스크롤 위치에 반응 : 많은 앱이 스크롤 위치와 항목 레이아웃 변경사항에 반응하고 이를 수신 대기해야 합니다. 지연 구성요소는 LazyListState를 호이스팅하여 이 사용 사례를 지원합니다.
간단한 사용 사례의 경우 앱에서 일반적으로 첫 번째로 표시되는 항목에 관한 정보만 알면 됩니다. 이를 위해 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()
}
}
}
스크롤 위치 제어
ScrollToTopButton(
onClick = {
coroutineScope.launch {
// Animate scroll to the first item
listState.animateScrollToItem(index = 0)
}
}
)