viewModel을 생성하는 방법은 크게 viewModel(), hiltViewModel()을 이용하는 2가지의 방법이 있다고 보여진다. 이 두개의 차이점이 궁금해졌고, 이 글에서는 이 두개의 방법에 대해서 알아보려 한다.
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)
위 코드는 viewModel()의 내부 코드입니다. 컴포저블에서 생성한 ViewModel의 ViewModelStoreOwner는 LocalViewModelStoreOwner가 제공해주는데 NavHost 컴포저블 범위가 아니라면 viewModelStoreOwner.get 함수를 통해서 얻은 ViewModelStoreOwner를 반환합니다.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
TesdtTheme {
var currentScreen by remember { mutableStateOf("home") }
Crossfade(targetState = currentScreen) { screen ->
when (screen) {
"home" ->
MainScreen { currentScreen = "sub" }
"sub" ->
SubScreen { currentScreen = "home" }
}
}
}
}
}
}
@Composable
fun MainScreen(
mainViewModel: MainViewModel = viewModel(),
onNavigateToSubScreen: () -> Unit = {}
) {
Text(
text = "MainScreen",
modifier = Modifier
.fillMaxSize()
.clickable { onNavigateToSubScreen() }
)
Log.d("MainViewModel", "MainScreen : $mainViewModel")
}
@Composable
fun SubScreen(
mainViewModel: MainViewModel = viewModel(),
onNavigateToMainScreen: () -> Unit = {}
) {
Log.d("MainViewModel", "SubScreen : $mainViewModel")
Text(text = "SubScreen",
Modifier
.fillMaxSize()
.clickable { onNavigateToMainScreen() })
}
viewModel로 생성한 경우 최상위 뷰를 VIewModelStoreOwner로 지정을 합니다. 주로 액티비티, 프래그먼트가 됩니다. 이 경우에는 화면에 보이는 컴포저블 함수가 바뀌더라도 상위 뷰의 인스턴스는 아직 유효하기 때문에 같은 ViewModel 인스턴스를 부르게 됩니다.
아래는 위 코드를 실행시켰을 때의 결과입니다. 같은 뷰모델이 호출되는 것을 볼 수 있습니다.
각각의 화면에서 다른 뷰모델 인스턴스를 원한다면, 기존의 ViewModel 인스턴스를 소멸시키고 새로운 인스턴스를 할당하는 방법을 취해야 합니다. 하지만 이렇게 되면 보일러 플레이트 코드와 번거롭다는 단점이 있습니다.
hiltViewModel()
/**
* Returns an existing
* [HiltViewModel](https://dagger.dev/api/latest/dagger/hilt/android/lifecycle/HiltViewModel)
* -annotated [ViewModel] or creates a new one scoped to the current navigation graph present on
* the {@link NavController} back stack.
*
* If no navigation graph is currently present then the current scope will be used, usually, a
* fragment or an activity.
*
* @sample androidx.hilt.navigation.compose.samples.NavComposable
* @sample androidx.hilt.navigation.compose.samples.NestedNavComposable
*/
@Composable
inline fun <reified VM : ViewModel> hiltViewModel(
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
): VM {
val factory = createHiltViewModelFactory(viewModelStoreOwner)
return viewModel(viewModelStoreOwner, factory = factory)
}
@Composable
@PublishedApi
internal fun createHiltViewModelFactory(
viewModelStoreOwner: ViewModelStoreOwner
): ViewModelProvider.Factory? = if (viewModelStoreOwner is NavBackStackEntry) {
HiltViewModelFactory(
context = LocalContext.current,
navBackStackEntry = viewModelStoreOwner
)
} else {
// Use the default factory provided by the ViewModelStoreOwner
// and assume it is an @AndroidEntryPoint annotated fragment or activity
null
}
hiltViewModel 내부로 들어가보면 위와 같은 코드를 볼 수가 있습니다.
주석을 해석 해보면 hiltViewModel은 기존의 HiltViewModel을 반환하거나 현재 네비게이션 그래프에 스코프된 새 ViewModel을 생성하고 네비게이션 그래프가 없다면 기본적으로 프래그먼트나 액티비티 스코프에 맞춰 ViewModel을 제공한다는 것을 알 수 있습니다.
코드에서는 createHiltViewModelFactory 함수가 그 역할을 하는 것을 알 수 있습니다. 해당 함수를 이용해서 viewModelStoreOwner를 인자로 받아 NavBackStackEntry에 포함된 경우 HiltViewModelFactory 함수에 context, navBackStackEntry를 넘겨서ViewModel을 생성하고 아닌 경우에는 null값을 주어 기본 ViewModel을 생성하는 동작을 합니다.
/**
* Creates a [ViewModelProvider.Factory] to get
* [HiltViewModel](https://dagger.dev/api/latest/dagger/hilt/android/lifecycle/HiltViewModel)
* -annotated `ViewModel` from a [NavBackStackEntry].
*
* @param context the activity context.
* @param navBackStackEntry the navigation back stack entry.
* @return the factory.
* @throws IllegalStateException if the context given is not an activity.
*/
@JvmName("create")
public fun HiltViewModelFactory(
context: Context,
navBackStackEntry: NavBackStackEntry
): ViewModelProvider.Factory {
val activity = context.let {
var ctx = it
while (ctx is ContextWrapper) {
if (ctx is Activity) {
return@let ctx
}
ctx = ctx.baseContext
}
throw IllegalStateException(
"Expected an activity context for creating a HiltViewModelFactory for a " +
"NavBackStackEntry but instead found: $ctx"
)
}
return HiltViewModelFactory.createInternal(
activity,
navBackStackEntry,
navBackStackEntry.arguments,
navBackStackEntry.defaultViewModelProviderFactory,
)
}
위에서 말한 인자를 받은 HiltViewModelFactory 내부 코드는 위와 같습니다.
코드는 우선 인자로 받은 context가 Activity 타입인지 확인하고 맞다면 내부적으로 HitlViewModel을 생성하여 반환하고 아닌 경우 IllegalStateException 예외를 발생시킵니다.
정리해보자면
viewModel은 viewModelStoreOwner가 Activity, Fragment라서 여러 컴포저블 함수들에서 viewModel을 호출하더라도 생명주기에 의해서 같은 인스턴스가 제공이 된다. 방법으로는 컴포저블 함수 이동간의 ViewModel 인스턴스 해제를 하는 방법이 있을 것 같다.
hiltViewModel은 viewModelStoreOwner가 NavBackStackEntry라서 Navigation 컴포넌트의 각 Destination(화면 단위)로 인스턴스가 제공이 된다.
참고
https://developer.android.com/develop/ui/compose/libraries?hl=ko
https://velog.io/@wlsrhkd4023/Compose-hiltViewModel%EA%B3%BC-viewModel-%EC%B0%A8%EC%9D%B4
'안드로이드' 카테고리의 다른 글
[Android] Coroutine (0) | 2024.10.14 |
---|---|
[Compose] LaunchedEffect Unit, true (0) | 2024.10.05 |
[Compose] 중복 클릭 제어(Throttle) (0) | 2024.09.25 |
[Android] ViewModel에서의 context (0) | 2024.09.21 |
[Compose] Navigation back stack (0) | 2024.09.18 |