Compose를 사용하지 않은 기존 Android 앱은 Activity, Fragment, 기타 수명주기에 따라 상태관리를 수행해야 합니다.
이를 위해 수명 주기에 맞춰서 생성되고 소멸되는 상태관리 매니저가 필요했고, Android Jetpack에서는 lifecycle-viewmodel을 통해서 수명 주기와 연결된 상태관리 매니저를 쉽게 사용할 수 있었습니다.
또한 이러한 수명주기를 고려한 의존성 주입법이 필요했고, Hilt를 사용해 이를 극복했습니다.
하지만 Compose를 도입하면 기존 생명주기와는 다른 Composable의 생명주기를 따라야 합니다.
따라서 만약 앱의 A부터 Z까지 Compose만을 사용해서 구현한다면 굳이 ViewModel을 사용하지 않고 임의의 클래스를 구현해 상태관리에 사용해도 됩니다.
여기서 말하는
ViewModel
은 lifecycle-viewmodel을 말하는 것이지 일반적인 MVVM의 ViewModel을 이야기 하는 것이 아닙니다.
Compose를 처음 접할 당시엔 ViewModel을 사용했는데 다음과 같은 이유였습니다.
하지만 점차 Compose 앱을 구현하면서 불편함에 마주했는데 이는 특히 Hilt에서 가장 크게 느꼈습니다. Hilt가 지원하는 클래스가 아래와 같이 제한적이라 ViewModel에 필요한 의존성 및 필요한 생성자 매개변수를 주입하기 매우 까다로웠기 때문입니다.
Application(@HiltAndroidApp을 사용하여)
ViewModel(@HiltViewModel을 사용하여)
Activity
Fragment
View
Service
BroadcastReceiver
Hilt를 사용하면 의존성 주입에 필요한 boilerplate code를 줄일 수 있는 장점이 있긴 했지만, Hilt의 큰 장점 중 하나인 수명 주기 관리가 Compose에서는 (아직까지) 큰 의미를 가지지 못했고, 의존성 주입에 제한을 가지는 부분이 훨씬 큰 불편함으로 다가와서 Hilt를 제거하게 됐습니다.
대신, Flutter와 거의 유사한 방식으로 전역 상태관리 및 의존성 주입에 유용하게 사용할 수 있는 CompositionLocal이 있어 이것을 도입했습니다.
CompositionLocal
은 Composable의 하위 계층에 Provider Pattern을 사용해 데이터를 전달할 수 있는 방식입니다.
예시와 함께 CompositionLocal
사용법을 살펴보겠습니다.
먼저 compositionLocalOf
함수로 CompositionLocal
객체를 생성합니다.
compositionLocalOf
의 defaultFactory
로 의존성의 기본값을 주입할 수 있습니다.
일반적으로 CompositionLocal
은 테스트 및 Preview 용이성을 위해 적절한 기본값을 제공하는 것이 좋다고 합니다.
// LocalElevations.kt file
data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp)
// Define a CompositionLocal global object with a default
// This instance can be accessed by all composables in the app
val LocalElevations = compositionLocalOf { Elevations() }
앞에서 만든 CompositionLocal
인 LocalElevation
에 주입할 의존성인 elevations
을 provides
infix 함수를 사용해 연결하면(LocalElevations provides elevations
) providedValue
를 얻을 수 있습니다.
이렇게 얻은 providedValue
를 CompositionLocalProvider
Composable 함수에 매개변수로 전달합니다.
그리고 CompositionLocalProvider
에 elevations
를 사용하고자 하는 Composable들을 구성합니다.
// MyActivity.kt file
class MyActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// Calculate elevations based on the system theme
val elevations = if (isSystemInDarkTheme()) {
Elevations(card = 1.dp, default = 1.dp)
} else {
Elevations(card = 0.dp, default = 0.dp)
}
// Bind elevation as the value for LocalElevations
CompositionLocalProvider(LocalElevations provides elevations) {
// ... Content goes here ...
// This part of Composition will see the `elevations` instance
// when accessing LocalElevations.current
}
}
}
}
CompositionLocalProvider
하위 계층의 Composables들은 LocalElevation
를 사용해 elevations
에 접근할 수 있게 됩니다.
@Composable
fun SomeComposable() {
// Access the globally defined LocalElevations variable to get the
// current Elevations in this part of the Composition
Card(elevation = LocalElevations.current.card) {
// Content
}
}
Provider 패턴은 Compose, Flutter와 같은 선언형 UI 도구에 적합한 방식이었고 CompositionLocal
라는 도구로 사용할 수 있게 됐습니다.
하지만 암시적으로 의존성을 주입하기 떄문에 Composable의 동작 추론을 어렵게 할 수도 있어 Repository, Theme, Service와 같은 성격의 전역 상태가 아닌 경우에는 사용에 유의해야 합니다.
애초에 Compose에서는 Composable의 재사용성을 위해 가능한 필요한 상태만을 하위 계층에 전달하도록 권고하고 있기 때문에 ViewModel을 통째로 CompositionLocal
으로 넘기는 등의 동작은 가능한 피해야 할 것입니다.