Composable 내부에서 액티비티의 메서드 호출이 필요한 상황은 종종 발생한다.
예를 들어 뒤로가기, 권한 요청, 외부 앱 호출 등 시스템과 상호작용해야 하는 경우가 있다.
@Composable
private fun Screen(
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
) {
IconButton(onClick = {
// TODO: 액티비티 종료
}) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
}
}
}
이를 해결하는 방법은 대표적으로 두 가지가 있을 것이다. 인자를 통해 Activity
에서 내려받거나, CompositionLocal
을 사용하거나.
// 방법 1: Activity에서 인자를 통해 내려주기
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
TestAppTheme {
Screen(
onBackButtonClick = ::finish,
modifier = Modifier.fillMaxSize()
)
}
}
}
}
@Composable
private fun Screen(
onBackButtonClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
) {
IconButton(onClick = onBackButtonClick) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
}
}
}
// 방법 2: CompositionLocal을 사용하기
@Composable
private fun Screen(
modifier: Modifier = Modifier,
) {
val activity: Activity? = LocalActivity.current
Column(
modifier = modifier
) {
IconButton(onClick = { activity?.finish() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
}
}
}
인자와 CompositionLocal
, 어떤 것을 선택할지 결정하는 기준은 무엇일까?
CompositionLocal
?CompositionLocal
은 Composable 트리에 데이터를 암시적으로 전달하는 도구이다.
일반적으로 UI 트리의 하위에 있는 컴포저블에 데이터를 전달할 때는 인자를 사용해야 한다. 이렇게 하면 각 컴포저블의 의존성을 명시적으로 확인할 수 있다.
하지만 거의 모든 컴포저블에서 자주 사용되는 값들(테마 등)의 경우도 전부 인자를 사용하면 번거로울 수 있다.
@Composable
fun MyApp() {
val colors = colors()
...
}
// 트리 계층 구조의 하위에 존재하는 컴포저블
@Composable
fun SomeTextLabel(labelText: String, colors: Colors ...) {
Text(
text = labelText,
color = colors.onPrimary
...
)
}
따라서 인자 없이 암시적으로 필요한 의존성을 충족시키기 위해 컴포즈는 CompositionLocal
을 사용한다. 이를 통해 UI 트리의 스코프 동안 필요한 데이터를 제공하는 객체를 생성하고 암시적으로 활용할 수 있다.
대표적으로 MaterialTheme
가 내부적으로 CompositionLocal
을 사용하고 있다.
object MaterialTheme {
val colorScheme: ColorScheme
@Composable @ReadOnlyComposable get() = LocalColorScheme.current
val typography: Typography
@Composable @ReadOnlyComposable get() = LocalTypography.current
val shapes: Shapes
@Composable @ReadOnlyComposable get() = LocalShapes.current
}
CompositionLocal
을 사용해야 하는가?안드로이드 공식문서에서 CompositionLocal
을 사용하기 좋은 몇 가지 조건을 확인할 수 있다.
좋은 기본값을 가질 수 있을 때
val LocalSpacing = compositionLocalOf { 0.dp } // 기본값: 0dp
val LocalUser = compositionLocalOf<User> { error("No User provided") } // 기본값 X
@Composable
fun Example() {
val spacing = LocalSpacing.current
Text(
"Hello!",
modifier = Modifier.padding(spacing)
)
val user = LocalUser.current
Text("Hello, ${user.name}!")
}
기본값이 없으면 테스트나 프리뷰에서 매번 값을 명시적으로 제공해야 해서 번거롭고, 실수로 누락 시 오류로 이어질 수 있다.
트리 전역 또는 특정 하위 트리에 걸쳐 의미 있는 값일 때
CompositionLocal
은 하위 모든 컴포저블이 잠재적으로 접근할 수 있다는 점이 특징이다. 따라서 일부 컴포저블만 쓰는 값이라면 적합하지 않다.
val LocalSpacing = compositionLocalOf { 8.dp }
@Composable
fun MyApp() {
CompositionLocalProvider(LocalSpacing provides 16.dp) {
Column {
Header()
Content()
Footer()
}
}
}
@Composable
fun Header() {
val spacing = LocalSpacing.current
Text("Header", modifier = Modifier.padding(spacing))
}
@Composable
fun Content() {
val spacing = LocalSpacing.current
Text("Content", modifier = Modifier.padding(spacing))
}
@Composable
fun Footer() {
val spacing = LocalSpacing.current
Text("Footer", modifier = Modifier.padding(spacing))
}
여기서 LocalSpacing
은 트리 전역적으로 의미가 있는 값이므로 CompositionLocal
을 사용하는 것이 자연스럽다.
뷰모델 같은 화면 단위의 구체적 의존성을 담지 않을 것
화면 전체의 ViewModel
을 CompositionLocal
로 노출하는 건 권장되지 않는다. 모든 자식이 접근할 수 있지만 실제로 필요한 건 일부 컴포저블뿐이기 때문이다.
// 부적합한 예시
val LocalCardViewModel = compositionLocalOf<CardViewModel> { error("No VM") }
@Composable
fun CardScreen() {
val viewModel = LocalCardViewModel.current
Column {
CardHeader()
CardContent()
CardFooter()
}
}
@Composable
fun CardHeader() {
val viewModel = LocalCardViewModel.current
Text("User: ${viewModel.userName}")
}
LocalActivity
?LocalActivity
는 시스템 레벨 API에 접근할 수 있도록 Compose에서 제공하는 특수한 CompositionLocal
이다.
이를 통해 Activity
가 필요한 권한 요청, startActivityForResult
, WindowInsets
제어 같은 상황에서 사용할 수 있다.
그러나 LocalActivity
를 남용하면, 컴포저블이 Activity
에 직접 의존하게 되고, 재사용성과 테스트성이 떨어진다.
따라서 일반적인 UI 이벤트(뒤로가기 버튼 등)는 콜백 인자를 통해 외부에서 전달하는 방식이 더 바람직하다.
@Composable
fun CardScreen(onBackClick: () -> Unit) {
Button(onClick = onBackClick) { Text("Back") }
}
이 방식은 화면 구조가 바뀌거나, Compose Navigation을 사용하더라도 유연하게 대응할 수 있다.
CompositionLocal
: 전역적, 환경적 값에 적합. 기본값이 안전하고, 거의 모든 하위 컴포저블이 필요로 하는 값이어야 한다.LocalActivity
: 학습용이나 불가피한 시스템 API 접근 시 사용할 수 있지만, 일반 UI 이벤트에서는 인자 전달 방식을 우선적으로 선택하는 것이 좋다.Locally scoped data with CompositionLocal | Jetpack Compose | Android Developers