본문 바로가기

안드로이드 Android

안드로이드 개발 (16) Compose Lists

안녕하세요 안드로이드 개발자 Loner입니다. Compose의 정리를 이어서 진행해보도록 하겠습니다.

List

기존 Xml방식으로 List는 주로 리싸이클러 뷰 혹은 리스트뷰로 많이 구현을 해왔습니다. 

Compose에서는 List를 어떻게 구현해야할지 살펴보겠습니다.

 

심플 구현

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

Column 또는 Row를 사용하여 위과 같이 각 아이템의 콘텐츠를 표시할 수 있음

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

하지만 아이템 갯수만큼 UI가 미리 만들어져 있기 때문에 많은 size의 아이템을 표시해야 하는 경우 

성능문제가 발생할 수 있음 

 

1. Lazy Composable

위의 Column를 이용한 심플 구현과 달리 화면상 표시되는 영역만 아이템을 구성하며 배치할 수 있습니다.

이러한 구성요소에는 LazyColumn  LazyRow가 포함됩니다.

 

참고: RecyclerView와 동일한 원칙을 따릅니다.

 

1) LazyColumn 와 LazyRow

- LazyColumn은 세로로 스크롤되는 목록을 생성

- LazyRow는 가로로 스크롤되는 목록을 생성

- Lazy 구성요소는 Compose의 대부분 레이아웃과 다름

- Lazy 구성요소는 @Composable 콘텐츠 블록 구성요소를 수락하고 앱에서 직접 Composable을 내보낼 수 있도록 허용하는 대신 LazyListScope.() 블록을 제공함

 

 

2) LazyListScope DSL

 LazyListScope 블록은 앱에서 항목 콘텐츠를 설명할 수 있는 DSL을 제공합니다.

그런 다음 지연 구성요소가 레이아웃 및 스크롤 위치에 따라 각 항목의 콘텐츠를 추가합니다.

 

DSL은 도메인별 언어를 의미함. Compose에서 일부 API용 DSL을 정의하는 방법에 관한 자세한 내용은 아래 문서를 참고할 것 

https://developer.android.com/jetpack/compose/kotlin#dsl

 

Jetpack Compose용 Kotlin  |  Android Developers

Jetpack Compose는 Kotlin을 중심으로 빌드되었습니다. 일부 경우에 Kotlin은 좋은 Compose 코드를 더 쉽게 작성할 수 있게 하는 특수 관용구를 제공합니다. 다른 프로그래밍 언어로 생각하고 그 언어를 Kot

developer.android.com

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와 같이 항목 컬렉션을 추가할 수 있는 다양한 확장 함수도 있습니다.

 

예제

import androidx.compose.foundation.lazy.items

@Composable
fun MessageList(messages: List<Message>) {
    LazyColumn {
        items(messages) { message ->
            MessageRow(message)
        }
    }
}

- 기본 Column과 달리 성능문제가 해결됨

 

색인을 제공하는 itemsIndexed()라고 하는 items() 확장 함수의 버전도 있습니다.

자세한 내용은 아래 문서를 확인하세요.

 

https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyListScope

 

LazyListScope  |  Android 개발자  |  Android Developers

 

developer.android.com

 

2. 콘텐츠 패딩

콘텐츠 가장자리 주변에 패딩을 추가해야 하는 경우가 있습니다.

지연 구성요소를 사용하면 일부 PaddingValues을 contentPadding 매개변수에 전달하여 이 작업을 지원할 수 있습니다.

 

예제

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

 

결과

 

3. 콘텐츠 간격

 Arrangement.spacedBy()를 사용하면 아이템 마다 간격을 줄 수 있음

 

예제

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

-Column은 verticalArrangement만 사용 

LazyRow(
    horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

-Row는 horizontalArrangement만 사용

 

결과

 

4. 항목 애니메이션

-RecyclerView 의경우 아이템 변경시 자동으로 애니메이션 처리되지만

지연 레이아웃에서는아직이 기능을 제공하지 않음.

 

- 항목을 변경하면 인스턴트 '스냅'이 발생

- 차후 업데이트 가능성이 있음

 

5. 고정 헤더(실험용)

주의: 실험용 API는 향후 변경되거나 완전히 삭제될 수 있음

 

'고정 헤더' 패턴은 그룹화된 데이터 목록을 표시할 때 유용함

LazyColumn이 있는 고정 헤더를 표시하려면 헤더 콘텐츠를 제공하는 실험용 stickyHeader() 함수를 사용

 

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

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

 

위예와 같이 여러 헤더를 표시하려면 다음 예제를 참고하세요

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

 

5. 그리드(실험용)

주의: 실험용 API는 향후 변경되거나 완전히 삭제될 수 있음

LazyVerticalGrid Composable은 아이템을 그리드로 표시하기 위한 실험용 지원 기능을 제공합니다.

 

예제

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PhotoGrid(photos: List<Photo>) {
    LazyVerticalGrid(
        cells = GridCells.Adaptive(minSize = 128.dp)
    ) {
        items(photos) { photo ->
            PhotoItem(photo)
        }
    }
}

cells 매개변수는 셀을 열로 구성하는 방식을 제어합니다. 

사용할 열의 정확한 수를 알고 있으면 필요한

수의 열이 포함된 GridCells.Fixed의 인스턴스를 대신 제공할 수 있습니다.

 

 

 

6. 스크롤 위치에 반응

 스크롤 위치와 항목 레이아웃 변경사항에 반응하려면

지연 구성요소는LazyListState를 호이스팅하여 사용합니다.

 

예제1.

@OptIn(ExperimentalAnimationApi::class) // AnimatedVisibility
@Composable
fun MessageList(messages: List<Message>) {
    Box {
        val listState = rememberLazyListState()

        LazyColumn(state = listState) {
            // ...
        }
        val showButton by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex > 0
            }
        }

        AnimatedVisibility(visible = showButton) {
            ScrollToTopButton()
        }
    }
}

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

 - LazyListState는 firstVisibleItemIndex  firstVisibleItemScrollOffset 속성을 제공합니다.

-  위의 예에서는 derivedStateOf()를 사용하여 불필요한 Composition을 최소화함

 

 

Composition에서  동일한 컴퍼지션에서 직접 상태를 읽는 기능을 처리할 필요가 없는 시나리오도 있습니다.

snapshotFlow()를 사용할 수 있습니다.

 

예제2.

val listState = rememberLazyListState()

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

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

- 이 시나리오의 일반적인 예는 사용자가 특정 지점을 지나 스크롤한 후 분석 이벤트를 보내는 것입니다.

- 이 시나리오를 효율적으로 처리하기 위해 snapshowFlow()를 사용했습니다.

- LazyListState는 또한 layoutInfo 속성을 통해 현재 표시된 모든 항목 및 화면의 경계에 관한 정보를 제공합니다.

 

자세한 내용은 아래 문서를 참고하세요.

https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfo

 

LazyListLayoutInfo  |  Android 개발자  |  Android Developers

 

developer.android.com

 

7. 스크롤 위치 제어

LazyListState는 스크롤 위치를 '즉시' 스냅하는 scrollToItem() 및 애니메이션을 사용하여

스크롤하는 animateScrollToItem() 함수를 통해 이 기능을 지원합니다.

 

참고: scrollToItem()  animateScrollToItem()은 모두 suspend 함수입니다. 즉 코루틴에서 호출해야 합니다.

 

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

 

8. 페이징

아이템 양이 많으면 Paging 라이브러리를 사용해서 필요한 만큼 아이템을 불러와서 표시할 수 있음.

Paging 3.0 이상에서는 androidx.paging:paging-compose 라이브러리를 통해 Compose 지원 기능을 제공합니다.

 

페이징된 콘텐츠 목록을 표시하려면 collectAsLazyPagingItems() 확장 함수를 사용한 다음

반환된 LazyPagingItems를 LazyColumn의 items()에 전달하면 됩니다.

 

 item이 null인지 확인하여 데이터가 로드되는 동안 자리표시자를 표시할 수 있습니다.

 

import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items

@Composable
fun MessageList(pager: Pager<Int, Message>) {
    val lazyPagingItems = pager.flow.collectAsLazyPagingItems()

    LazyColumn {
        items(lazyPagingItems) { message ->
            if (message != null) {
                MessageRow(message)
            } else {
                MessagePlaceholder()
            }
        }
    }
}

경고: RemoteMediator를 사용하여 데이터를 가져오는 경우 실제 크기의 자리표시자 아이템을 제공해야합니다

 RemoteMediator를 사용하는 경우 화면이 콘텐츠로 채워질 때까지 반복적으로 호출되어 새 데이터를 가져옵니다. 

위처럼 하지 않으면 화면이 채워지지 않을 수 있으며 앱에서 많은 데이터 페이지를 가져오게 됩니다.

9. 항목 키

기본적으로 각 아이템의 상태는 List에 있는 아이템의 position를 기준으로 키가 지정됩니다.

하지만 position을 효율적으로 변경하는 List에 상태가 저장되지 않아

데이터 세트가 변경되면 문제가 발생할 수 있습니다.

 

LazyColumn  LazyRow 시나리오의 경우 행에서 아이템 위치가 변경되면

사용자가 행 내에서 스크롤 위치를 잃게 됩니다.

 

이 문제를 해결하려면 각 아이템에 안정적이고 고유한 키를 제공하여 key 매개변수에 블록을 제공하면 됩니다.

 

예제

@Composable
fun MessageList(messages: List<Message>) {
    LazyColumn {
        items(
            items = messages,
            key = { message ->
                // Return a stable + unique key for the item
                message.id
            }
        ) { message ->
            MessageRow(message)
        }
    }
}


참고: 제공된 모든 키를 Bundle 내에 저장할 수 있어야 합니다.

 

 

10. 정리

- 아이템 수가 많지 않을땐 Lazy Composable를 사용하지 않고 심플하게 Column이나 Row로 만들수 있음

- Lazy Composable로 구현해야 리사이클러 뷰 처럼 성능상 효율적으로 UI를 배치하고 표시함

- Compose에서 List는 매우 만들기 편리함 간단하게 각 아이템마다 간격을 쉽게 넣을 수도 있음

- 스크롤 관련 이벤트나 제어를 쉽게 컨트롤 하게끔 지원해줌

- 페이징 라이브러리는 compose를 공식적으로 지원해주기 떄문에 paging을 사용하기 매우 편리함

- List의 position을 이용해서 해당 아이템을 찾아쓸 때 Compose 상태관리 특징상 문제가 있을 수 있음

  그래서 키를 직접 등록해주는게 안전함

 

이상 Compose의 List에 관해서 정리를 해봤습니다. 개인적으로 RecyclerView에서 어댑터를 따로 만들어 셋팅할 때 보일러플레이트 코드가 너무 많이 발생해서 상당히 불편했는데, 이번 Compose로 이런 문제들이 많이 해결되는것 같습니다. 

 

 

 

이전 Compose 내용 정리

https://gift123.tistory.com/33?category=967702 

 

안드로이드 개발 (8) Compose 이해 정리

이번 포스팅부터 Compose에 대해 차근히 파헤쳐 가보겠습니다. Android Compose 공식 문서를 보면서 정리한 내용들 입니다. https://developer.android.com/jetpack/compose/mental-model?hl=en Compose 이해  |..

gift123.tistory.com

https://gift123.tistory.com/34?category=967702 

 

안드로이드 개발 (9) Compose 상태 관리

jetpack compose 에 한창 포스팅 중입니다. https://gift123.tistory.com/33 안드로이드 개발 (8) Compose 이해 정리 이번 포스팅부터 Compose에 대해 차근히 파헤쳐 가보겠습니다. Android Compose 공식 문서를..

gift123.tistory.com

 

https://gift123.tistory.com/38?category=967702 

 

안드로이드 개발 (10) Compose Composable Lifecycle

안녕하세요 Loner 입니다. 오늘은 Compose의 컴포저블 라이프사이클 공부를 정리해 봤습니다. 아래 문서를 정리한 내용입니다. https://developer.android.com/jetpack/compose/lifecycle?hl=en 컴포저블 수명 주..

gift123.tistory.com

https://gift123.tistory.com/39?category=967702 

 

안드로이드 개발 (11) Compose Side-effects

https://developer.android.com/jetpack/compose/side-effects#state-effect-use-cases Compose의 부수 효과  | Jetpack Compose  | Android Developers 컴포저블에는 부수 효과가 없어야 합니다. 하지만 앱의..

gift123.tistory.com

https://gift123.tistory.com/41?category=967702 

 

안드로이드 개발 (13) Layout in Compose 1편

지금까지 Compose에 대해 composable 라이프사이클, Compose 내부흐름 , Composition, recompostion, Sdie-effects 활용방법에 대한 원론 방법에 알았다면 이제 실질적으로 Compose로 Layout을 어떻게 구성하는지..

gift123.tistory.com

https://gift123.tistory.com/42?category=967702 

 

안드로이드 개발 (14) Layout in Compose 2편

1편 정리 https://gift123.tistory.com/41 안드로이드 개발 (13) Layout in Compose 1편 지금까지 Compose에 대해 composable 라이프사이클, Compose 내부흐름 , Composition, recompostion, Sdie-effects 활용방..

gift123.tistory.com

https://gift123.tistory.com/43?category=967702

 

안드로이드 개발 (15) Theming in Compose

https://developer.android.com/jetpack/compose/themes?hl=en Compose의 레이아웃  | Jetpack Compose  | Android Developers Jetpack Compose를 사용하면 앱의 UI를 훨씬 쉽게 디자인하고 빌드할 수 있습니다...

gift123.tistory.com