Compose에서 Context는 어떻게 얻을 수 있나요?

Compose에서 Context는 어떻게 얻을 수 있나요?

Nov 10, 2024. | By: pluulove

Compose에서 Composable 함수에서 사용할 수 있는 다양한 기능들을 제공합니다. 그중 대표적인 LocalContext를 사용해 Context를 얻어오는 흐름을 살펴보겠습니다.


코드를 살펴본 기준은 AndroidX 최신 코드 기준입니다.

  • 아직 릴리즈 안 된 내용이 포함되어 있을 수도 있습니다.

샘플 주소 : https://github.com/Pluu/LocalContextSample


LocalContext의 정의

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

LocalContext의 객체 주입 흐름

실제 CompositionLocalProvider를 사용하여 전달하는 곳에서 역으로 코드를 살펴보겠습니다.

1) ProvideAndroidCompositionLocals

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에서 활용할 수 있도록 여러 정의가 존재합니다.

2) WrappedComposition

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

3) AbstractComposeView#setContent

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

4) AndroidComposeView

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

5) AbstractComposeView

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를 체크해 보겠습니다.

주요 케이스 체크

Activity에서의 LocalContext

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에서의 LocalContext

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

LocalContext 확인 샘플

아래 결과는 Compose에서 LocalContext를 확인 모습입니다.

Activity + FragmentContainer Activity + Navigation

정리

Compose에서 편하게 사용할 수 있는 기능들은 다수 존재하지만, 흐름을 재정리를 위해 조사해 봤습니다.

역순으로 복잡했지만 정리하자면 아래와 같습니다.

Activity/Fragment의 Context
↓
ComposeView
↓
AbstractComposeView
↓
WrappedComposition
↓
ProvideAndroidCompositionLocals

comments powered by Disqus

Currnte Pages Tags

Android AndroidX

About

Pluu, Android Developer Blog Site

이전 블로그 링크 :네이버 블로그

Using Theme : SOLID SOLID Github

Social Links