2022년 5월 29일 일요일

[안드로이드] Compose 4 - 렌더링 수행 과정

 

목차

  1. Compose 시작
  2. Compose의 생명주기
  3. Compose에서의 상태(State)
  4. 렌더링 수행 과정
  5. 부수효과(Side-Effect)
  6. 시맨틱(Semantics)
  7. 아키텍쳐 레이어
  8. 로컬 범위지정(Scope) 데이터

Compose 렌더링 수행 과정

프레임의 3가지 단계(Phase)

  1. Composition : 표시할 UI를 결정하는 단계. Composable 함수를 실행하고 UI의 설명(descrtiption)을 생성.
  2. Layout : UI의 위치를 측정(measuring)하고 배치(placement)하는 단계. 각 요소들을 2D좌표계로 측정&배치 수행.
  3. Drawing : 렌더링을 수행하는 단계. UI 요소들은 일반적으로 디바이스의 화면인 Canvas에 그려짐.


  • 대부분의 Compose 컴포넌트들은 데이터를 이 단계의 순서(Composition -> Layout -> Drawing)로 한방향으로만 전달해서 하나의 프레임을 구성.
    • BoxWithConstraintsLayzColumn & LazyRow는 위의 흐름을 따르지않는 대표적인 예외 컴포넌트 - 하위요소의 Composition이 상위요소의 Layout 단계에 따라 달라짐
  • Compose 컴포넌트가 화면에 표시될 때마다 위의 단계를 반복적으로 수행하는데, 성능개선을 위해 같은 입력(데이터)으로 인한 프레임구성을 최대한 수행하지 않도록 최적화 되어있음.

상태 읽기 (State reads)

  • 상태(State)는 일반적으로 mutableStateOf() 함수를 이용하여 생성 (Compose에서의 상태 참고)
  • 상태를 읽는 방법은 2가지
    • value필드를 직접 사용
    • 코틀린의 프로퍼티 위임 기법 사용
// value 필드 직접 읽기
val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.padding(paddingState.value)
)
// 프로퍼티 위임으로 읽기
var padding: Dp by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.padding(padding)
)
  • Compose는 상태를 읽을 때 어떤 동작을 하고있었는지(프레임 구성의 어떤 단계였는지)를 추적하여, 상태가 변하는 경우에 그 단계부터 다시 수행
  • 이 상태는 어떤 단계에 있는가? : 상태를 읽는 시점이 Composable 함수 내에 어떤 스코프인가?

단계별 상태 읽기

Composition 단계에서의 읽기

  • @Composable 함수/람다 내에서 상태를 읽는 경우를 의미
  • 상태값의 변화를 감지하면 Recomposer는 Composable함수의 재실행을 예약
    • Composition -> Layout -> Drawing의 세 단계를 모두 다시 실행할 수도 있음.
    • 컨텐츠가 변경되지 않고, 크기/레이아웃이 변경되지 않았으면 이후 단계는 스킵할 수 있음
@Composable
fun showHello() {
    var padding by remember { mutableStateOf(8.dp) }
    Text(
        text = "Hello",
        // `padding` 상태값은 Composable함수내에서 값을 읽었으므로, Composition 단계의 읽기
        // modifier를 생성할 때 `padding`의 값이 변경되었으면, recomposition을 수행하도록 예약
        modifier = Modifier.padding(padding)
    )
}

Layout 단계에서의 읽기

  • Layout의 두 단계 - 측정(measurement) & 배치(placement)
  • Layout의 단계 내에서 상태를 읽는 경우를 의미
  • 상태값이 변경되면 Compose는 Layout단계를 예약
    • Layout -> Drawing의 단계를 다시 실행할 수도 있음
    • 크기/위치의 변경이 없으면 이후 단계는 스킵할 수 있음
var offsetX by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.offset {
        // `offsetX` 상태는 배치 단계에서 값을 읽었으므로, Layout 단계의 읽기
        // `offsetX`가 변경되면 layout을 다시 수행
        IntOffset(offsetX.roundToPx(), 0)
    }
)

Drawing 단계에서의 읽기

  • Drawing 람다/함수 내에서 상태를 읽는 경우를 의미
  • 상태값이 변경되면 Drawing 단계를 예약
    • 가장 마지막 단계이므로 다른단계에는 영향을 미치지 않음
var color by remember { mutableStateOf(Color.Red) }
Canvas(modifier = modifier) {
    // `color` 상태는 Canvas람다 내부에서 상태를 읽었으므로, Drawing 단계의 읽기
    // `color`가 변화하면 drawing을 다시 수행
    drawRect(color)
}

최적화

  • 각 단계에서 필요한 상태에 대해서만 읽기를 수행하면 불필요한 렌더링 단계를 줄일 수 있음

예시 - 사용자 스크롤에 따라 offset을 변화하는 경우

  • Composition 단계에서의 상태읽기 - 전체 단계가 다시 수행됨
    • 데이터(값)이 변경되는 경우가 아니라면, Composition부터 수행하는 것은 불필요한 동작
Box {
    val listState = rememberLazyListState()

    Image(
        //최적화 필요한 구현
        Modifier.offset(
            with(LocalDensity.current) {
                //firstVisibleItemScrollOffset 상태값을 Composition 단계에서 읽고 있음.
                (listState.firstVisibleItemScrollOffset / 2).toDp()
            }
        )
    )

    LazyColumn(state = listState)
}
  • 최적화 - firstVisibleItemScrollOffset 상태를 Layout 단계에서 읽도록 변경
    • 불필요한 Composition 단계를 수행하지 않음
Box {
    val listState = rememberLazyListState()

    Image(
        Modifier.offset {
            //firstVisibleItemScrollOffset 상태값을 Layout 단계에서 읽고 있음. (Modifier.offset의 람다 안)
            IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2)
        }
    )

    LazyColumn(state = listState)
}