Layout과 SubcomposeLayout

SSY·2024년 9월 26일
0

Compose

목록 보기
10/10
post-thumbnail

Layout

컴포즈는 렌더링될 때, Composition -> Layout(Measurement, Placement) -> Drawing단계를 거친다.

[Composition]
Composition단계일 땐, 컴포저블 함수의 계층구조를 파악하여 이를 Tree형태로 나타내는데, 이를 Layout Node Tree라고 부른다. 또한 이때, 하나의 Node는 Layout이라는 컴포저블 함수가 기준이 된다. 즉, Layout컴포저블 함수를 단위로, 컴포저블 함수의 계층을 구조화해 이를 Tree형태로 나타내게 된다.

[Layout]
Root단계의 컴포저블 함수가 우선적으로 자식 컴포저블 함수의 측정(높이/너비)과 배치(x, y좌표)를 지시한다. 자식 컴포저블 함수들의 측정과 배치가 완료되면 이를 상위 컴포저블 함수에 보고하게 되고, 이러한 작업의 반복하여 결국, Root컴포저블 함수 또한 측정과 배치작업이 완료된다. 즉, 하위 컴포저블 함수의 측정과 배치 작업이 완료되면, 비로소 상위 컴포저블 함수의 작업 또한 완료되며, 최종적으로 Root컴포저블 함수의 최종 측정/배치 작업이 끝나게 된다. 이를통해 비로소 하나의 화면의 레이아웃 요소들의 높이와 너비 요소들이 결정된다.

[Drawing]
그 후,Drawing단계에선 결정된 높이/너비, x,y좌표를 기준으로 UI에 표시될 수 있게 해당 요소들을 그리는 작업이다. 아래 코드는 Layout을 사용하여 기초적인 UI를 그리는 코드이다.

@Composable
fun CustomLayout(
  content: @Composable () -> Unit,
  // …
) {

  // 1. `Layout()`을 호출 및 `measurePolicy`파라미터 내 '측정'과 '배치'코드를 작성 준비
  Layout(
    content = content,
    modifier = modifier
  ) { measurables, constraints ->

	// 1.1. 측정 : `measureable`파라미터를 통해 `measure()`를 호출 및 `placeable()`를 반환받음
    val placeables = measurables.map { measurable ->
      // Returns a placeable
      measurable.measure(constraints)
    }
    
    // 1.2. `MeasureScope.layout()` 호출 및 `placementBlock` 내에서 '배치'를 준비
    layout(totalWidth, totalHeight) {
    
      // 1.3. 배치 : `placementBlock`파라미터 안에 `placeable.place()`를 호출하여 측정을 수행
      placeables.map { it.place(xPosition, yPosition) }
    }
  }
}

SubcomposeLayout

참고 :

[부모로부터 받아온 제약을 자식에게 전달]
SubcomposeLayout은 부모로부터 제약사항을 받아와 자식 컴포저블 함수에게 이를 전달한다. 이를 가장 잘 나타내는 게, LazyColumn이 있는데, 디바이스 크기의 너비/높이를 SubcomposeLayout으로 전달 후, 아이템들(하위 컴포저블 함수)의 높이와 너비를 계산하고 화면에 표시될 수 있는 아이템 수를 계산하여 로딩 가능한 아이템들만 로딩하게 된다.

즉, LazyColumn은 부모로부터 제약사항을 받아올 수 있기에, 아이템들을 Lazy하게 로딩할 수 있으며,
Column는 그럴 수 없기에 Lazy한 로딩이 안되는 것이다.

가장 이해하기 쉬운 예시로 BoxWithConstraint가 있다. 해당 컴포저블 함수는 SubcomposeLayout로 구현되었을 뿐만 아니라, 부모의 높이와 너비와 같은 제약사항을 받아올 수 있다.

@Composable
@UiComposable
fun BoxWithConstraints(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,
    content:
        @Composable @UiComposable BoxWithConstraintsScope.() -> Unit
) {
    val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
    SubcomposeLayout(modifier) { constraints ->
        val scope = BoxWithConstraintsScopeImpl(this, constraints)
        val measurables = subcompose(Unit) { scope.content() }
        with(measurePolicy) { measure(measurables, constraints) }
    }
}

fun main() {
    BoxWithConstraints {
        maxWidth
        maxHeight
        minWidth
        maxWidth
    }

[자식의 제약을 또 다른 자식이 사용]
또 다른 특징으로, 첫 번째 아이템에 적용됐던 제약 사항을 두 번째 아이템에도 적용할 수 있다는 것이다. SubcomposeLayout은 내부 구현체를 보면 알 수 있듯, 기존 아이템들의 제약사항을 모두 받아올 수 있다. 이를 토대로, 새로운 아이템을 구성할 때, 기존 아이템에 적용됐던 높이와 너비를 적용해 새롭게 구성할 수 있다.

import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.unit.IntSize

// enum class SlotsEnum { Main, Dependent }
SubcomposeLayout { constraints ->
    val mainPlaceables = subcompose(SlotsEnum.Main, mainContent).map { it.measure(constraints) }
    val maxSize =
        mainPlaceables.fold(IntSize.Zero) { currentMax, placeable ->
            IntSize(
                width = maxOf(currentMax.width, placeable.width),
                height = maxOf(currentMax.height, placeable.height)
            )
        }
    layout(maxSize.width, maxSize.height) {
        mainPlaceables.forEach { it.placeRelative(0, 0) }
        subcompose(SlotsEnum.Dependent) { dependentContent(maxSize) }
            .forEach { it.measure(constraints).placeRelative(0, 0) }
    }
}

Layout vs SubcomposeLayout

아래와 같은 특징으로 위 둘의 사용을 선택하면 좋다.

[Layout]

  • 로딩할 아이템 갯수가 정해져 있으며, 많지 않은 경우(ex. 단순 UI 덩어리의 배치)
  • 아이템들의 정보를 받아올 필요가 없는 경우

[SubcomposeLayout]

  • 로딩할 아이템 갯수가 정해져있지 않으며, 많은 갯수가 될 수 있는 경우(ex. API 페이징 조회)
  • 아이템들의 정보를 받아올 필요가 있는 경우(ex. firstVisibleIndex or firstVisibleOffset)
profile
불가능보다 가능함에 몰입할 수 있는 개발자가 되기 위해 노력합니다.

0개의 댓글