① 제가 Dagger2 를 공부하여 Android 개발에서 최종적으로 하고싶은 것은 ViewModel(+ UseCase, Repository, DataSource, APIService) 을 직접 생성하지 않고 외부에서 주입받아서 보일러 플레이트 코드를 줄이고 유닛 테스트를 보다 쉽게 작성하기 위함입니다. 하지만 개인적으로 느낀점은 Dagger2 를 통해 ViewModel(정확히는 ViewModel 을 가지고 있는 ViewModelFactory)을 주입해보니 일반적으로 ViewModelProvider 에 ViewModelFactory 를 넘겨서 ViewModel 을 생성하는 방법과 비교해봤을 때 코드의 양은 많이 줄어든 것 같지 않다는 점을 느꼈습니다.
② 예제 코드
Fragment 클래스 - SubComponent 를 통해 ViewModelFactory 필드 주입을 요청합니다.
class CommentsFragment : Fragment() {
@Inject
lateinit var factory: ViewModelProvider.Factory
private val viewModel by lazy { ViewModelProvider(this, factory).get(CommentsViewModel::class.java)}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
val binding = DataBindingUtil.inflate<FragmentCommentsBinding>(inflater, R.layout.fragment_comments, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(requireActivity().applicationContext as GlobalApplication).appComponent.getCommentsComponent().create().inject(this)
viewModel.getComments()
}
}
ViewModel 클래스 - Fragment 에 요청을 받아 UseCase 를 실행합니다.
class CommentsViewModel @Inject constructor(
private val getCommentsUseCase: GetCommentsUseCase
) : ViewModel() {
fun getComments() {
val handler = CoroutineExceptionHandler { _, throwable ->
Timber.tag("CoroutineException").d(throwable.toString())
}
viewModelScope.launch(handler) {
val response = getCommentsUseCase()
var cnt = 1
for(item in response.body()!!) {
Log.d("PostRetrofitService", "${cnt++} : $item")
}
}
}
}
ViewModelFactory 클래스 - ViewModel 이 저장된 Map(Dagger 에서 생성한)을 프로퍼티로 가지고 있습니다. ViewModel 객체가 필요할 경우 Map 에서 ViewModel 을 꺼내서 반환합니다.
class ViewModelFactory @Inject constructor(
private val creators: @JvmSuppressWildcards Map<Class<out ViewModel>, Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return creators[modelClass]?.get() as T
}
}
UseCase 클래스 - 주입된 Repository 클래스의 메서드를 호출합니다.
class GetCommentsUseCase @Inject constructor(
private val repository: PostRepository
) {
suspend operator fun invoke() = repository.getComments()
}
Repository 클래스 - 주입된 DataSource 의 메서드를 호출합니다.
class PostRepositoryImpl @Inject constructor(
private val remoteDataSource: PostRemoteDataSource
) : PostRepository{
override suspend fun getComments() = withContext(Dispatchers.IO) {
remoteDataSource.getRemoteComments()
}
}
DataSource 클래스 - 주입된 Retrofit Service 의 메서드를 호출하여 서버에 데이터를 요청합니다.
class PostRemoteDataSourceImpl @Inject constructor(
private val postService: PostService
) : PostRemoteDataSource {
override suspend fun getRemoteComments() = withContext(Dispatchers.IO) {
postService.getComments()
}
}
Retrofit Service 인터페이스
interface PostService {
@GET("/comments")
suspend fun getComments(): Response<Comments>
}
SubComponent - AppComponent 의 하위 컴포넌트로 Fragment 에서 ViewModelFactory 주입을 요청할 때 호출할 메서드를 노출합니다.
@Subcomponent(modules = [ViewModelModule::class])
interface CommentsComponent {
@Subcomponent.Factory
interface Factory {
fun create(): CommentsComponent
}
fun inject(commentsActivity: CommentsActivity)
fun inject(commentsFragment: CommentsFragment)
}
ViewModelModule - SubComponent 와 연결된 모듈로 Dagger 에서 생성한 Map 에 ViewModel 객체를 저장합니다.
@Module
abstract class ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(CommentsViewModel::class)
abstract fun bindCommentsViewModel(viewModel: CommentsViewModel): ViewModel
}
ViewModelFactoryModule - Fragment 에서 SubComponent 를 통해 ViewModelFactory 주입을 요청하면 상위 컴포넌트와 연결된 ViewModelFactoryModule 을 통해 ViewModelFactory 객체를 제공합니다.
@Module
abstract class ViewModelFactoryModule {
/*
@Provides
fun provideViewModelFactory(creators: @JvmSuppressWildcards Map<Class<out ViewModel>, Provider<ViewModel>>): ViewModelProvider.Factory {
return ViewModelFactory(creators)
}
*/
@Binds
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
}
RepositoryModule - ViewModel 생성 시 계층적으로 주입될 객체(UseCase -> Repository -> DataSource -> Retrofit Service)를 생성하여 생성자를 통해 주입하는데 이때 Repository 와 Remote DataSource 객체를 제공하는 모듈입니다.
@Module
class RepositoryModule {
@Singleton
@Provides
fun providePostRepository(remoteDataSource: PostRemoteDataSource): PostRepository {
return PostRepositoryImpl(remoteDataSource)
}
@Singleton
@Provides
fun providePostRemoteDataSource(postService: PostService): PostRemoteDataSource {
return PostRemoteDataSourceImpl(postService)
}
}
NetworkModule - Retrofit Service 객체를 제공하는 모듈입니다.
@Module
class NetworkModule {
@Singleton
@Provides
fun providePostRetrofitService(): PostService {
return Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(PostService::class.java)
}
}
ViewModelKey 어노테이션 - ViewModel 객체를 생성하여 Map에 저장하는데 이때 해당 어노테이션을 붙여서 Class 타입을 Key 값으로 사용하도록 합니다.
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.PROPERTY_GETTER)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)
③ Dagger 코드 분석
AppComponent(상위 컴포넌트)
private static final class AppComponentImpl implements AppComponent {
private final AppComponentImpl appComponentImpl = this;
private Provider<PostService> providePostRetrofitServiceProvider;
private Provider<PostRemoteDataSource> providePostRemoteDataSourceProvider;
private Provider<PostRepository> providePostRepositoryProvider;
private AppComponentImpl(NetworkModule networkModuleParam, RepositoryModule repositoryModuleParam) {
initialize(networkModuleParam, repositoryModuleParam);
}
@SuppressWarnings("unchecked")
private void initialize(final NetworkModule networkModuleParam, final RepositoryModule repositoryModuleParam) {
/*
1. AppComponent(CommentsComponent 의 상위 컴포넌트)에서는 RetrofitService, RemoteDataSource, Repository 객체를 제공할 수 있는 Provider 를 생성합니다.
해당 객체들을 제공하는 Provides 메서드에는 @Singleton 어노테이션이 붙어있기 때문에 컴포넌트 범위내에서 동일한 객체를 제공하기 위해 DoubleCheck 클래스를 통해 싱글톤으로 제공될 수 있도록 합니다.
*/
this.providePostRetrofitServiceProvider = DoubleCheck.provider(NetworkModule_ProvidePostRetrofitServiceFactory.create(networkModuleParam));
this.providePostRemoteDataSourceProvider = DoubleCheck.provider(RepositoryModule_ProvidePostRemoteDataSourceFactory.create(repositoryModuleParam, providePostRetrofitServiceProvider));
this.providePostRepositoryProvider = DoubleCheck.provider(RepositoryModule_ProvidePostRepositoryFactory.create(repositoryModuleParam, providePostRemoteDataSourceProvider));
}
@Override
public CommentsComponent.Factory getCommentsComponent() {
return new CommentsComponentFactory(appComponentImpl);
}
}
SubComponent(하위 컴포넌트)
private static final class CommentsComponentImpl implements CommentsComponent {
private final AppComponentImpl appComponentImpl;
private final CommentsComponentImpl commentsComponentImpl = this;
private Provider<GetCommentsUseCase> getCommentsUseCaseProvider;
private Provider<CommentsViewModel> commentsViewModelProvider;
private CommentsComponentImpl(AppComponentImpl appComponentImpl) {
//1. 상위 컴포넌트의 참조를 저장합니다.
this.appComponentImpl = appComponentImpl;
initialize();
}
/*
6. ViewModelFactory 객체를 생성하기 위해서는 의존성을 가진 Map 객체가 필요합니다.
해당 Map 객체에는 현재 컴포넌트와 연결된 Module 중 ViewModel 의 하위 클래스 타입인 객체들이 저장됩니다. 현재는 CommentsViewModel 하나밖에 없기 때문에 하나만 저장됩니다.
ViewModelModule 의 bindCommentsViewModel 추상 메서드에 @IntoMap 어노테이션이 붙어있기 때문에 bindCommentsViewModel 추상 메서드는 ViewModel 객체를 생성하여 반환하는 것이 아니라
ViewModel 객체를 생성하여 Map 에 저장하게 됩니다.
*/
private Map<Class<? extends ViewModel>, Provider<ViewModel>> mapOfClassOfAndProviderOfViewModel() {
return Collections.<Class<? extends ViewModel>, Provider<ViewModel>>singletonMap(CommentsViewModel.class, ((Provider) commentsViewModelProvider));
}
//5. ViewModelFactory 객체를 생성하여 반환합니다.(ViewModelModule 의 bindCommentsViewModel 추상 메서드에 매치됩니다)
private ViewModelFactory viewModelFactory() {
return new ViewModelFactory(mapOfClassOfAndProviderOfViewModel());
}
//2. SubComponent 에서는 ViewModel, UseCase 객체를 제공할 수 있는 Provider 를 생성합니다.
@SuppressWarnings("unchecked")
private void initialize() {
this.getCommentsUseCaseProvider = GetCommentsUseCase_Factory.create(appComponentImpl.providePostRepositoryProvider);
this.commentsViewModelProvider = CommentsViewModel_Factory.create(getCommentsUseCaseProvider);
}
@Override
public void inject(CommentsActivity commentsActivity) {}
//3. Fragment 에서 필드 주입을 요청할 때 실행하는 메서드입니다.
@Override
public void inject(CommentsFragment commentsFragment) {
injectCommentsFragment(commentsFragment);
}
/*
4. 필드 주입을 위해 필요한 객체인 ViewModelProvider 를 생성하여 Dagger 에 의해 생성된 Member Injector 클래스를 통해 필드 주입을 실행합니다.
*/
private CommentsFragment injectCommentsFragment(CommentsFragment instance) {
CommentsFragment_MembersInjector.injectFactory(instance, viewModelFactory());
return instance;
}
}
④ 요약
Dagger 를 통해 ViewModel 의존성 주입이란 ViewModel 객체를 생성하여 Dagger 에서 제공해준 Map 에 저장한 후 ViewModelFactory 를 통해 해당 Map 에서 클래스 타입에 맞는 ViewModel 을 찾아서 반환해주는 방식으로 동작합니다.
'dagger' 카테고리의 다른 글
Dagger2 Singleton (0) | 2022.12.03 |
---|---|
Dagger2 필드 주입 (0) | 2022.11.24 |
Dagger2 생성자 주입 (0) | 2022.11.23 |
Dagger2 시작 (0) | 2022.11.22 |