Hilt와 Dagger, JSR-330 (2) - ViewModel

이태훈·2022년 2월 5일
1

Hilt, Dagger, JSR-330

목록 보기
2/3

안녕하세요, 이번 포스팅에서는 저번 시리즈에 이어서 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("...") <-- ViewModelpackage + 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 ...; <-- ViewModelpackage + 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을 하는 것을 다뤄보겠습니다.


References

https://qiita.com/takahirom/items/17349916359a4481cbed

profile
https://www.linkedin.com/in/%ED%83%9C%ED%9B%88-%EC%9D%B4-7b9563237

0개의 댓글