본문 바로가기

dagger

Dagger2 with ViewModel

제가 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