목차
- Compose 시작
- Compose의 생명주기
- Compose에서의 상태(State)
- 렌더링 수행 과정
- 부수효과(Side-Effect)
- 시맨틱(Semantics)
- 아키텍쳐 레이어
- 로컬 범위지정(Scope) 데이터
Compose 렌더링 수행 과정
프레임의 3가지 단계(Phase)
Composition
: 표시할 UI를 결정하는 단계. Composable 함수를 실행하고 UI의 설명(descrtiption)을 생성.Layout
: UI의 위치를 측정(measuring)하고 배치(placement)하는 단계. 각 요소들을 2D좌표계로 측정&배치 수행.Drawing
: 렌더링을 수행하는 단계. UI 요소들은 일반적으로 디바이스의 화면인 Canvas에 그려짐.
- 대부분의 Compose 컴포넌트들은 데이터를 이 단계의 순서(
Composition -> Layout -> Drawing
)로 한방향으로만 전달해서 하나의 프레임을 구성.- BoxWithConstraints, LayzColumn & 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)
)
}
- 상태값이 동일하다면, Recomposition은 수행하지 않음. (Compose의 생명주기 참고)
Layout 단계에서의 읽기
- Layout의 두 단계 - 측정(measurement) & 배치(placement)
- 측정 :
Layout
컴포저블(Box, Column 등)에 전달된 측정람다와 LayoutModifier의 MeasureScope.measure() 등을 실행 - 배치 : LayoutModifier의 layout() 함수의
placementBlock
람다와 Modifier의 offset()의offset
람다 등을 실행
- 측정 :
- 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)
}