Android

[Android/Kotlin] Compose Lazy lists

YusAOS 2024. 11. 13. 18:16

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