코린이 개발로그
Compose ViewModel 본문
viewModel()
Android 를 개발할때 ViewModel 은 필수적인 요소로 사용되고있습니다.
Compose 로 개발할때도 ViewModel 은 여전히 많이 사용됩니다.
일반적인 Compose 의 예제를 확인했을때 우리는 Compose 에서 제공하는 viewModel() 함수를 주로사용하는것을 볼 수 있습니다.
@Composable
MyScreen() {
val viewModel = viewModel()
...
}
일반적으로 사이드 프로젝트나 비교적 간단한 어플리케이션의 경우에는 해당 함수를 간단하게 사용해도 큰 문제가 없습니다.
하지만 해당 함수는 몇가지 파라미터를 받고있습니다.
@Suppress("MissingJvmstatic")
@Composable
public inline fun <reified VM : ViewModel> viewModel(
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
},
key: String? = null,
factory: ViewModelProvider.Factory? = null,
extras: CreationExtras = if (viewModelStoreOwner is HasDefaultViewModelProviderFactory) {
viewModelStoreOwner.defaultViewModelCreationExtras
} else {
CreationExtras.Empty
}
): VM = viewModel(VM::class, viewModelStoreOwner, key, factory, extras)
각각 파라미터에 대한 설명을 보겠습니다.
viewModelStoreOwner - The owner of the ViewModel that controls the scope and lifetime of the returned ViewModel. Defaults to using LocalViewModelStoreOwner.
viewModel 의 생명주기를 관리해주는 값의 파라미터입니다.
key - The key to use to identify the ViewModel.
viewModel 을 식별하기위한 키로 보입니다.
factory - The ViewModelProvider. Factory that should be used to create the ViewModel or null if you would like to use the default factory from the LocalViewModelStoreOwner
ViewModelFactory 를 파라미터로 받고있습니다. 파라미터를 가진 viewModel 을 생성할경우 ViewModelFactory 를 통해 커스터마이징이 필요합니다.
extras - The default extras used to create the ViewModel.
ViewModel 을 생성할때 필요한 인자를 전달할 수 있는 extras 입니다.
viewModelStoreOwner
우리가 Android View 에서 ViewModel 을 사용했을때는 context 를 제공받아 해당 Activity 나 Fragment 의 생명주기를 따라가게 됩니다.
하나의 Activity 에서 여러 Fragment 를 사용하게 되는 경우에는 같은 데이터를 공유하기위해 context 를 requireActivity() 를 통해 제공받아 해당 context 를 통해 ViewModel 을 생성하게 됩니다.
viewModelStoreOwner 또한 같은 논리로 Compose 에서의 생명주기를 관리하는 객체입니다.
/**
* The CompositionLocal containing the current [ViewModelStoreOwner].
*/
public object LocalViewModelStoreOwner {
private val LocalViewModelStoreOwner =
compositionLocalOf<ViewModelStoreOwner?> { null }
/**
* Returns current composition local value for the owner or `null` if one has not
* been provided nor is one available via [findViewTreeViewModelStoreOwner] on the
* current [androidx.compose.ui.platform.LocalView].
*/
public val current: ViewModelStoreOwner?
@Composable
get() = LocalViewModelStoreOwner.current ?: findViewTreeViewModelStoreOwner()
/**
* Associates a [LocalViewModelStoreOwner] key to a value in a call to
* [CompositionLocalProvider].
*/
public infix fun provides(viewModelStoreOwner: ViewModelStoreOwner):
ProvidedValue<ViewModelStoreOwner?> {
return LocalViewModelStoreOwner.provides(viewModelStoreOwner)
}
}
default 값인 LocalViewModelStoreOwner.current 코드를 보겠습니다.
내부 변수인 current 는 기본적으로 findViewTreeViewModelStoreOwner() 함수를 사용하고 있으며 해당 함수는 아래와 같습니다.
@Composable
internal actual fun findViewTreeViewModelStoreOwner(): ViewModelStoreOwner? =
LocalView.current.findViewTreeViewModelStoreOwner()
즉 LocalView 의 ViewModelStoreOwner 를 반환하게됩니다. 그렇다면 여기서 LocalView 는 무엇일까요?
/**
* The CompositionLocal containing the current Compose [View].
*/
val LocalView = staticCompositionLocalOf<View> {
noLocalProvidedFor("LocalView")
}
설명을 보면 가장최근의 Compose "View" 를 담고있는 compositionLocalOf 로 볼수있습니다. 즉 최상위의 AndroidComposeView 를 가져오게됩니다.
즉 하나의 Compose View 내부에서 여러가지 내부 Composable 함수가 불려도 결과적으론 같은 최상단의 AndroidComposeView의 ViewModelStoreOwner 를 반환하게 됩니다.
@Composable
fun MainScreen() {
println(LocalViewModelStoreOwner.current) // com.xx.composestudy.MainActivity@3c7e4ed
SubScreenA()
SubScreenB()
}
@Composable
fun SubScreenA(){
println(LocalViewModelStoreOwner.current) // com.xx.composestudy.MainActivity@3c7e4ed
}
...
위의 함수에서 볼수있듯이 하나의 Composable 함수내에선 같은 결과값을 리턴하는것을 볼 수 있습니다.
하지만 우리는 Compose 도입이후 하나의 Fragment 를 대채했고 기존에 우리는 Fragment 의 생명주기를 따르는 ViewModel 을 사용하고있습니다.
이런 경우에는 NavHost 를 사용해여 해결하기도 합니다.
Factory
ViewModel 이 파라미터가 있는 경우에는 ViewModelProvider 의 Factory 를 사용하여 커스터 마이징이 필요합니다.
기존의 경우에는 ViewModelProvier를 직접적으로 사용하는 경우도 있지만 해당 viewModel() 함수를 사용하기위해선 ViewModelProvider 의 Factory 를 만들어야합니다.
일반적으로 ViewModel 을 생성할때 External 클래스나 Repository 클래스를 파라미터로 두는 경우들이 있습니다. 그런경우에는 viewModel() 함수를 사용했을때 자동적으로 클래스를 생성할 수 없게됩니다. 따라서 해당 클래스의 파라미터들을 생성해주는 ViewModelProvider.Factory 를 만들어줘야합니다.
class MyViewModel(
private val myRepository: MyRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// ViewModel logic
// ...
// Define ViewModel factory in a companion object
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
modelClass: Class<T>,
extras: CreationExtras
): T {
// Get the Application object from extras
val application = checkNotNull(extras[APPLICATION_KEY])
// Create a SavedStateHandle for this ViewModel from extras
val savedStateHandle = extras.createSavedStateHandle()
return MyViewModel(
(application as MyApplication).myRepository,
savedStateHandle
) as T
}
}
}
}
이렇게 해당 ViewModel 클래스 내부에 작성하면 가독성 및 더 쉬운 검색을 지원합니다.
이렇게 작성된 코드는 viewModel() 함수의 파라미터로 사용됩니다.
@Composable
MyScreen() {
val viewModel = viewModel(factory = MyViewModel.Factory)
...
}
당연히 기존의 Android View 에서도 사용이 가능합니다.
class MyActivity : AppCompatActivity() {
private val viewModel: MyViewModel by viewModels { MyViewModel.Factory }
...
}
또 Factory 를 정의하는 코드는 ViewModel 팩토리 DSL 을 사용하여 더 자연스러운 Kotlin API 로 생성할 수 있습니다.
class MyViewModel(
private val myRepository: MyRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// ViewModel logic
// Define ViewModel factory in a companion object
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val savedStateHandle = createSavedStateHandle()
val myRepository = (this[APPLICATION_KEY] as MyApplication).myRepository
MyViewModel(
myRepository = myRepository,
savedStateHandle = savedStateHandle
)
}
}
}
}
결론
Compose 에서 viewModel 을 생성하는 viewModel API 에 대해 공부해 보았습니다. 기존에 Activity 와 Fragment 의 생명주기를 따라가던 ViewModel 을 어떤식으로 Compose 에서도 효율적으로 사용하기 위해선 위와같은 파라미터들을 잘 이해하는게 중요한거같습니다.
'Kotlin & Android' 카테고리의 다른 글
DI 와 Hilt 와 Repository패턴 (0) | 2021.11.29 |
---|---|
Android ListView Widget을 만들어보자 (0) | 2021.11.29 |
AAC 와 MVVM (0) | 2021.11.09 |
FireBase Gradle build 오류 (2) | 2021.04.24 |