Jetpack Compose : Lazy Layouts

wonseok·2022년 9월 13일
2
post-thumbnail

앱의 가장 흔한 형태는 데이터를 보여주는 것이다!

이러한 데이터 정보를 보여주는 방법으로 다양한 플랫폼에서 다양한 방법이 존재한다.

최근 안드로이드에서는 Lazy composables (데이터 리스트를 화면에 보여주는 쉽고 효율적인 최신의 솔루션)을 사용한다.

안드로이드 개발자들은 현재 deprecated된 ListView에서부터 현재의 RecyclerView를 사용해왔다.
그러나 두 가지 방법은 여전히 XML 코드를 사용한다.

Jetpack Compose는 Google I/O 2019에서 처음 소개되었다.
이것은 xml코드를 완전히 제거하고 데이터를 화면에 표시하는 더 다루기 쉬운 방법을 제공한다.

이번 포스팅에서는 그 중에서 Lazy Composables를 활용하는 방법을 작성할 것이다.

Lazy Lists 이해하기

Lazy Composables가 제공하는 혜택을 좀 더 잘 이해하기 위해서는 먼저 그것들이 무엇인지, 그리고 어떻게 동작하는지 이해해야 한다.

당신이 정해지지 않은 엄청난 양의 데이터를 화면에 보여주고 싶다고 가정해보자.
만약 당신이 Column/Row 레이아웃을 사용한다고 결정한다면, 이것은 그것들이 보여지고 안보여지에 상관없이 모두 그려지기 때문에 엄청난 성능적인 이슈를 야기할 것이다.

그렇지만 Lazy 옵션을 사용한다면 이것은 그것들이 보여질 때에만 컴포넌트들을 놓게 된다.
이것들은 LazyColumn, LazyRow 등이 있다.

Lazy Composables는 RecyclerView 위젯과 동일한 원칙을 따르지만, RecyclerView 에 비해 귀찮은 보일러 플레이트 코드를 훨씬 더 줄여준다.

LazyListScope 이해하기

Lazy Composable은 다른 레이웃과 약간 다른 점이 존재하는데, 그것은 @Composable 인스턴스가 아닌 LazyListScope으로 부터 DSL 블록을 제공한다는 점이다.

DSL : Domain Specific Language
앱으로 하여금 특정한 문제를 해결하기 위한 특정한 방법을 제공한다.

이 예시에서는 코틀린이 DSL을 만들기 위해 타입 세이프 빌더를 사용하고, 약간의 선언적인 방법으로 복잡한 계층적 자료구조를 완벽하게 만든다.

LazyListScope은 LazyRow와 LazyColumn에서 receiver scope 역할을 한다.

@LazyScopeMarker
interface LazyListScope {
    // 1
    fun item(key: Any? = null, content: @Composable LazyItemScope.() -> Unit)

    // 2
    fun items(
        count: Int,
        key: ((index: Int) -> Any)? = null,
        itemContent: @Composable LazyItemScope.(index: Int) -> Unit
    )
		
    // 3
    @ExperimentalFoundationApi
    fun stickyHeader(key: Any? = null, content: @Composable LazyItemScope.() -> Unit)
}

코드를 설명해보겠다.

    1. item 리시버는 Lazy layout 안에 하나의 composable을 추가할 수 있도록 한다. 만약에 당신이 원하는 만큼 많은 item을 추가하고 싶은면, 아래의 items 옵션을 확인해보자.
    1. items 리시버는 모든 아이템들을 개별적으로 정의하는 것이 아니라 아이템의 수를 기대한다. 여기, 리스트의 길이를 정의하고, 모든 아이템을 위한 specifications를 만든다.
    1. 마지막으로, stickyHeader는 상단에 sticky item을 추가한다. 스크롤 상태에서도 상단에 고정된다. 헤더는 다음 헤더가 차지하기 전가지 유지된다.

실습하기

실습하고자 하는 모듈의 build.gradle 파일에 다음을 추가한다.

def compose_version = "1.1.1"
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"

LazyRow 추가하기

LazyRow(
    modifier = Modifier.align(Alignment.CenterHorizontally)
) {
  items(cat.tags) {
    CatTag(tag = it)
  }
}

그런데 이상한 점이 있다.
위에 있는 코드는 items 메소드를 사용하는데, 위에서는 count를 인자로 받아야 하지만 여기 예제에서는 items 리스트를 받는다.
그렇다면 이 코드는 어떻게 된 것일까?

코틀린은 확장함수를 제공한다.

inline fun <T> LazyListScope.items(
    items: List<T>,
    noinline key: ((item: T) -> Any)? = null,
    crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
) = items(items.size, if (key != null) { index: Int -> key(items[index]) } else null) {
    itemContent(items[it])
}

LazyColumn 추가하기

LazyColumn {
  stickyHeader {
    CuteCatsHeader()
  }
  items(cats) {
    CatItem(cat = it)
  }
}

Lazy composables을 위한 DSL은 다른 타입의 아이템들을 emit 한다.
이 예시에서는 stickyHeader를 추가했고, 그 다음에 items를 사용하여
CatItem 리스트를 만들어냈다.

그런데, stickyHeader는 지금 약간 실험적인 단게이기 때문에 다음을 추가해주어야 한다.

@file:OptIn(ExperimentalFoundationApi::class)

LazyVeriticalGrid 추가하기

LazyVerticalGrid는 그리드(격자)뷰를 쉽게 만들어준다.
이 레이아웃 역시 실험적인 단계이기 때문에 위에서처럼 ExperimentalFoundationApi 어노테이션을 달아줘야 한다.

LazyVerticalGrid(
 cells = GridCells.Fixed(2),
) {
  item {
    CuteCatsHeader()
  }
  items(cats) {
    CatItem(cat = it)
  }
}

이 레이아웃은 다른 파라미터들을 갖게 되는데 예를들어, cells 파라미터는 grid를 fixed로 할 건지, 아니면 adaptive로 할 건지 결정한다.

여기서는 GridCells.Fixed(2)를 사용하여 고정된 길이의 두 가지 아이템들을 사용한다.
마지막에는 그냥 CatItem 리스트를 추가한다.

Data Items간 간격 두기

아마 이렇게 하다보면 Lazy composables안의 요소들이 매우 가깝게 붙어있음을 알 수 있다.

verticalArrangement 와 horizontalArrangement를 통해 Arrangement.spacedBy를 사용하면 아이템 간 간격을 띄어줄 수 있다.

가장자리에 padding을 주고 싶다면, contentPadding 파라미터에 PaddingValues를 전달함으로써 해결할 수 있다.

LazyRow에 간격 주기

LazyRow(
    modifier = Modifier.align(Alignment.CenterHorizontally),
    // New horizontal content spacing
    horizontalArrangement = Arrangement.spacedBy(12.dp),
) 
...
}

horizontalArrangement 파라미터는 오직 LazyRow에서만 가능하고 verticalArrangement 파라미터는 오직 LazyColumn에서만 가능하다.

LazyColumn에 간격 주기

@Composable
fun LazyListCats(cats: List<Cat>, state: LazyListState) {
  LazyColumn(
      // New content padding 
      contentPadding = PaddingValues(horizontal = 32.dp, vertical = 16.dp),
      // New vertical spacing 
      verticalArrangement = Arrangement.spacedBy(12.dp),
  ) {
    ...
}

LazyVerticalGrid에 간격 주기

@Composable
fun LazyGridCats(cats: List<Cat>, state: LazyListState) {
  LazyVerticalGrid(
      cells = GridCells.Fixed(2),
      // Content padding for the grid
      contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
      // LazyGrid supports both vertical and horizontal arrangement
      verticalArrangement = Arrangement.spacedBy(8.dp),
      horizontalArrangement = Arrangement.spacedBy(16.dp),
  ) {
    ...
  }
}

Lazy Composables에서 State 관리하기

Column과 Row를 사용하는 방식에 비해 Lazy composables이 갖는 가장 큰 장점은
바로 레이아웃의 state와 상호작용할 수 있다는 점이다.

그런데 state란 정확히 무엇일까?

state는 관리할 수 있고, 스크롤링을 관찰할 수 있는 object이다.

이것은 rememberLazyListState 메서드를 사용하면 가능하다.

Lazy Composables에 state 전달하기

@Composable
fun LazyGridCats(cats: List<Cat>, state: LazyListState) {
  LazyVerticalGrid(
      cells = GridCells.Fixed(2),
      contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
      verticalArrangement = Arrangement.spacedBy(8.dp),
      horizontalArrangement = Arrangement.spacedBy(16.dp),
      // Add LazyListState controller for grid
      state = state,
  ) {
    ...
  }
}

@Composable
fun LazyListCats(cats: List<Cat>, state: LazyListState) {
  LazyColumn(
      contentPadding = PaddingValues(horizontal = 32.dp, vertical = 16.dp),
      verticalArrangement = Arrangement.spacedBy(12.dp),
      // Add LazyListState controller for column
      state = state,
  ) {
    ...
  }
}

0개의 댓글