[Android Compose] LazyRow란?
이전에 세로 스크롤 기능을 구현할 때 LazyColumn을 사용한 예시를 포스팅한 적이 있다.
https://onlyfor-me-blog.tistory.com/511
이번엔 세로가 아닌 가로로 스크롤할 수 있는 LazyRow에 대해 포스팅한다.
안드로이드 디벨로퍼에서 말하는 LazyRow는 아래와 같다.
현재 표시된 아이템만 구성하고 배치하는 가로 스크롤 리스트다. 컨텐츠 블록은 다양한 유형의 아이템을 내보낼 수 있는 DSL을 정의한다. 예를 들어 LazyListScope.item을 써서 단일 아이템을 추가하고 LazyListScope.items를 써서 아이템 리스트를 추가할 수 있다
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.Text
val itemsList = (0..5).toList()
val itemsIndexedList = listOf("A", "B", "C")
LazyRow {
items(itemsList) {
Text("Item is $it")
}
item {
Text("Single item")
}
itemsIndexed(itemsIndexedList) { index, item ->
Text("Item at index $index is $item")
}
}
< 파라미터 목록 >
- modifier: Modifier = Modifier - 이 레이아웃에 적용할 modifier
- state: LazyListState = rememberLazyListState() - 리스트의 상태를 제어하거나 관찰하는 데 사용되는 상태 객체
- contentPadding: PaddingValues = PaddingValues(0.dp) - 전체 컨텐츠 주변의 패딩. 이것은 컨텐츠가 잘린 후 컨텐츠에 패딩이 추가되며 modifier로는 불가능하다. 이걸 써서 1번째 아이템 앞이나 마지막 아이템 뒤에 패딩을 추가할 수 있다. 각 아이템 사이에 간격을 추가하려면 horizontalArrangement를 써라
- reverseLayout: Boolean = false - 스크롤 및 레이아웃 방향을 반대로 바꾼다. true면 아이템이 역순 배치되며 LazyListState.firstVisibleItemIndex == 0은 Row가 끝까지 스크롤됨을 의미한다. reverseLayout은 horizontalArrangement의 동작을 바꾸지 않는다. 123###은 321###이 된다
- horizontalArrangement: Arrangement.Horizontal - 레이아웃 자식의 수평 배치. 이걸 쓰면 전체 최소 크기를 채우기에 아이템이 불충분할 때 아이템 사이에 간격을 추가하고 아이템 배열을 지정할 수 있다
- verticalAlignment: Alignment.Vertical - 아이템에 적용된 수직 정렬
- flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior() - 플링 동작을 설명하는 로직
- userScrollEnabled: Boolean = true - 사용자 제스처 또는 접근성 작업을 통한 스크롤이 허용되는지 여부. 비활성화된 경우에도 상태를 써서 프로그래밍 방식으로 계속 스크롤할 수 있다
- content: LazyListScope.() -> Unit - 내용을 설명하는 블록. 이 블록 안에서 LazyListScope.item 같은 메서드를 써서 단일 아이템을 추가하거나 LazyListScope.items를 써서 아이템 리스트를 추가할 수 있다
아이템 사이에 간격을 주는 방법과 첫 아이템 왼쪽, 마지막 아이템 오른쪽에 간격을 주는 방법이 다르다는 걸 파라미터 목록을 확인하면 알 수 있다.
그리고 LazyColumn, LazyRow에 대해 설명하는 안드로이드 디벨로퍼 페이지도 확인한다.
https://developer.android.com/jetpack/compose/lists?hl=ko
많은 수의 아이템이나 길이를 알 수 없는 리스트를 표시해야 할 때 Column 같은 레이아웃을 쓰면 모든 아이템이 표시 가능 여부와 상관없이 구성, 배치되므로 성능 문제가 발생할 수 있다. Compose는 구성요소의 표시 영역에 표시되는 아이템만 구성해 배치하는 구성요소 집합을 제공한다. 이런 구성요소에는 LazyColumn, LazyRow가 포함된다
이름에서 알 수 있듯 LazyColumn과 LazyRow의 차이는 아이템을 배치하고 스크롤하는 방향이다. LazyColumn은 세로 스크롤되는 리스트를 만들고 LazyRow는 가로 스크롤되는 리스트를 만든다
지연 구성요소는 대부분의 Compose 레이아웃과 다르다. 지연 구성요소는 @Composable 컨텐츠 블록 구성요소를 수락하고 앱에서 직접 컴포저블을 내보낼 수 있도록 허용하는 대신 "LazyListScope.()" 블록을 제공한다. 이 LazyListScope 블록은 앱에서 아이템 컨텐츠를 설명할 수 있는 DSL을 제공한다. 그 다음 지연 구성요소가 레이아웃 및 스크롤 위치에 따라 각 아이템의 컨텐츠를 추가한다
정리하면 LazyRow는 핸드폰 화면에 보이는 아이템들만 배치하는 가로로 스크롤할 수 있는 리스트다. 가로 리사이클러뷰와 비슷하다고 볼 수 있다.
예시 코드는 맨 위에 걸어둔 LazyColumn 포스팅의 코드를 복붙하고 LazyColumn을 LazyRow로 바꾸면 바로 확인할 수 있으니 생략한다. 대신 안드로이드 디벨로퍼를 훑으면서 모르는 단어였던 rememberLazyListState에 대해 정리한다.
rememberLazyListSate
안드로이드 디벨로퍼에선 아래와 같이 설명하고 있다.
컴포지션 간에 기억되는 LazyListState를 만든다. 제공된 초기값을 바꿔도 상태가 이미 생성된 경우 어떤 식으로든 재생성되거나 바뀌지 않는다
여기서 말하는 LazyListState는 아래와 같다.
https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyListState
스크롤을 제어하고 관찰하기 위해 끌어올릴 수 있는(can be hoisted) 상태 객체다. 대부분 rememberLazyListState를 통해 생성된다
https://blog.devgenius.io/navigating-lists-in-jetpack-compose-with-lazyliststate-b416d7448014
...(중략) LazyListState는 3개의 속성인 firstVisibleItemIndex, firstVisibleItemScrollOffset, isScrollInProgress가 있다. firstVisibleItemIndex는 화면에 표시되는 첫 번째 아이템의 인덱스를 반환한다. 이는 리사이클러뷰 레이아웃 매니저의 findFirstVisibleItemPosition()과 유사하지만 Compose에선 주어진 시간에 상태를 반영하므로 기본 상태가 바뀔 때 리컴포지션을 강제한다...(중략)...스크롤 리스너, 텍스트 업데이트 호출, 뷰 무효화가 없다. Compose의 선언적 패러다임 덕분에 firstVisibleItemIndex를 읽으면 UI에 항상 최신 데이터가 반영된다
마찬가지로 firstVisibleItemScrollOffset은 1번째 표시 아이템의 상단, 1번째 표시 픽셀 사이의 거리를 픽셀 단위로 반영한다. 예를 들어 리스트에서 인덱스 3의 아이템으로 이동한 다음 135픽셀 아래로 스크롤하면 firstVisibleItemScrollOffset은 135가 된다. 이 속성은 아이템 경계를 넘어 스크롤할 때 자동 재설정된다. 따라서 이전 예시에서 인덱스 3의 아이템 높이가 100픽셀인 경우, 135 픽셀을 스크롤하면 firstVisibleItemIndex는 4가 되고 firstVisibleItemScrollOffset은 35가 된다
3번째 속성은 현재 진행 중인 스크롤이 있는지 여부에 따라 T/F를 반환하는 isScrollInProgress다. Compose는 선언적이고 LazyListState는 상태 저장이므로 항상 최신 데이터를 반영한다. 이에 대한 사용례는 스크롤할 때마다 현재 스크롤 위치의 헤더 배너를 표시하는 것이다
if (listState.isScrollInProgress) {
val currentItem = items[listState.firstVisibleItemIndex]
HeaderBanner(currentItem)
}
그리고 호이스트라는 단어가 나온다. Compose의 호이스트는 상태 호이스팅(State hoisting)이라 해서 상태를 보유한 컴포저블을 상태를 보유하지 않은 컴포저블로 만들기 위해 상태를 컴포저블을 호출하는 위치로 옮기는 패턴이다. 여기서 상태 호이스팅을 더 정리하는 건 이 포스팅의 범위를 벗어나기 때문에 생략하고 별도의 포스팅에서 작성한다.