안녕하세요 안드로이드 개발자 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
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
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
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
https://gift123.tistory.com/34?category=967702
https://gift123.tistory.com/38?category=967702
https://gift123.tistory.com/39?category=967702
https://gift123.tistory.com/41?category=967702
https://gift123.tistory.com/42?category=967702
https://gift123.tistory.com/43?category=967702
'안드로이드 Android' 카테고리의 다른 글
안드로이드 개발 (18) Compose Text 2편 (0) | 2021.06.17 |
---|---|
안드로이드 개발 (17) Compose Text 1편 (0) | 2021.06.17 |
안드로이드 개발 (15) Theming in Compose (0) | 2021.06.15 |
안드로이드 개발 (14) Layout in Compose 2편 (0) | 2021.06.15 |
안드로이드 개발 (13) Layout in Compose 1편 (2) | 2021.06.14 |