[요약] Google I/O 2022 Lazy layouts in Compose
Basic
리사이클러 뷰를 사용해본 경험이 있다면 Lazy Layout의 컨셉을 쉽게 이해할 수 있다. Lazy Layout은 스크롤 가능한 리스트에 대해 모든 아이템을 한 번에 그리는 대신, on demend로 현재 화면에서 볼 수 있는 아이템만을 그린다.
Compose 1.2 에서 lazy layout은 LazyColumn, LazyRow, Lazy grids로 구성된다.
Implement
Compose의 lazy layout은 RecyclerView와 비교했을 때, 상당히 적은 라인의 코드만으로도 리스트를 그릴 수 있다. 리사이클러 뷰 하나를 사용하기 위해서는 아래의 코드가 필요하다.
- 리사이클러 뷰 어댑터 및 뷰홀더
- 리사이클러 뷰 xml
- 리사이클러 뷰 아이템 xml
- 리사이클러 뷰와 어댑터의 바인딩
LazyLayout은 동일한 목적을 아래의 코드만으로도 달성할 수 있다.
lazy list scope DSL block을 사용하면 lazy list에 아이템을 추가할 수 있다. 여러 타입의 뷰를 그리기 위해 별도의 뷰 타입을 지정할 필요 없이 item과 items 블록 만으로도 표현 가능하다.
Costomize
Lazy list에서는 아이템에 대한 커스터마이징도 쉽게 구현 가능하다.
리스트의 시작과 끝에 여백을 추가하고 싶을 때 Modifier.padding을 사용할 수도 있지만 이렇게 구현하면 리스트를 스크롤 할때 추가된 여백만큼 아이템이 잘려서 보여진다.
Lazy list는 이러한 유스케이스를 위해 contentPadding 파라미터를 제공한다. 해당 파라미터를 사용하면 리스트의 가장자리에 대해 여백을 추가할 수 있으며, 리스트를 스크롤하여도 아이템이 clipping 되지 않고 full size로 보여진다.
추가로 horizontalAlignment, verticalAlignment 파라미터를 사용하여 아이템 사이의 간격을 제공할 수도 있다.
State
리스트를 사용하며 사용하는 일반적인 유스케이스 중 하나는, 리스트가 스크롤되는 상태를 관찰하며 이에 반응하는 것이다. Lazy list에서는 lazy list state를 사용하여 해당 기능을 구현할 수 있다.
lazy list state는 firstVisibleItem에 대한 index와 offset을 제공한다.
lazy list state를 사용하며 한 가지 주의할 점은 state의 property가 매우 빈번하게 변경될 수 있다는 것이다. 따라서 composable에서 단순히 state의 property를 읽는다면 불필요한 리컴포지션이 발생할 수 있다. 이러한 동작을 방지하기 위해 derivedStateOf를 사용할 수 있다.
derivedStateOf를 사용하면 람다에서 계산된 값이 변경될 때만 리컴포지션을 수행할 수 있다.
또한 lazy list state는 layoutInfo 프로퍼티를 제공하는데, layoutInfo를 사용하면 현재 보여지는 아이템의 정보와 lazy list의 전체 아이템 카운트 등을 얻을 수 있다.
lazy list 스크롤의 조작 또한 lazy list state를 통해 수행된다. lazy list state는 리스트의 스크롤을 조작할 수 있는 함수를 제공하는데, suspend 함수로 제공되기 때문에 함수의 호출을 위해서는 rememberCoroutineScope처럼 composable 스코프 내에서 참조할 수 있는 코루틴 스코프가 필요하다.
Lazy grids
experimental API로 제공되던 lazy grid가 Compose 1.2 부터 Stable API로 제공된다.
lazy grid는 LazyVerticalGrid, LazyHorizontalGrid를 통해 사용할 수 있다.
lazy list와 동일한 방식으로 사용할 수 있으며, [vertical|horizontal]Alignemnt, contentPadding, state 등의 파리미터를 제공한다.
GridCells.Fixed API를 사용하여 그리드에서 사용하는 셀의 수를 지정할 수 있다. 하지만 이렇게 셀의 수가 고정된다면 가로 모드 또는 대형 화면 같은 경우 어색한 UI가 보여진다. GridCells.Adaptive API를 사용하면 이 문제를 해결할 수 있다. 셀의 수는 Adaptive에 지정한 사이즈에 맞게 나눠지며, 나눠지고 남은 사이즈는 각각의 셀에 공평하게 분배된다.
좀 더 복잡한 케이스가 필요하다면 GridCells를 구현하여 셀을 나누는 방식을 커스터마이징할 수 있다. 아래의 예시는 첫 번째 column의 width가 두 번째 column의 width 보다 2배 큰 width를 가지도록 구현한다.
특정 아이템이 그리드 내에서 차지하는 사이즈를 변경해야 한다면 span을 지정할 수 있다.
LazyLayout
리사이클러 뷰에서는 아이템을 표시할 LayoutManager를 직접 구현할 수 있다. Compose에서는 동일한 목적을 달성하기 위해 LazyLayout API(experimental in Compose 1.2)를 제공한다. lazy list와 lazy grid가 이 LazyLayout을 기반으로 작성되었다.
현재 사용할 수는 없지만 Staggered grid가 LazyLayout을 사용하여 구현될 예정이다.
Item animations
Compose 1.1에서 lazy list의 item reordering animation을 experimental API로 제공하였고, Compose 1.2에서는 lazy grid에 대해서도 동일한 애니메이션을 제공한다.
아이템 재배치 애니메이션가 정상적으로 동작하기 위해서는 아이템에 적절한 key를 제공해야 한다.
아이템의 추가, 삭제에 대한 애니메이션은 현재 작업 진행 중이다.
Tips
Don’t use 0-pixel sized items
이미지를 비동기적으로 받아오고, 받아온 이미지를 바탕으로 새로운 사이즈를 결정하는 경우, 로딩 중 아이템의 사이즈가 0으로 지정되지 않도록 주의해라.
아이템이 0-pixel 사이즈를 가진다면 컴포지션 과정에서 lazy column은 모든 아이템을 한 번에 그리려고 한다.
이후 이미지가 로딩되고 리컴포지션될 때, 화면에는 일부의 아이템만 보여지기 때문에 더 이상 보이지 않는 아이템들은 무시된다. 이는 불필요한 과정이므로 0-pixel 대신 적절한 placeholder를 지정하는것이 좋다.
Avoid nesting components scrollable in the same direction
좀 더 명확히 말하자면, 스크롤 가능한 컴포저블의 사이즈가 정해지지 않은 상태로, 동일한 방향을 가지는 스크롤 가능한 컴포저블에 추가될 때 문제가 된다.
Android view system에서는 리사이클러 뷰를 동일한 방향의 스크롤 뷰 내에 추가할 수 있었지만, 이러한 방식은 퍼포먼스에 심각한 영향을 끼친다.
이는 이전 단계와 비슷한 원인을 가지는데, 자식 뷰를 배치하기 위해 부모 뷰의 높이는 제한이 없는 값을 가지고, 따라서 자식 리사이클러 뷰는 전체 아이템을 한 번에 생성한다.
컴포즈에서는 이러한 방식의 동작을 허용하지 않으며, lazy list를 다른 스크롤 가능한 컴포저블에 추가하는 대신 lazy list DSL을 통해 구현하는 것이 권장된다.
부모 컴포저블과 자식 컴포저블의 스크롤 방향이 다르거나, 자식 컴포저블의 사이즈가 고정된다면 문제가 되지 않는다.
Beware of putting multiple elements in one item
lazy list의 item 블록 내에서 여러 개의 컴포저블을 호출하더라도 warning이 나타나지 않으며, 실제로 표시될 때도 각각의 item을 호출하는 것 처럼 보여진다.
이렇게 사용하는 것은 몇 가지 문제점이 있다.
- 블록 내에 속한 컴포저블이 하나의 엔티티로 취급된다. 즉, 각각의 컴포저블이 독립적으로 리컴포지션될 수 없다. 이는 성능 이슈를 유발한다.
- 보여질 때는 각각의 아이템 처럼 보여지지만 하나의 엔티티로 취급되기 때문에 scrollToPosition 등의 동작이 기대와 다를 수 있다. 위의 예시에서 scrollToPosition(2)를 호출한다면 Item(3)로 이동하게 된다.
하지만 리스트 내에 디바이더를 표시하는 경우에는 이러한 유스케이스가 도움될 수 있다.
Consider using custom arrangements
아래와 같은 방식으로 아이템의 배치 방식을 커스터마이징할 수 있다.
Optimization
lazy list의 퍼포먼스 측정은 릴리즈 빌드에서 수행되어야 한다. 디버그 빌드에서는 레이아웃의 스크롤이 느리게 보일 수 있다. 또한 앱이 실행될 때 느려지고 점차 빨라진다면 baseline profile을 고려해봐라.
Composition reusing
리사이클러 뷰와 비슷하게, 스크롤 수행 후 아이템이 더 이상 보이지 않게 된다면, lazy list는 아이템을 즉시 제거하지 않고, 재사용하기 위해 몇 개의 아이템을 유지한다.
이를 통해 아이템을 삭제하고 다시 구성하는 시간을 절약하며, layout node(UI 트리에 속하는 레이아웃에 대한 표현)를 최대한 재사용한다.
재사용되는 컴포지션으로 새 아이템이 컴포즈될 때, 비슷한 UI 구조를 가지는 아이템의 layout node를 자동으로 재사용한다.
이러한 과정은 대부분 자동으로 수행되지만 리스트 내에서 다른 타입의 아이템을 보여주는 경우, contentType을 명시하여 같은 타입에 속하는 아이템을 재사용할 수 있다.
Prefetching
스크롤 중 새로운 아이템을 처리할 필요가 없다면, lazy list가 프레임을 그리는데 큰 문제가 없다.
하지만 새로운 아이템이 화면에 등장한다면, 컴포지션을 위한 추가적인 과정이 필요하고, 이러한 과정은 UI junk를 유발할 수 있다.
prefetching을 통해 이러한 문제를 해결할 수 있다.