Jetpack Compose Internals를 참고하여 작성하였습니다
https://kancho.tistory.com/60 ComposeRuntime - (1)에서는 Composer에 대한 내용을 작성
이 글에서는 Composition에 대해 알아본다
Composition
Composition을 생성하는 주체는 누구이며 어떻게, 언제 이루어져있는지 알아보자
Composition이 생성되면 Composer는 스스로 구축한다
Composer는 currentComposer 메커니즘을 통해 접근하고, Composition이 관리하는 트리를 생성하고 업데이트하는 데 사용된다
Compose Runtime으로 접근하는 2가지 진입점
- Composable 함수를 작성해 관련된 모든 정보를 방출하고 런타임과 연결
- setContent를 호출함으로써 Composition이 생성되고 시작되어 Composable 실행
Composition 생성하기 (Creating a Composition)
ViewGroup, ComposeView의 setContent 호출을 통해 새로운 composition 생성
internal fun AbstractComposeView.setContent(
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
GlobalSnapshotManager.ensureStarted()
val composeView =
if (childCount > 0) {
getChildAt(0) as? AndroidComposeView
} else {
removeAllViews(); null
} ?: AndroidComposeView(context, parent.effectCoroutineContext).also {
addView(it.view, DefaultLayoutParams)
}
return doSetContent(composeView, parent, content)
}
private fun doSetContent(
owner: AndroidComposeView,
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
...
// Composition 생성
val original = Composition(UiApplier(owner.root), parent)
// owner인 AndroidComposeView의 setTag를 통해 Android 뷰 객체에 직접 연결
val wrapped = owner.view.getTag(R.id.wrapped_composition_tag)
as? WrappedComposition
?: WrappedComposition(owner, original).also {
owner.view.setTag(R.id.wrapped_composition_tag, it)
}
wrapped.setContent(content)
if (owner.coroutineContext != parent.effectCoroutineContext) {
owner.coroutineContext = parent.effectCoroutineContext
}
return wrapped
}
WrappedComposition
Composition을 AndroidComposeView에 연결하는 방법을 알고 있는 데코레이터 패턴을 구현한 클래스
private class WrappedComposition(
val owner: AndroidComposeView,
val original: Composition
) : Composition, LifecycleEventObserver, CompositionServices {
private var disposed = false
private var addedToLifecycle: Lifecycle? = null
private var lastContent: @Composable () -> Unit = {}
override fun setContent(content: @Composable () -> Unit) {
owner.setOnViewTreeOwnersAvailable {
if (!disposed) {
val lifecycle = it.lifecycleOwner.lifecycle
lastContent = content
if (addedToLifecycle == null) {
addedToLifecycle = lifecycle
// this will call ON_CREATE synchronously if we already created
lifecycle.addObserver(this)
} else if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
original.setContent {
...
LaunchedEffect(owner) {
owner.boundsUpdatesAccessibilityEventLoop()
}
LaunchedEffect(owner) {
owner.boundsUpdatesContentCaptureEventLoop()
}
CompositionLocalProvider(LocalInspectionTables provides inspectionTable) {
// 안드로이드 Context에 대한 정보 제공
ProvideAndroidCompositionLocals(owner, content)
}
}
}
}
}
}
WrappedComposition 클래스에서는
- Android Lifecycle 이벤트 처리 (LifecycleEventObserver 구현)
- Android Context에 대한 정보를 Composition에 CompositionLocal 형태로 제공 (ProvideAndroidCompositionLocals)
- 키보드 가시성 변경이나 접근성과 같은 것들을 추적하기 위한 제어된 이펙트 시작
// ProvideAndroidCompositionLocals 내부 코드
CompositionLocalProvider(
LocalConfiguration provides configuration,
LocalContext provides context,
LocalLifecycleOwner provides viewTreeOwners.lifecycleOwner,
LocalSavedStateRegistryOwner provides viewTreeOwners.savedStateRegistryOwner,
LocalSaveableStateRegistry provides saveableStateRegistry,
LocalView provides owner.view,
LocalImageVectorCache provides imageVectorCache,
LocalResourceIdCache provides resourceIdCache,
LocalProvidableScrollCaptureInProgress provides scrollCaptureInProgress,
) {
ProvideCommonCompositionLocals(
owner = owner,
uriHandler = uriHandler,
content = content
)
}
Composition에 전달된 UiApplier 인스턴스가 트리의 루트 LayoutNode를 가리키는 것으로 시작한다는 것이 중요하다
val original = Composition(UiApplier(owner.root), parent)
마지막에 composition.setContent(content) 호출을 통해 Composition의 내용을 설정하고 Composable 함수가 실행되고 UI 트리가 만들어진다
Compose UI에서 Composition을 생성하는 사례들은
화면에 벡터를 그리는 VectorPainter, SubcomposeLayout 등이 있다
또한 Compose에서는
- Composition이 생성될 때마다 상위 CompositionContext가 전달될 수 있다 (null일 수도 있다)
- CoroutineContext를 통해 Composition 생성 시 recomposition Context를 전달할 수 있다
- 변경사항 적용과 트리 구체화에 사용된다
- 제공되지 않으면 기본적으로 EmptyCoroutineContext 사용
- Composition이 더 이상 필요하지 않을 경우 composition.dispose를 통해 폐기해야 한다
초기 컴포지션 과정 (The initial Composition process)
composition.setContent(content) 호출 시 컴포지션이 처음 구축된다
Composition.kt
override fun setContent(content: @Composable () -> Unit) {
composeInitial(content)
}
private fun composeInitial(content: @Composable () -> Unit) {
checkPrecondition(!disposed) { "The composition is disposed" }
this.composable = content
// composition 프로세스를 실행하기 위해 상위 composition에 위임
parent.composeInitial(this, composable)
}
초기 컴포지션에서 composition이 위임되는 순서
- Subcomposition의 경우 상위 composition으로 위임되고 루트 컴포지션까지 계속 진행
- 루트 Composition의 경우 상위 context는 Recomposer이다
- composeInitial 함수의 호출은 결국 recomposer.composeInitial 함수로 변환된다
- Recomposer는 초기 컴포지션을 구축하기 위해 중요한 작업을 수행한다
Recomposer의 composeInitial 함수에서 실행되는 중요한 작업들
- State 스냅샷 생성
- 모든 State 객체의 현재 값을 스냅샷으로 생성
- 다른 스냅샷의 변경사항으로부터 분리
- State 값들은 snapshot.enter(block: () -> T) 블록 내에서만 수정 가능
- 관찰자 설정
- Recomposer는 State 객체들에 대한 읽기, 쓰기 관찰자를 전달해 작업 발생 시 Composition에 알림
- 영향 받은 부분만 Recomposition을 위해 플래그 처리
- Composition 실행
- composition.composeContent(content) 블록에 snapshot.enter(block) 블록을 전달함으로써 스냅샷에 들어간다
- 실제 composition이 발생
- Composition 중 접근한 모든 State 객체들이 추적된다
- Composition 프로세스는 Composer에게 위임
- 변경사항 전파
- Composition 완료 시 변경된 state 값들은 현재 스냅샷에만 적용된 상태라 snapshot.apply 함수를 통해 변경사항 전파
Composer에게 위임된 실제 컴포지션 과정
- 재진입 금지
- Composition이 이미 진행 중인 경우 새로운 컴포지션을 시작할 수 없다
- 새 composition은 폐기되고 예외 발생
- Invalidation 처리
- 보류 중인 invalidation이 있으면, 이를 Composer가 유지하는 invalidation 목록으로 복사
- RecomposeScope의 invalidation 요청을 추적
- Composition 상태 설정
- isComposing 플래그를 true로 설정하여 컴포지션이 시작되었음을 표시
- RootGroup 시작
- startRoot()를 호출하여 슬롯 테이블에서 컴포지션의 루트 그룹을 시
- 필요한 필드와 구조 초기화
- Content Group 시작
- startGroup을 호출하여 슬롯 테이블에서 content의 group을 시작
- Content 실행
- content 람다식을 호출하여 모든 변경사항을 방출
- 컴포저블 함수들이 실행되고 UI 요소들이 생성
- Group 종료
- endGroup을 호출하여 슬롯 테이블에서 그룹을 종료
- Root 종료
- endRoot()를 호출하여 컴포지션을 종료
- Composition 상태 업데이트
- isComposing 플래그를 false로 변경하여 컴포지션이 완료되었음을 표시
초기 Composition 후 변경 사항 적용 (Applying changes after initial Composition)
초기 컴포지션 이후 Applier는 composition.applyChanges 함수를 호출해 기록된 변경사항을 실제 UI에 적용하라는 알림을 받는다
Applier 호출 과정
applier.onBeginChanges 함수를 먼저 호출해 변경사항 적용 시작을 알리고
각 변경사항에 필요한 Applier와 SlotWriter 인스턴스를 전달하고
모든 변경 사항이 적용된 후 applier.onEndChanges 함수를 호출해 변경사항 적용 종료를 알린다
이후 등록된 모든 RememberedObserver를 전달한다
RememberObserver 계약을 구현하는 모든 클래스는 composition에 들어가거나 나갈 때 알림을 받을 수 있다
LaunchedEffect나 DisposableEffect 등의 Effect들이 RememberedObserver를 구현하고 있어 Composition 내의 Composable 수명 주기에 따라 Effect를 제한할 수 있다
internal class LaunchedEffectImpl(
parentCoroutineContext: CoroutineContext,
private val task: suspend CoroutineScope.() -> Unit
) : RememberObserver { ... }
private class DisposableEffectImpl(
private val effect: DisposableEffectScope.() -> DisposableEffectResult
) : RememberObserver { ... }
'Compose' 카테고리의 다른 글
[ComposeInternals] 컴포즈 UI (Compose UI) - (1) (0) | 2025.04.03 |
---|---|
[ComposeInternals] 컴포즈 런타임 (ComposeRuntime) - (3) Recomposer (0) | 2025.03.30 |
[ComposeInternals] 컴포즈 런타임 (ComposeRuntime) - (1) Composer (0) | 2025.03.16 |
[ComposeInternals] Composable 함수들 (0) | 2025.03.03 |
[Compose] Compose 아키텍처 레이어링 (0) | 2025.02.23 |