기존 XML을 사용하던 방식에서는 RecyclerView를 활용해서 리스트를 구현했습니다. RecyclerView로 구현하기 위해서는 item, Adapter, ViewHoler를 모두 구현해야 했기 때문에 많은 코드를 작성해야 했습니다. 하지만, Compose에서는 Row나 Column 컴포저블이나 LazyColumn, LazyRow 그리고 LazyVerticalGrid 컴포저블로 쉽게 구현할 수 있습니다.
Compose에서 Row나 Column으로 리스트를 구현할 수는 있지만 한꺼번에 아이템의 수가 많아지는 경우 한꺼번에 렌더링한다면 성능 저하가 발생할 수 있습니다. 따라서 리스트를 구현할 때는 LazyColumn, LazyRow, LazyVerticalGrid으로 구현하는 것이 좋습니다. 이 컴포넌트를 사용하게 된다면 실제로 보이는 아이템만 인스턴스를 생성하기 때문에 화면에서 벗어나는 아이템은 파괴하여 리소스를 확보하고 아이템이 표시되는 시점에 인스턴스를 생성합니다. 이러한 방식을 통해서 무한한 길이의 리스트도 성능 저하 없이 화면에 표시할 수 있습니다.
LazyList는 LazyColumn, LazyRow 컴포저블로 구현할 수 있습니다. 이 컴포저블들은 LazyListScope 블록 안에 자식들을 배치해서 리스트 아이템들을 관리할 수 있습니다.
LazyColumn {
item {
ListItem()
}
}
LazyListScope의 item() 함수로 LazyList에 한 개의 아이템을 추가할 수 있습니다.
val items = listOf("apple", "banana", "kiwi")
LazyColumn {
items(items.size) { index - >
Text("This is ${items[index]}")
}
}
items() 함수로 LazyList에 여러 개의 아이템을 추가할 수 있습니다.
val items = listOf("apple", "banana", "watermelon")
LazyColumn {
itemsIndexed(items) { index, item - >
Text("This item is $item, index is $index")
}
}
itemsIndexed() 함수로 아이템의 콘텐츠와 인덱스값을 함께 얻을 수 있습니다.
LazyList와 LazyRow는 스크롤을 기본으로 지원하지만 Row, Column으로 리스트를 만드는 경우 스크롤을 가능하게 ScrollState 인스턴스를 만들어야 합니다.
ScrollState는 Row와 Column 부모가 재구성을 하더라도 현재 스크롤 위치를 기억하는 상태 객체입니다. ScrollState 인스턴스를 만들 때는 rememberScrollState() 함수를 호출해야 합니다.
val scrollState = rememberScrollState()
ScrollState 인스턴스를 생성 후 Modifier의 확장함수인 verticalScroll()이나 horizontalScroll()의 파라미터로 전달합니다.
Column(
modifier = Modifier.verticalScroll(scrollState)
) {
repeat(100) {
ListItem()
}
}
위 코드는 Column 리스트에서 수직 스크롤을 활성화한 코드입니다.
Row(
modifier = Modifier.horizontalScroll(scrollState)
) {
repeat(100) {
ListItem()
}
}
위 코드는 Row 리스트에서 수평 스크롤을 활성화한 코드입니다.
코드로 현재 스크롤 위치를 변경할 수 있습니다. 예를 들어, 리스트를 아래로 스크롤 한 경우 다시 최상단으로 돌아갈 수 있도록 FAB 버튼을 구현하고자 할 때 사용합니다.
Row/Column 기반 리스트와 LazyList의 구현 방법은 다릅니다.
LazyColumn, LazyRow 리스트를 프로그래밍적으로 스크롤을 구현할 때는 LazyListState 인스턴스가 제공하는 함수를 호출해야합니다. LazyListState 인스턴스는 rememberLazyListState() 함수를 호출해서 생성할 수 있습니다.
val listState = rememberLazyListState()
리스트의 상태를 얻은 뒤에는 LazyColumn 이나 LazyRow의 선언에 적용할 수 있습니다.
LazyColumn(
state = listState,
...
) {
...
}
animateScrollToItem(index: Int): 지정한 리스트 아이템까지 부드럽게 스크롤합니다.scrollToItem(index: Int): 지정한 리스트 아이템까지 애니메이션 없이 즉시 스크롤(이동)한다.위 두 함수는 아이템의 인덱스를 이용해서 스크롤의 위치를 이동합니다.
LazyListState가 제공하는 함수를 사용할 때는 재구성 과정에도 유지되는 코루틴 스코프 인스턴스의 코루틴 스코프 안에서 실행되어야 합니다.
val items = listOf("apple", "banana", "watermelon")
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
listState.scrollToItem(0)
}
}) {
...
}
버튼을 클릭하면 첫 아이템으로 이동하는 코드입니다.
val items = listOf("apple", "banana", "watermelon")
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
listState.scrollToItem(items.size - 1)
}
}) {
...
}
버튼을 클릭하면 가장 마지막 아이템으로 이동하는 코드입니다.
Row, Column 기반 리스트에 프로그래밍적 스크롤을 구현하기 위해서는 ScrollState 인스턴스가 필요합니다. ScrollState는 스크롤 활성화를 구현했을 때와 마찬가지로 rememberScrollState() 함수를 통해 생성할 수 있습니다.
val scrollState = rememberScrollState()
animateScrollTo(value: Int): 애니메이션을 이용해서 지정한 픽셀 위치까지 부드럽게 스크롤 합니다.scrollTo(value: Int): 지정한 픽셀 위치까지 곧바로 스크롤(이동) 합니다.위 두 함수 모두 value의 파라미터 값은 아이템의 index가 아닌 픽셀 기준의 리스트의 위치입니다.
리스트의 시작점 픽셀 위치는 0 이고, 리스트의 끝을 나타내는 픽셀 위치는 ScrollState 인스턴스의 maxValue 프로퍼티를 통해 알아낼 수 있습니다.
LazyList와 마찬가지로 ScrollState가 제공하는 함수를 사용할 때는 재구성 과정에도 유지되는 코루틴 스코프 인스턴스의 코루틴 스코프 안에서 실행되어야 합니다.
val scrollState = rememberScrollState()
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
scrollState.scrollTo(0)
}
}) {
...
}
버튼을 클릭하면 첫 아이템으로 이동하는 코드입니다.
val scrollState = rememberScrollState()
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
scrollState.scrollTo(scrollState.maxValue)
}
}) {
...
}
버튼을 클릭하면 가장 마지막 아이템으로 이동하는 코드입니다.
리스트를 아래로 스크롤 한 경우 다시 최상단으로 돌아갈 수 있도록 FAB 버튼을 구현하기 위해서는 현재 스크롤 위치에 반응해야 합니다. 아래로 스크롤 한 경우 이외에도 리스트를 특정한 아이템 위치까지 스크롤 한 경우 특정한 액션을 수행할 수 있습니다.
LazyListState 인스턴스의 firstVisibleItemIndex 프로퍼티에 접근해서 구현할 수 있습니다. firstVisibleItemIndex 프로퍼티는 리스트에서 현재 가장 처음에 보이는 아이템의 인덱스 값을 갖습니다. LazyColumn 리스트를 스크롤 했을 때 일곱 번째 아이템이 가장 위에 표시된다면 firstVisibleItemIndex 값은 6가 됩니다. (index는 0부터 시작하기 때문입니다.)
val listState = rememberLazyListState()
val firstVisibleIndex = listState.firstVisibleItemIndex
if (firstVisibleIndex > 6) {
TODO("FAB 버튼 구현하기")
}
위 코드는 7번째 아이템이 처음 표시된다면 FAB 버튼이 표시되는 코드입니다.
Compose로 리스트를 구현했을 때 코드가 확실히 줄어들어 구현 속도가 빨라졌고, 무엇보다 가독성이 좋아졌습니다. XML 기반으로 구현하는 경우, 아이템에 대한 ClickListener만 구현하더라도 ClickListener 인터페이스, 무명 객체, 이를 전달받는 Adapter와 ViewHolder 등 많은 코드가 필요했지만, Compose로 구현한다면 아이템의 Modifier의 clickable 확장 함수를 사용하여 쉽게 구현할 수 있었습니다.
코드의 간결함 덕분에 유지 관리가 편리하고 가독성이 향상되기 때문에, 리스트를 구현하는 화면만이라도 LazyRow나 LazyColumn 그리고 LazyVerticalGrid를 사용해서 구현하는 것이 좋겠다는 생각이 들었습니다.