[Kotlin Side Project] Dagger2 적용

2022. 3. 21. 11:54Kotlin/Kotlin Side Project

Android에 Dagger를 적용하기 위해서 DaggerApplication, DaggerActivity를 상속하는 방법을 쓴다.

코드를 분석해보면서 알았지만, DaggerApplication은 androidInjector.inject(this)를 구현해놓았고, 여기서 androidInjector를 얻기 위해서는 AppComponent가 AndroidInjector<App>을 상속받는 형태여야한다.

class App : DaggerApplication() {

    override fun applicationInjector(): AndroidInjector<out DaggerApplication> =
        DaggerAppComponent.factory().create(this@App)
}
@Singleton
@Component(
    modules = [
        AndroidSupportInjectionModule::class,
        ActivityBindingModule::class,
        AppModule::class,
        RetrofitModule::class,
        RoomModule::class,
        ViewModelModule::class,
        ViewModelFactoryModule::class,
        UseCaseModule::class,
        RepositoryModule::class]
)
interface AppComponent : AndroidInjector<App> {

    @Component.Factory
    interface Factory {
        fun create(@BindsInstance application: Application): AppComponent
    }
}

AppComponent를 보면 @Component.Factory로 Factory 인터페이스를 구현해놓았고, create()의 인자로 Android Framework 자체에서 생성이 되는 Application이라는 인스턴스를 넘겨준다. 

Application은 Dagger에서 생성할 수 없기 때문에 외부에서 주입받아서 사용하고, 그에 따라 @BindsInstance를 사용해 바인딩시킨다.

 

@Component의 modules 속성에 기술된 상단 2개는 필수적인 Module로 작용을 하는데, AndroidSupportInjectionModule은 ActivityBindingModule에서 @Binds되는 Activity를 Map으로 제공할 수 있도록 제공되는 Module이다.

ActivityBindingModule에서는 AndroidInjector를 제공받아야하는 인스턴스들을 @Binds시키는 코드를 작성해야하는 Module이다.

@Module
abstract class ActivityBindingModule {

    // @ContributesAdnroidInjector는 dagger-android에서 제공하는 방식으로, 
    // lifecycle을 가진 activity, fragment, service 등에만 작용한다.
    // 따라서, 일반 class에 주입을 원하는 경우,
    // 멤버 변수에 @Inject는 할 수 없기 때문에(일반 class에 AndroidInjector 전달 불가)
    // 생성자 @Inject를 통해 전달하는 방식으로 한다.
    @ContributesAndroidInjector(modules = [MainActivityModule::class])
    abstract fun mainActivity() : MainActivity

}

ActivityBindingModule에서는 주입 대상이 되는 lifecycle을 가진 인스턴스들을 @ContributesAndroidInjector라는 annotation을 통해 @Binds @IntoMap을 동시에 처리한다. @Scope을 통해 해당 인스턴스 내의 custom scope을 지정할 수 있다.

 

DaggerActivity도 마찬가지로 AndroidInjection.inject(this)을 통해 최종적으로 androidInjector.inject(this)를 구현한 것이다. 이를 상속받으면 별도의 추가 코드 없이 Member-injection이 가능하다.

단순히 @Inject만 달아준 멤버 변수만 선언을 해놓으면 자동으로 주입이 되기 때문에 별도의 객체 생성 및 할당 없이 해당 변수를 통해 구현이 가능하다.

class MainActivity : DaggerAppCompatActivity() {

    @Inject
    lateinit var viewModel:  MainViewModel

    @Inject
    lateinit var adapter : PagingAdapter

    // ui 관련된 DI는 설정하지 않는 것이 권장됨(activity -> binding -> activity의 순환주입이 발생할 수 있기 때문)
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
        binding.recyclerView.adapter = adapter

        lifecycleScope.launch {
            viewModel.getContent().collectLatest {
                adapter.submitData(it)
            }
        }

    }
}

DI를 통해 binding까지 제공하려고 했으나, MainActivity와 ActivityBinding은 서로 참조가 되는 형태가 되어 버려서 주입이 실행되지 않았다.(이는 정확하지 않아 좀 더 분석이 필요할 듯하다.)

 

Activity 이외의 Class에서도 DI를 사용하고 싶지만, dagger-android에서는 injector를 lifecycle을 가진 대상에게만 제공하므로 위의 MainActivity에서 멤버 변수에 @Inject한 형태로는 사용이 불가하고, 생성자의 인자에 주입하는 방식을 써야한다. 

class PopularMovieRepositoryImpl(
    private val db : LocalMovieDatabase,
    private val api : TMDBService,
    private val dispatchers : CoroutineDispatcher,
    private val pager: Pager<Int, PopularMovieLocal>
) : PopularMovieRepository {


    @OptIn(ExperimentalPagingApi::class)
    override suspend fun getAll(): Flow<PagingData<PopularMovie>> {
        Log.i("KMJTEST","[PopularMovieRespositoryImpl] getAll()")
        return withContext(dispatchers) {
            pager.flow.map {
                it.map {
                    it.toEntity()
                }
            }
        }
    }

}

 

ViewModel의 생성은 ViewmodelProvider를 사용한다. 인자가 있는 경우, Factory를 넘겨서 ViewModel의 인자가 넘겨진 상태로 새로운 인스턴스를 반환하게 한다.

DI가 없는 경우, ViewModel마다 Factory를 생성해 인자를 넘겨주는 형태로 새로운 인스턴스를 반환했었다.

DI가 적용되는 경우, ViewModel을 @Binds시켜 해당 ViewModel의 클래스를 key로 ViewModel에 대한 Provider를 제공하는 Map을 생성하고, 이를 Factory에서 인자로 주입받아 접근하는 형태로 구현해야한다.

@Module
abstract class ViewModelModule {

    @Binds
    @IntoMap
    @ViewModelKey(MainViewModel::class)
    abstract fun bindMainViewModel(mainViewModel: MainViewModel): ViewModel

}
class ViewModelFactory @Inject constructor(val viewModelMap : Map<Class<out ViewModel>, 
		@JvmSuppressWildcards Provider<ViewModel>>) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        Log.i("KMJTEST","Provider : ${viewModelMap[modelClass]?.get()}")
        return viewModelMap[modelClass]?.get() as T
    }
}

ViewModelProvider.Factory를 구현하는 ViewmodelFactory의 생성자 주입을 통해 해당값을 인자로 받아와 Provider<>.get() 형태로 제공한다. Provider를 통해 인스턴스를 얻는 것은 @Scope에 맞게끔 새로운 인스턴스의 ViewModel을 제공할 수 있는 방법이다.

 

@Module
abstract class ViewModelFactoryModule {
    @Binds
    abstract fun bindViewModelFactory(viewModelFactory : ViewModelFactory) : ViewModelProvider.Factory
}

위에서 @Inject 받은 ViewModelFactory로 @Binds가 가능해지며, ViewModelProvider.Factory의 타입으로 @Inject가 요청되면 제공될 수 있다.

 

class MainActivity : DaggerAppCompatActivity() {

    @Inject
    lateinit var viewModelFactory:  ViewModelProvider.Factory // ViewModelFactory 도 가능..

    @Inject
    lateinit var adapter : PagingAdapter

    // ui 관련된 DI는 설정하지 않는 것이 권장됨(activity -> binding -> activity의 순환주입이 발생할 수 있기 때문)
    private lateinit var binding: ActivityMainBinding

    //private val viewModel by viewModels<MainViewModel>{
    //    viewModelFactory
    //}

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
        binding.recyclerView.adapter = adapter

        val viewModel = ViewModelProvider(this, viewModelFactory).get(MainViewModel::class.java)
        lifecycleScope.launch {
            viewModel.getContent().collectLatest {
                adapter.submitData(it)
            }
        }

    }
}

ViewModel을 생성할 때, ViewModelProvider의 인자로 넘겨주어야하는 Factory는 @Inject 받은 viewModelFactory를 넘겨준다. 동작과정은 위에서 설명한대로 클래스명을 key로 하기 때문에, ViewModelFactory의 인자로 넘어온 Map에서 넘겨준 key를 Wrapping 하고있는 Provider를 참조해 get()을 호출하면 @Scope에 맞는 인스턴스를 제공한다. (ViewModelProvider.Factory를 implements한 ViewModelFactory 형태로 요청해도 @Binds가 작동한다..)

 

activity-ktx에서 제공하는 ViewModel 위임 생성 확장함수를 이용하려면, 주석과 같이 by viewModels<생성하려는 ViewModel 클래스>{ 생성을 위한 Factory }로 사용하면 된다.