안녕하세요, 이번 포스팅에서는 저번 시리즈에 이어서 ViewModel 쪽을 알아보겠습니다.
지난 포스팅에서 멈춘 getHiltViewModelFactory 쪽부터 이어서 시작하겠습니다.
DefaultViewModelFactories.java
private ViewModelProvider.Factory getHiltViewModelFactory(
SavedStateRegistryOwner owner,
@Nullable Bundle defaultArgs,
@Nullable ViewModelProvider.Factory extensionDelegate
) {
ViewModelProvider.Factory delegate = extensionDelegate == null
? new SavedStateViewModelFactory(application, owner, defaultArgs)
: extensionDelegate;
return new HiltViewModelFactory(
owner, defaultArgs, keySet, delegate, viewModelComponentBuilder
);
}
여기서, SavedStateViewModelFactory를 가져오는 것을 보아 ViewModel에서 savedStateHandle을 사용할 수 있는 것을 알 수 있습니다.
이어서 HiltViewModelFactory 쪽을 보겠습니다.
HiltViewModelFactory.java
public HiltViewModelFactory(
@NonNull SavedStateRegistryOwner owner,
@Nullable Bundle defaultArgs,
@NonNull Set<String> hiltViewModelKeys,
@NonNull ViewModelProvider.Factory delegateFactory,
@NonNull ViewModelComponentBuilder viewModelComponentBuilder
) {
this.hiltViewModelKeys = hiltViewModelKeys;
this.delegateFactory = delegateFactory;
this.hiltViewModelFactory =
new AbstractSavedStateViewModelFactory(owner, defaultArgs) {
@NonNull
@Override
@SuppressWarnings("unchecked")
protected <T extends ViewModel> T create(
@NonNull String key, @NonNull Class<T> modelClass, @NonNull SavedStateHandle handle
) {
ViewModelComponent component =
viewModelComponentBuilder.savedStateHandle(handle).build();
Provider<? extends ViewModel> provider =
EntryPoints.get(component, ViewModelFactoriesEntryPoint.class)
.getHiltViewModelMap()
.get(modelClass.getName());
if (provider == null) {
throw new IllegalStateException(
"Expected the @HiltViewModel-annotated class '"
+ modelClass.getName()
+ "' to be available in the multi-binding of "
+ "@HiltViewModelMap but none was found.");
}
return (T) provider.get();
}
};
}
@EntryPoint
@InstallIn(ViewModelComponent.class)
public interface ViewModelFactoriesEntryPoint {
@HiltViewModelMap
Map<String, Provider<ViewModel>> getHiltViewModelMap();
}
@EntryPoint
@InstallIn(ActivityComponent.class)
interface ActivityCreatorEntryPoint {
@HiltViewModelMap.KeySet
Set<String> getViewModelKeys();
ViewModelComponentBuilder getViewModelComponentBuilder();
}
여기서, EntryPoint를 통해 ViewModel을 가져오기 위한 Provider와 ViewModelKey를 받습니다.
Provider는 JSR-330에 있는 인터페이스로, Dependency Lookup 기능을 제공합니다. Provider를 통해 직접 종속항목을 가져올 수 있습니다.
이 종속항목을 어떻게 가져올 수 있는지 보겠습니다. 먼저, 해당 ViewModel로 갑니다.
ScreenViewModel.kt
@HiltViewModel
class ScreenViewModel @Inject constructor() {
...
}
여기서, JSR-300의 Annotation인 @Inject Annotation과 @HiltViewModel Annotation을 통해 Hilt는 ScreenViewModel_Factory, ScreenViewModel_HiltModules, ... 을 생성합니다.
HiltViewModelMap을 알아보기 위해 ScreenViewModel_HiltModules 파일을 봅시다.
ScreenViewModel_HiltModules.java
@OriginatingElement(
topLevelClass = ScreenViewModel.class
)
public final class ScreenViewModel_HiltModules {
private ScreenViewModel_HiltModules() {
}
@Module
@InstallIn(ViewModelComponent.class)
public abstract static class BindsModule {
private BindsModule() {
}
@Binds
@IntoMap
@StringKey("...") <-- ViewModel의 package + Class Name이 들어갑니다.
@HiltViewModelMap
public abstract ViewModel binds(ScreenViewModel vm);
}
@Module
@InstallIn(ActivityRetainedComponent.class)
public static final class KeyModule {
private KeyModule() {
}
@Provides
@IntoSet
@HiltViewModelMap.KeySet
public static String provide() {
return ...; <-- ViewModel의 package + Class Name이 들어갑니다.
}
}
}
여기서, Dagger의 Multibinding 개념이 나옵니다.
Multibinding을 사용하면, 서로 다른 모듈에서 바인딩 되는 경우에서도 여러 객체를 Collections에 넣어 바인딩할 수 있습니다.
이러한 Multibinding을 사용하여 각 뷰모델의 모듈에서 바인딩을 해주어도 Collections에서 처리를 할 수 있습니다.
Multibinding에서 사용하는 Collections는 Set과 Map이 있습니다.
간략하게 설명해드리자면, Map Multibinding을 사용하고 싶은 항목에는 @IntoMap과 Map에서 사용할 Key를 지정해주면 됩니다. Set Multibinding은 @IntoSet을 지정해주면 됩니다.
여기선 HiltViewModelMap에서는 Map Multibinding, KeySet에서는 Set Multibinding을 적용한 걸 볼 수 있습니다.
이러한 뷰모델의 BindsModule은 ViewmodelComponent로, KeyModule은 ActivityRetainedComponent로 들어갑니다.
MainApplication_HiltComponents.java
@Subcomponent(
modules = {
ScreenViewModel_HiltModules.BindsModule.class,
...
}
)
@ViewModelScoped
public abstract static class ViewModelC implements ViewModelComponent,
HiltViewModelFactory.ViewModelFactoriesEntryPoint,
... {
@Subcomponent.Builder
abstract interface Builder extends ViewModelComponentBuilder {
}
}
@Subcomponent(
modules = {
ScreenViewModel_HiltModules.KeyModule.class,
...
}
)
@ActivityRetainedScoped
public abstract static class ActivityRetainedC implements ActivityRetainedComponent,
...
{
@Subcomponent.Builder
abstract interface Builder extends ActivityRetainedComponentBuilder {
}
}
@Subcomponent(
modules = {
...
}
)
@ActivityScoped
public abstract static class ActivityC implements ActivityComponent,
...,
DefaultViewModelFactories.ActivityEntryPoint,
HiltWrapper_HiltViewModelFactory_ActivityCreatorEntryPoint,
...
{
@Subcomponent.Builder
abstract interface Builder extends ActivityComponentBuilder {
}
}
여기서, HiltViewModelFactory의 두 EntrypPoint를 기억하셔야 합니다.
ActivityC, ViewModelC에서 HiltViewModelFactory의 두 EntryPoint를 상속함으로써 ViewModel은 HiltViewModelFactory에서 EntryPoint를 통해 가져올 수 있고, ViewModel의 KeySet은 getHiltInternalFactoryFactory 가져올 수 있게 됩니다.
DaggerMainApplication_HiltComponents_SingletonC.java
private static final class ActivityCImpl extends MainApplication_HiltComponents.ActivityC {
...
@Override
public DefaultViewModelFactories.InternalFactoryFactory getHiltInternalFactoryFactory() {
return DefaultViewModelFactories...newInstance( ..., getViewModelKeys(), ...);
}
@Override
public Set<String> getViewModelKeys() {
return SetBuilder.<String>newSetBuilder(3).add(GalleryViewModel_HiltModules_KeyModule_ProvideFactory.provide()).add(MarketViewModel_HiltModules_KeyModule_ProvideFactory.provide()).add(UserCountViewModel_HiltModules_KeyModule_ProvideFactory.provide()).build();
}
}
DefaultViewModelFactories.java
@EntryPoint
@InstallIn(ActivityComponent.class)
public interface ActivityEntryPoint {
InternalFactoryFactory getHiltInternalFactoryFactory();
}
HiltViewModelFactory.java
@EntryPoint
@InstallIn(ActivityComponent.class)
interface ActivityCreatorEntryPoint {
@HiltViewModelMap.KeySet
Set<String> getViewModelKeys();
ViewModelComponentBuilder getViewModelComponentBuilder();
}
DaggerMainApplication_HiltComponents_SingletonC.java
private static final class ViewModelCImpl extends MainApplication_HiltComponents.ViewModelC {
...
@Override
public Map<String, Provider<ViewModel>> getHiltViewModelMap() {
return MapBuilder.<String, Provider<ViewModel>>newMapBuilder(3).put("org.kumnan.aos.apps.testpractice.ui.gallery.GalleryViewModel", (Provider) galleryViewModelProvider()).put("org.kumnan.aos.apps.testpractice.ui.market.MarketViewModel", (Provider) marketViewModelProvider()).put("org.kumnan.aos.apps.testpractice.ui.mypage.UserCountViewModel", (Provider) userCountViewModelProvider()).build();
}
}
이렇게 @HiltViewModel Annotation을 이해하는 시간을 가져보았습니다.
제 글이 중구난방이라 이해가 쉽지 않으실테지만.. 질문이 있으시거나 틀린 부분이 있으면 양해 부탁드리고 댓글로 남겨주시면 감사하겠습니다.
이어서 다음 포스팅에서는 Pure Java/Kotlin 모듈에서 Hilt를 사용할 수 없는 환경일 때 JSR-330을 이용해서 Dependency Injection을 하는 것을 다뤄보겠습니다.