Compose에서 Composable 함수에서 사용할 수 있는 다양한 기능들을 제공합니다. 그중 대표적인 LocalContext
를 사용해 Context를 얻어오는 흐름을 살펴보겠습니다.
코드를 살펴본 기준은 AndroidX 최신 코드 기준입니다.
샘플 주소 : https://github.com/Pluu/LocalContextSample
LocalContext
는 Context가 필요한 경우에 사용됩니다. 기존 View 시스템에서도 동일하듯 Context는 자주 변경되지 않은 객체이므로 staticCompositionLocalOf으로 정의되어 있습니다.
val LocalContext = staticCompositionLocalOf<Context> { noLocalProvidedFor("LocalContext") }
소스 출처 : https://github.com/androidx/androidx/blob/androidx-main/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidCompositionLocals.android.kt#L47-L48
실제 CompositionLocalProvider를 사용하여 전달하는 곳에서 역으로 코드를 살펴보겠습니다.
CompositionLocalProvider를 사용하여 LocalContext의 값을 전달하는 가장 안쪽은 ProvideAndroidCompositionLocals
입니다.
실제 전달되는 context는 ProvideAndroidCompositionLocals Composable 함수의 파라미터로 전달된 AndroidComposeView
가 가지는 View의 Context입니다.
@Composable
@OptIn(ExperimentalComposeUiApi::class)
internal fun ProvideAndroidCompositionLocals(
owner: AndroidComposeView,
content: @Composable () -> Unit
) {
val view = owner
val context = view.context
...
CompositionLocalProvider(
...,
LocalContext provides context,
...
) {
ProvideCommonCompositionLocals(owner = owner, uriHandler = uriHandler, content = content)
}
}
소스 출처 : https://github.com/androidx/androidx/blob/androidx-main/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidCompositionLocals.android.kt#L72-L124
위 소스에는 생략했지만, ProvideAndroidCompositionLocals에서는 기존 View에서 사용되는 기본 기능들을 Composable에서 활용할 수 있도록 여러 정의가 존재합니다.
1번 과정의 ProvideAndroidCompositionLocals을 호출하는 곳은 WrappedComposition입니다.
ProvideAndroidCompositionLocals 호출하는 곳을 살펴보면, 전달되는 AndroidComposeView는 클래스 생성자로 전달된 Owner입니다.
private class WrappedComposition(val owner: AndroidComposeView, val original: Composition) :
Composition, LifecycleEventObserver, CompositionServices {
...
override fun setContent(content: @Composable () -> Unit) {
owner.setOnViewTreeOwnersAvailable {
if (!disposed) {
val lifecycle = it.lifecycleOwner.lifecycle
lastContent = content
if (addedToLifecycle == null) {
addedToLifecycle = lifecycle
// 이미 생성한 경우 ON_CREATE를 동기식으로 호출
lifecycle.addObserver(this)
} else if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
original.setContent {
...
CompositionLocalProvider(LocalInspectionTables provides inspectionTable) {
ProvideAndroidCompositionLocals(owner, content)
}
}
}
}
}
}
...
}
소스 출처 : https://github.com/androidx/androidx/blob/androidx-main/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/Wrapper.android.kt#L110-L179
2번 과정의 WrappedComposition을 호출하는 곳은 AbstractComposeView의 확장함수인 setContent입니다.
WrappedComposition의 파라미터로 전달되는 owner는 AbstractComposeView가 가지는 context입니다. 부모 AbstractComposeView의 Context가 자식 AndroidComposeView에 전달되고서 WrappedComposition에 전달합니다.
internal fun AbstractComposeView.setContent(
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
...
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 {
...
val wrapped =
owner.view.getTag(R.id.wrapped_composition_tag) as? WrappedComposition
?: WrappedComposition(owner, Composition(UiApplier(owner.root), parent)).also {
owner.view.setTag(R.id.wrapped_composition_tag, it)
}
wrapped.setContent(content)
...
return wrapped
}
소스 출처 : https://github.com/androidx/androidx/blob/androidx-main/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/Wrapper.android.kt
AndroidComposeView는 기존 View 시스템 요소 중 하나인 ViewGroup을 상속한 구현 클래스입니다. ViewGroup에서도 View의 자식 클래스이므로 Context를 얻을 수 있습니다.
https://developer.android.com/reference/android/view/View#getContext()
이 클래스는 internal이므로 public에서 주입되는 곳을 찾아야 합니다.
internal class AndroidComposeView(context: Context, coroutineContext: CoroutineContext) :
ViewGroup(context), Owner, ViewRootForTest, MatrixPositionCalculator, DefaultLifecycleObserver {
...
}
소스 출처 : https://github.com/androidx/androidx/blob/androidx-main/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
AbstractComposeView도 AndroidComposeView와 동일하게 ViewGroup을 상속한 추상 클래스입니다
abstract class AbstractComposeView
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
ViewGroup(context, attrs, defStyleAttr) {
...
}
AbstractComposeView는 Activity/Fragment/View에서 Compose를 사용할 때 필요한 ComposeView의 추상 클래스이기도 합니다.
지금까지의 결과로 CompositionLocalProvider로 LocalContext에 전달되는 값은 AbstractComposeView의 Context라는 것을 알 수 있습니다.
이어서 주요 케이스의 Context를 체크해 보겠습니다.
Acitivity에서 사용하는 setContent
에서는 Activity의 Context가 ComposeView에 전달됩니다.
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
val existingComposeView =
window.decorView.findViewById<ViewGroup>(android.R.id.content).getChildAt(0) as? ComposeView
if (existingComposeView != null)
...
else
ComposeView(this).apply {
...
}
}
소스 출처 : https://github.com/androidx/androidx/blob/androidx-main/activity/activity-compose/src/main/java/androidx/activity/compose/ComponentActivity.kt#L50-L74
Fragment에서 사용하는 setContent
에서는 Fragment의 requireContext()
가 ComposeView에 전달됩니다.
fun Fragment.content(content: @Composable () -> Unit): ComposeView {
return ComposeView(requireContext()).apply {
...
}
}
소스 출처 : https://github.com/androidx/androidx/blob/androidx-main/fragment/fragment-compose/src/main/java/androidx/fragment/compose/Fragment.kt
아래 결과는 Compose에서 LocalContext를 확인 모습입니다.
Activity + FragmentContainer | Activity + Navigation |
---|---|
Compose에서 편하게 사용할 수 있는 기능들은 다수 존재하지만, 흐름을 재정리를 위해 조사해 봤습니다.
역순으로 복잡했지만 정리하자면 아래와 같습니다.
Activity/Fragment의 Context
↓
ComposeView
↓
AbstractComposeView
↓
WrappedComposition
↓
ProvideAndroidCompositionLocals
comments powered by Disqus
Subscribe to this blog via RSS.
LazyColumn/Row에서 동일한 Key를 사용하면 크래시가 발생하는 이유
Posted on 30 Nov 2024