https://developer.android.com/jetpack/compose/lists

 

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

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

developer.android.com

 

많은 앱들은 여러 항목을 표시하고 싶어 함.

이 문서는 Jetpeck Compose 에서 어떻게 효율적으로 하는지 설명.

스크롤을 필요로 하지 않는 간단한 행열은 아래와 같이 구현 할 수도 있음.(잘 사용하지 않는 방법)

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

 

Lazy lists

Colunm에 많은 항목을 표시하면 성능 이슈 발생

(보여지는 여부와 관계 없이 모두 그리기 때문.)

 

LazyColunm - 세로 리스트

LazyRow - 가로 리스트

 

일반 layout은

@Composable content을 파라미터로 받아

앱이 직접 이 composable를 emit 할 수 있게 해줌.

 

하지만 Lazy component는 LazyListScope라는것을 제공해줌.

LazyListScope는 앱이 항목들을 describe 할 수 있는 DSL이라는 것을 제공

 

DSL(domail-specific language)의 개념 는 따로 공부해야함

 

LazyListScope DSL

LazyListScope DSL는 리스트를 구현 할 수 있는 여러 함수를 제공

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

 

items라는 확장함수를 사용하면 아래와 같이 코드를 변경 가능.

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

// 확장 함수 사용 시.
LazyColumn {
    items(messages) { message ->
        MessageRow(message)
    }
}

 

Lazy grids

DSL 아주 비슷한 LazyGridScope 지원하여 쉽게 구현 가능

 

 

LazyVerticalGrid - 세로 스크롤 그리드

LazyHorizontalGrid - 가로 스크롤 그리드

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

 

LazyVerticalGrid는 가로 크기를 지정.

CridCells.Adaptive API에서 계산 후 균등하게 나눔(정확하게 지정한 크기로 나오지 않음.). 

한번 설정으로 다양한 화면 크기에 적용할 수 있어 유용

 

GridCells.Fixed는 크기가 아닌 갯수로 지정 가능.

 

maxLineSpan과 같은 옵션을 사용하여 한줄을 하나의 컬럼으로 채울 수 있음.

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

 

Lazy staggered grid

항목 크기가 랜덤하게 지정.

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

 

Content padding

가장자리에 패딩을 주고 싶은 경우. 개별 컬럼이 아닌 리스트 최상/하단 양옆에 적용

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

 

Content spacing

개별 컬럼사이에 간격 적용 시

LazyColumn(
    verticalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

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

 

Item keys

항목의 상태는 위치에 따라 결정.

데이터 변경 시, 항목의 위치가 바뀌므로 항목이 상태를 잃게됨.

항목에 키를 부여하면 위치가 바뀌어도 항목이 상태를 유지함.

LazyColumn {
    items(
        items = messages,
        key = { message ->
            // Return a stable + unique key for the item
            message.id
        }
    ) { message ->
        MessageRow(message)
    }
}

 

안드로이드 액티비티가 재생성될 때 유지하는 매커니즘으로 Bundle를 사용

리스트도 이를 따라 적용 하여 Bundle 에서 지원 타입만 을 key값으로 사용 할 수 있다.

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

 

rememberSaveable를 사용하면 액티비티 재생성시에도 리스트를 유지 할 수 있다.

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

 

Item animations

애니메이션을 사용하고 싶을 경우

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

 

새로운 위치로 이동하는 애니메이션을 적용하고자 할 경우 항목에 키를 설정해야 한다.

 

 

Sticky headers (experimental)

그룹 데이터를 표시하는데 유용.

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

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

 

Reacting to scroll position

위치와 항목 레이아웃 변경을 listen 하고 싶을 때 LazyListState를 사용

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

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

 

리스트의 최상단이 아니면 버튼을 나오게 하는 코드

@OptIn(ExperimentalAnimationApi::class)
@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()
        }
    }
}

 

리스트의 특정 위치를 감지하고 싶을 때

val listState = rememberLazyListState()

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

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

 

Controlling the scroll position

스크롤 위치를 직접 지정하고 싶을 때. 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)
            }
        }
    )
}

 

Large data-sets (paging)

무한 스크롤이라고 말하는 기능을 구현하고자 할 때 사용.

큰 데이터의 일부분을 스크롤 시 계속 가져올 수 있음.

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

 

RemoteMediator 사용하여 네트워크로 부터 데이터 fetch를 할 경우. 화면이 충분이 꽉 채워 질 수 있게 placeholder items을 넣어줘야함. 그렇지 않으면 여러번 데이터를 요청 할 수 있으므로 주의.

 

Tips on using Lazy layouts

Avoid using 0-pixel sized items

이미지 로드시 크기를 지정하지 않으면 컬럼 크기에 0에서 갑자리 바뀌는 현상 방지를 위해 고정으로 크기를 넣기

 

Avoid nesting components scrollable in the same direction

스크롤 안에 리스트를 넣는것을 피하기

// throws IllegalStateException
Column(
    modifier = Modifier.verticalScroll(state)
) {
    LazyColumn {
        // ...
    }
}

 

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

 

위와같이 구현하면 리스트는 정상적으로 보이지만. 1,2 항목은 하나의 항목으로 보기 때문에 개별 항목에 대한 변경(ex: 클릭 시 항목 디자인 변경)가  불가.

모든 항목을 재구성해야 하므로 성능 이슈가 발생.

스크롤 시도 마찬가지 성능 이슈가 발생.

 

 

Consider using custom arrangements

리스트에 예를들면 코멘트를 작성하는 footer을 구현 시

리스트 크기가 충분히 크기 않을 경우 footer을 화면 가장 아래로 배치시키는 방법

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

Compose1.2 부터 리스트에 컨텐츠 타입을 지정 가능. 타입에 따라 다양하게 항목을 나타내고 싶을 때 유

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

 

Measuring performance

레이지 레이아웃은 스크롤 시 느릴 수 있다.

Compose performance. 참고 해보기

+ Recent posts