Dagger
의 가장 큰 장점은 계층관계의 오브젝트 그래프를 만들 수 있다는 것 입니다.
이번에 다룰 @Subcomponent
와 @Scope
를 이용하면 오브젝트 그래프
를 이용하여 앱 구성요소의 생명주기 동안 메모리 할당과 해제를 효율적으로 관리할 수 있게 해줍니다.
Application(Singleton) Scope 지정
먼저, Application Scope를 가져야하는 클래스들(Repository, DataSource 등등....)은 앱이 실행되는 동안 인스턴스가 메모리에 있기를 원하기 때문에 Application의 생명주기와 동일한 범위를 따르는 오브젝트 그래프를 만듭니다. 이렇게 하면 그래프는 앱 수명 주기에 연결됩니다.
그래프를 생성하는 인터페이스는 @Component
로 주석이 지정되므로 ApplicationComponent로 호출할 수 있습니다. Dagger는 종속 관계에 따른(일반적으로 Lifecycle에 관련한 종속관계) 프로젝트의 오브젝트 그래프를 만들 수 있습니다. Dagger가 이렇게 하도록 하려면 인터페이스를 만들고 @Component
로 주석을 지정해야 합니다.
프로젝트 빌드 시 Dagger는 자동으로 ApplicationComponent 인터페이스의 구현, 즉 DaggerApplicationComponent
를 생성합니다. 또한, Dagger는 Annotation Proceesor
를 사용하여 최초의 진입점으로 계층 관계로 구성된 종속 항목을 만듭니다.
@Singleton
@Component(
modules = [ApplicationModule::class,
ViewModelModule::class,
NetworkModule::class,
DataSourceModule::class]
)
interface ApplicationComponent {
fun userActivityComponentBuilder(): UserActivityComponent.Builder
fun inject(myApplication: MyApplication)
@Component.Builder
interface Builder {
fun application(@BindsInstance application: Application): Builder
fun setApplicationModule(applicationModule: ApplicationModule): Builder
fun setDataSourceModule(dataSourceModule: DataSourceModule): Builder
fun setNetworkModule(networkModule: NetworkModule): Builder
fun build(): ApplicationComponent
}
}
@Module
class DataSourceModule {
@Singleton
@Provides
fun provideUserRemoteDataSource(userRetrofitService: UserRetrofitService): UserRemoteDataSource {
return UserRemoteDataSourceImpl(userRetrofitService)
}
@Singleton
@Provides
fun provideUserLocalDataSource(): UserLocalDataSource {
return UserLocalDataSourceImpl()
}
}
@Module
abstract class RepositoryModule {
@Singleton
@Binds
abstract fun bindUserRepository(userRepositoryImpl: UserRepositoryImpl): UserRepository
}
@Module(
includes = [
RepositoryModule::class],
subcomponents = [UserActivityComponent::class]
)
class ApplicationModule {
@Singleton
@Provides
fun provideApplicationContext(application: Application): Context {
return application.applicationContext
}
}
ApplicationComponent에 속한 Module(현재 예제에서는 ApplicationModule, RepositoryModule, DataSourceModule, NetWorkModule)들에 선언되어있는 객체들에 @Singleton
어노테이션을 통해 객체를 요청할 때 마다 항상 동일한 인스턴스를 주입받을 수 있습니다.
만약, 싱글톤이 아닌 매번 새로운 인스턴스를 받고싶다면 @Singleton
어노테이션을 제거해주면 됩니다.
Activity, Fragment Scope 지정
Dagger에서의 권장사항은 앱의 라이프사이클에 따라 Scope를 정의하는 것 입니다.
그러므로, @ActivityScope와 @FragmentScope를 정의하겠습니다.
@Scope
@Retention(value = AnnotationRetention.RUNTIME)
annotation class ActivityScope
@Scope
@Retention(value = AnnotationRetention.RUNTIME)
annotation class FragmentScope
일반적으로 앱 구성요소의 생명주기는 Application 생명주기 안에서 Activity 생명주기가 존재하고, Activity안에 Fragment의 생명주기가 존재합니다.
그러므로, ApplicationComponent의 SubComponent
로 UserActivityComponent를 만들어주고, UserActivityComponent로 UserFragmentComponent를 생성해줍니다.
또한, SubComponent
에는 @Subcomponent.Builder
를 통해 UserActivityComponent의 Provision 메소드
를 반환할 수 있도록 해줘야합니다.
@ActivityScope
@Subcomponent(modules =[UserActivityModule::class])
interface UserActivityComponent {
fun inject(activity: UserActivity)
fun userFragmentBuilder(): UserFragmentComponent.Builder
@Subcomponent.Builder
interface Builder {
fun setModule(module: UserActivityModule):Builder
@BindsInstance
fun setActivity(activity: UserActivity): Builder
fun build(): UserActivityComponent
}
}
@Module(subcomponents = [UserFragmentComponent::class])
class UserActivityModule {
@Provides
@ActivityScope
fun provideActivityName(): String {
return UserActivity::class.simpleName ?: "UserActivity"
}
}
@FragmentScope
@Subcomponent(modules = [UserFragmentModule::class])
interface UserFragmentComponent {
fun inject(fragment: UserFragment)
@Subcomponent.Builder
interface Builder {
fun setModule(module: UserFragmentModule): Builder
@BindsInstance
fun setFragment(fragment: UserFragment): Builder
fun build(): UserFragmentComponent
}
}
@Module
class UserFragmentModule {
@Provides
@FragmentScope
fun provideFragmentName(): String {
return UserFragment::class.simpleName ?: "UserFragment"
}
}
이렇게 코드를 작성해주면, 아래와 같은 그림의 오브젝트 그래프
의 구조가 됩니다.
Dagger에서 @Inject를 통한 인스턴스 주입 과정은 SubComponent를 타고 내려와 가장 아래부터 우선 탐색하여 인스턴스를 반환하고 인스턴스가 없으면 상위 컴포넌트들을 순차적으로 올라가 탐색하는 과정을 반복합니다. 이러한 원리를 이용하여 @Scope를 지정하여 인스턴스의 생명주기를 조절할 수 있습니다.
위 코드에서, UserActivityComponent위에 @ActivityScope
를 등록해주었고, UserActivityModule내의 @ActivityScope
를 가진 바인딩 메소드를 지정해주면 UserActivityComponent를 통해 객체를 주입받는 인스턴스들은 해당 Scope
동안 항상 같은 객체를 주입받을 수 있습니다.
또한, 하위 컴포넌트에서는 상위 컴포넌트에 등록된 객체를 가져올 수 있습니다.
하위 컴포넌트에서 상위 컴포넌트의 상위 Scope로 지정되어있는 객체 또한 마찬가지로 항상 같은 객체로 주입받을 수 있습니다.
(반대로, Scope를 지정해주지 않은 객체는 항상 다른 객체를 주입받습니다.)
Fragment에서도 @ActivityScope
의 객체를 가져온다면 마찬가지로 항상 동일한 객체를 주입받을 수 있습니다.
그러므로, Activity가 그대로이고 Fragment가 생성, 삭제 된다해도 @FragmentScope
로 지정한 객체들은 매번 새로운 객체를 주입받을 수 있습니다.
class MyApplication : Application() {
private lateinit var appComponent: ApplicationComponent
override fun onCreate() {
super.onCreate()
appComponent = DaggerApplicationComponent.builder().application(this)
.setApplicationModule(ApplicationModule())
.setDataSourceModule(DataSourceModule())
.setNetworkModule(NetworkModule())
.build()
}
fun getAppComponent(): ApplicationComponent = appComponent
}
class UserActivity : AppCompatActivity() {
private lateinit var binding: ActivityUserBinding
private lateinit var component: UserActivityComponent
@Inject
lateinit var activityName: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
component =
(applicationContext as MyApplication).getAppComponent().userActivityComponentBuilder()
.setActivity(this).setModule(
UserActivityModule()
).build().apply {
inject(this@UserActivity)
}
binding = DataBindingUtil.setContentView<ActivityUserBinding>(this, R.layout.activity_user)
.apply {
activity = this@UserActivity
lifecycleOwner = this@UserActivity
vm = this@UserActivity.viewModel
}
}
}
class UserFragment : Fragment() {
private lateinit var binding: FragmentUserBinding
@Inject
lateinit var fragmentName: String
override fun onAttach(context: Context) {
super.onAttach(context)
(activity as UserActivity).component.userFragmentBuilder().setModule(UserFragmentModule())
.setFragment(this).build().inject(this@UserFragment)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_user, container, false)
return binding.root
}
}
각 앱 구성요소에서 멤버인젝션으로 주입하는 예제입니다. (Dagger 관련 부분 제외하고 코드를 지웠습니다.)
위 코드처럼, Application, Activity, Fragment에 맞는 Component를 가져와서 inject를 해주면, 각 Scope
에 맞게 객체가 주입됩니다.
dagger-android 적용
Dagger2의 문제점
- 보일러 플레이트 코드가 너무 많음 (프로젝트가 커질수록 코드 변경(리팩토링)에 대해 취약해짐)
- 매번 Member-injection 메소드를 작성해야함
- Activity 등, 주입받을 곳에서 일일히 Component 객체를 가져와 주입시켜줘야 함
이러한 문제점들을 해결하기 위해, Dagger2에서는 dagger-android 패키지를 제공하였습니다.
위의 예제 코드에 dagger-android를 적용시켜 보도록 하겠습니다.
과정은 아래와 같습니다.
1. UserActivityComponent에 AndroidInjector<T>상속, Factory 메소드에도 마찬가지로 AndroidInjector.Factory<T> 상속
2. ApplicationComponent에 AndroidInjector<T> 상속
3. AndroidInjector들의 멀티바인딩 수행
4. Application에 HasAndroidInjetor상속 및 DispatchingAndroidInjector 주입
5. Activity 혹은 Fragment에서 의존성 주입
UserActivityComponent.kt
@ActivityScope
@Subcomponent(modules =[UserActivityModule::class])
interface UserActivityComponent: AndroidInjector<UserActivity> {
//fun inject(activity: UserActivity)
@Subcomponent.Factory
interface Factory: AndroidInjector.Factory<UserActivity> {}
}
UserActivityComponent에 AndroidInjector<UserActivity>를 상속시켜줍니다.
또한, Factory 메소드에도 마찬가지로 AndroidInjector.Factory 상속시켜 줍니다.
ActivityBindingModule.kt
@Module
abstract class ActivityBindingModule {
@Binds
@IntoMap
@ClassKey(UserActivity::class)
abstract fun bindAndroidInjectorFactory(factory: UserActivityComponent.Factory): AndroidInjector.Factory<*>
}
AndroidInjector의 Factory들을 멀티바인딩 하는 메소드를 작성해야 합니다.
AndroidInjector를 통해서 SubComponent들을 Member Injection 을 수행할 수 있도록 해줍니다.
Key는 ClassKey 혹은 StringKey를 사용 가능한데 ClassKey를 이용해서 바인딩해주겠습니다.
AndroidInjectionModule.kt
@Beta
@Module
public abstract class AndroidInjectionModule {
@Multibinds
abstract Map<Class<?>, AndroidInjector.Factory<?>> classKeyedInjectorFactories();
@Multibinds
abstract Map<String, AndroidInjector.Factory<?>> stringKeyedInjectorFactories();
private AndroidInjectionModule() {}
}
AndroidInjector.kt
/**
* Performs members-injection for a concrete subtype of a <a
* href="https://developer.android.com/guide/components/">core Android type</a> (e.g., {@link
* android.app.Activity} or {@link android.app.Fragment}).
*
* <p>Commonly implemented by {@link dagger.Subcomponent}-annotated types whose {@link
* dagger.Subcomponent.Factory} extends {@link Factory}.
*
* @param <T> a concrete subtype of a core Android type
* @see AndroidInjection
* @see DispatchingAndroidInjector
* @see ContributesAndroidInjector
*/
@Beta
public interface AndroidInjector<T> {
/** Injects the members of {@code instance}. */
void inject(T instance);
/**
* Creates {@link AndroidInjector}s for a concrete subtype of a core Android type.
*
* @param <T> the concrete type to be injected
*/
interface Factory<T> {
/**
* Creates an {@link AndroidInjector} for {@code instance}. This should be the same instance
* that will be passed to {@link #inject(Object)}.
*/
AndroidInjector<T> create(@BindsInstance T instance);
}
/**
* An adapter that lets the common {@link dagger.Subcomponent.Builder} pattern implement {@link
* Factory}.
*
* @param <T> the concrete type to be injected
* @deprecated Prefer {@link Factory} now that components can have {@link dagger.Component.Factory
* factories} instead of builders
*/
@Deprecated
abstract class Builder<T> implements AndroidInjector.Factory<T> {
@Override
public final AndroidInjector<T> create(T instance) {
seedInstance(instance);
return build();
}
/**
* Provides {@code instance} to be used in the binding graph of the built {@link
* AndroidInjector}. By default, this is used as a {@link BindsInstance} method, but it may be
* overridden to provide any modules which need a reference to the activity.
*
* <p>This should be the same instance that will be passed to {@link #inject(Object)}.
*/
@BindsInstance
public abstract void seedInstance(T instance);
/** Returns a newly-constructed {@link AndroidInjector}. */
public abstract AndroidInjector<T> build();
}
}
ApplicationComponent.kt
@Singleton
@Component(
modules = [AndroidInjectionModule::class,
ApplicationModule::class,
ActivityInjectorModule::class,
ViewModelModule::class,
NetworkModule::class,
ActivityBindingModule::class,
DataSourceModule::class]
)
interface ApplicationComponent: AndroidInjector<MyApplication> { // member-injection 생략 ( inject() 생략 가능하게 해줌)
//fun userActivityComponentBuilder(): UserActivityComponent.Builder
//fun inject(myApplication: MyApplication)
@Component.Factory
interface Factory : AndroidInjector.Factory<MyApplication> { // MyApplication 인스턴스를 그래프에 바인딩하고 컴포넌트를 반환하는 create()메소드를 포함시켜줌
}
}
@Module에 AndroidInjectorModule과 ActivityInjectorModule을 추가해줍니다.
그리고 ApplicationComponent에서 AndroidInjector<T>를 상속해주고, Factory 인터페이스에 AndroidInjector.Factory를 상속해줍니다.
-> AndroidInjector
를 통해 오브젝트 그래프에 있는 컴포넌트들의 Member Injection 메소드들을 추가시켜줍니다.
또한, AndroidInjector의 Factory 메소드를 상속함으로서 MyApplication 인스턴스를 오브젝트 그래프에 바인딩 시켜주고 모듈을 포함한 AndroidInjector<MyApplication>의 create()메소드를 반환해줍니다.
MyApplication.kt
class MyApplication : Application(), HasAndroidInjector {
@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
override fun onCreate() {
super.onCreate()
DaggerApplicationComponent.factory().create(this).inject(this)
}
override fun androidInjector(): AndroidInjector<Any> {
return dispatchingAndroidInjector
}
}
Application에 HasAndroidInjector
인터페이스를 상속해주고 DispatchingAndroidInjector를 @Inject를 통해 주입시켜줍니다.
그러면, DispatchingAndroidInjector
를 통해서 Android의 구성요소(Activity, Fragment 등등)에 바인딩 되어있는 AndroidInjector를 통해서 Member Injection을 수행할 수 있도록 해줍니다.
UserActivity.kt
class UserActivity : AppCompatActivity(), HasAndroidInjector {
private lateinit var binding: ActivityUserBinding
@Inject
lateinit var activityName: String
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidInjection.inject(this)
binding = DataBindingUtil.setContentView<ActivityUserBinding>(this, R.layout.activity_user)
.apply {
activity = this@UserActivity
lifecycleOwner = this@UserActivity
vm = this@UserActivity.viewModel
}
}
override fun androidInjector(): AndroidInjector<Any> {
return androidInjector
}
}
@ContributesAndroidInjector 어노테이션 활용
ApplicationComponent의 AndroidInjector.Factory에 추가적인 메소드가 없을 경우 @ContributesAndroidInjector
를 이용하면 DispatchingAndroidInjector를 이용하지 않아도 되므로 보일러플레이트를 더 줄일 수 있습니다.
@Module
abstract class ActivityBindingModule {
@ActivityScope
@ContributesAndroidInjector(modules = [UserActivityModule::class])
abstract fun userActivity(): UserActivity
}
class MyApplication : DaggerApplication() {
override fun onCreate() {
super.onCreate()
DaggerApplicationComponent.factory().create(this).inject(this)
}
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerApplicationComponent.factory().create(this)
}
}
class UserActivity : AppCompatActivity() {
private lateinit var binding: ActivityUserBinding
@Inject
lateinit var activityName: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidInjection.inject(this)
binding = DataBindingUtil.setContentView<ActivityUserBinding>(this, R.layout.activity_user)
.apply {
activity = this@UserActivity
lifecycleOwner = this@UserActivity
vm = this@UserActivity.viewModel
}
}
}