이 글은 기존 운영했던 WordPress 블로그인 PyxisPub: Development Life (pyxispub.uzuki.live) 에서 가져온 글 입니다. 모든 글을 가져오지는 않으며, 작성 시점과 현재 시점에는 차이가 많이 존재합니다.
작성 시점: 2018-06-07
Dagger 를 사용하기 위해서는 주입할 의존성을 @Provides 나 @Binds 를 통하여 제공하는 것 말고도, 주입될 대상을 제공해야 하는데, Members-injection methods (dagger/api/latest/dagger/Component.html) 를 사용하거나 각 안드로이드 구성요소 (Activity, Service) 들에 대한 별도의 Subcomponent 를 만들고 @IntoSet 어노테이션을 이용해 DispatchingAndroidInjector 의 injectorFactories 에 해당 Subcomponent 를 추가해야 합니다.
두 가지 방법, Members-injection methods 와 Subcomponet 가 공통점을 가지고 있다면 빠르게 개발 해야 하는 입장에서는 상당히 고역이란 점입니다.
Members-injection methods 는 Component 내부에 직접 적어주긴 하나 작성해야 하는 코드 양이 적어 괜찮다고 볼 수 있습니다. 다음 코드에서 void inject ~ 코드가 하나의 Members-injection methods 를 나타냅니다.
public interface AppComponent {
@Component.Builder
interface Builder {
@BindsInstance
Builder application(MainApplication application);
AppComponent build();
}
void inject(MainApplication mainApp);
void inject(MenuView menuView);
}
Subcomponent 로 오게 되면 작성해야 하는 코드는 매우 많아집니다.
위 세 가지를 모두 반영한 것이 아래 코드입니다.
package com.github.windsekirun.daggerautoinject;
import android.app.Activity;
import com.github.windsekirun.daggerautoinject.sample.MainActivity;
import dagger.Binds;
import dagger.Module;
import dagger.Subcomponent;
import dagger.android.ActivityKey;
import dagger.android.AndroidInjector;
import dagger.multibindings.IntoMap;
@Module(subcomponents = ActivityModule_Contribute_MainActivity.MainActivitySubcomponent.class)
public abstract class ActivityModule_Contribute_MainActivity {
private ActivityModule_Contribute_MainActivity() {}
@Binds
@IntoMap
@ActivityKey(MainActivity.class)
abstract AndroidInjector.Factory<? extends Activity> bindAndroidInjectorFactory(
MainActivitySubcomponent.Builder builder);
@Subcomponent
public interface MainActivitySubcomponent extends AndroidInjector<MainActivity> {
@Subcomponent.Builder
abstract class Builder extends AndroidInjector.Builder<MainActivity> {}
}
}
아무리 Dependency Injection 가 좋다고 해도, 도입에 있어 조금 겁이 날 수 있는 부분이라 생각합니다.
그래서 Dagger-Android 모듈에서는 @ContributesAndroidInjector 어노테이션을 제공하는데, 하나의 전체 모듈을 만들고 @ContributesAndroidInjector 어노테이션을 부착한 메서드를 각 안드로이드 구성요소당 하나씩 만들어 주면 나머지 세 가지 코드에 대해서는 자동으로 생성하는 기능을 가지고 있습니다.
package com.github.windsekirun.daggerautoinject;
import com.github.windsekirun.daggerautoinject.sample.MainActivity;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;
@Module
public abstract class ActivityModule {
@ContributesAndroidInjector
abstract MainActivity contribute_MainActivity();
}
하지만, 이와 같은 @ContributeAndroidInjector 에도 개선할 점은 있습니다. 앱에 Activity 가 여러 개 있다면, Activity 를 하나 작성할 때 마다 ActivityModule 란 곳에 작성해주는 것도 꽤나 고역이라고 생각됩니다.
그래서 찾은 라이브러리가 florent37/DaggerAutoInject (https://github.com/florent37/DaggerAutoInject) 이고, 이 라이브러리가 Activity / Fragment 만 제공하던 것을 좀 더 확장해 Activity / Fragment / Service / Broadcast Receiver / ContentProvider / ViewModel (Android Architecture components) 에 대해 제공하게 한 것이 WindSekirun/DaggerAutoInject (https://github.com/WindSekirun/DaggerAutoInject) 입니다.
이번 글에서는 DaggerAutoInject 라이브러리가 어떻게 작동하는지 살펴보고 적용 방법을 설명하려 합니다.
이전 글인 Generate Kotlin Code with KotlinPoet uses Annotation Processor (https://blog.uzuki.live/generate-kotlin-code-with-kotlinpoet-uses-annotation-processor-1/) 에서도 설명한 Annotation Processor 로 특정 Annotation 가 붙은 클래스를 모두 찾아서 각 타입에 맞게 ActivityModule / ServiceModule / FragmentModule / ViewModelModule 를 생성하는 것입니다.
ActivityModule / ServiceModule / FragmentModule / BroadcastReceiverModule / ContentProviderModule 에 대해서는 모두 같은 구현 방법을 취합니다.
위 과정을 모두 포함하는 것이 다음 코드입니다.
static <A extends Annotation> void processHolders(RoundEnvironment env, Class<A> cls, Map<ClassName, ContributesHolder> map) {
for (Element element : env.getElementsAnnotatedWith(cls)) {
final ClassName classFullName = ClassName.get((TypeElement) element);
final String className = element.getSimpleName().toString();
map.put(classFullName, new ContributesHolder(element, classFullName, className));
}
}
static void constructContributesAndroidInjector(String className, Collection<ContributesHolder> holders, Filer filer) {
final TypeSpec.Builder builder = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.addAnnotation(Constants.DAGGER_MODULE);
for (ContributesHolder contributesHolder : holders) {
builder.addMethod(MethodSpec.methodBuilder(Constants.METHOD_CONTRIBUTE + contributesHolder.className)
.addAnnotation(Constants.DAGGER_ANDROID_ANNOTATION)
.addModifiers(Modifier.ABSTRACT)
.returns(contributesHolder.classNameComplete)
.build()
);
}
final TypeSpec newClass = builder.build();
final JavaFile javaFile = JavaFile.builder(Constants.PACKAGE_NAME, newClass).build();
try {
javaFile.writeTo(System.out);
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
첫 번째 메서드가 1번의 과정을 가지고 있으며, 두 번째 메서드가 2, 3번의 과정을 가지고 있습니다.
먼저, AppComponent 의 @Component 어노테이션에 ActivityModule 들을 삽입합니다.
@Singleton
@Component(modules = {
AppModule.class,
AndroidInjectionModule.class,
AndroidSupportInjectionModule.class,
ActivityModule.class,
FragmentModule.class,
ViewModelModule.class,
ServiceModule.class,
BroadcastReceiverModule.class,
ContentProviderModule.class
})
public interface AppComponent {
void inject(MainApplication application);
@Component.Builder
interface Builder {
@BindsInstance
Builder application(Application application);
AppComponent build();
}
}
그 다음, 적용할 Activity 나 Service 들에 @InjectActivity / @InjectFragment / @InjectService / @InjectBroadcastReceiver / @InjectContentProvider 를 부착합니다.
@InjectActivity
public class MainActivity extends BaseActivity {
@Inject SharedPreferences sharedPreferences;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
protected void onStart() {
super.onStart();
Log.d("MainActivity", sharedPreferences.getAll());
}
그 다음 Application 내에 @InjectApplication 어노테이션을 부착한 뒤 아래 필드를 삽입하고, HasActivityInjector, HasServiceInjector, HasBroadcastReceiverInjector, HasContentProviderInjector 인터페이스들을 구현합니다.
@Inject DispatchingAndroidInjector<Activity> mActivityDispatchingAndroidInjector;
@Inject DispatchingAndroidInjector<Service> mServiceDispatchingAndroidInjector;
@Inject DispatchingAndroidInjector<BroadcastReceiver> mBroadcastReceiverDispatchingAndroidInjector;
@Inject DispatchingAndroidInjector<ContentProvider> mContentProviderDispatchingAndroidInjector;
그 다음, Application.onCreate 에서 생성한 AppComponent 를 DaggerAutoInject 란 클래스에 넘겨줍니다.
전체 코드는 다음과 같습니다.
@InjectApplication(component = AppComponent.class)
public class MainApplication extends Application implements HasActivityInjector, HasServiceInjector,
HasBroadcastReceiverInjector, HasContentProviderInjector {
@Inject DispatchingAndroidInjector<Activity> mActivityDispatchingAndroidInjector;
@Inject DispatchingAndroidInjector<Service> mServiceDispatchingAndroidInjector;
@Inject DispatchingAndroidInjector<BroadcastReceiver> mBroadcastReceiverDispatchingAndroidInjector;
@Inject DispatchingAndroidInjector<ContentProvider> mContentProviderDispatchingAndroidInjector;
@Override
public void onCreate() {
super.onCreate();
final AppComponent appComponent = DaggerAppComponent.builder()
.application(this)
.build();
DaggerAutoInject.init(this, appComponent);
}
@Override
public AndroidInjector<Activity> activityInjector() {
return mActivityDispatchingAndroidInjector;
}
@Override
public AndroidInjector<Service> serviceInjector() {
return mServiceDispatchingAndroidInjector;
}
@Override
public AndroidInjector<BroadcastReceiver> broadcastReceiverInjector() {
return mBroadcastReceiverDispatchingAndroidInjector;
}
@Override
public AndroidInjector<ContentProvider> contentProviderInjector() {
return mContentProviderDispatchingAndroidInjector;
}
}
그 다음 Fragment 의 경우에는 앱의 BaseActivity 클래스에 HasSupportFragmentInjector 를 구현하고, DispatchingAndroidInjector<Fragment> dispatchingFragmentInjector;
필드를 삽입해서 supportFragmentInjector() 메서드에 반환합니다.
public class BaseActivity extends AppCompatActivity implements HasSupportFragmentInjector {
@Inject
DispatchingAndroidInjector<Fragment> dispatchingFragmentInjector;
@Override
public AndroidInjector<Fragment> supportFragmentInjector() {
return dispatchingFragmentInjector;
}
}
이 단계에서 DaggerAutoInject 를 사용할 준비는 모두 마쳤으며, 각 구성요소들의 onCreate 에서 AndroidInjection.inject(this);
를 호출하면 내부적으로 Dependency Injection 를 시행합니다. 단, Activity / Fragment 는 자동으로 inject 메서드를 호출하므로 따로 할 필요가 없습니다.
주의할 점은 이 dispatching 필드들은 Type Parameter 에 있는 클래스가 앱에 하나라도 존재 해야 작동합니다. 만일 앱에 Service 가 없는데 DispatchingAndroidInjector 를 사용하려 한다면 컴파일 단계에서 오류가 나옵니다.
ViewModel 의 경우에는 조금 다른 구현 방법 및 용도를 가집니다. ActivityModule 는 그 자체가 Contribute 의 용도를 가지지만 ViewModelModule 는 위에서도 언급했던 Map<Class<?>, Provider> 를 구성하게 도와주는 용도로 사용됩니다.
processHolders 메서드까지는 같지만, 생성하는 부분은 다른 메서드를 사용합니다.
private void constructViewHolderModule() {
final TypeSpec.Builder builder = TypeSpec.classBuilder(Constants.VIEWHOLDER_MODULE)
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.addAnnotation(Constants.DAGGER_MODULE);
for (ContributesHolder contributesHolder : mViewModelHolders.values()) {
TypeName typeName = contributesHolder.classNameComplete;
String parameterName = String.valueOf(contributesHolder.className.charAt(0)).toLowerCase() +
contributesHolder.className.substring(1);
builder.addMethod(MethodSpec.methodBuilder(Constants.METHOD_BIND + contributesHolder.className)
.addAnnotation(Constants.DAGGER_BINDS)
.addParameter(typeName, parameterName)
.addAnnotation(Constants.DAGGER_INTOMAP)
.addAnnotation(AnnotationSpec.builder(ViewModelKey.class)
.addMember("value", contributesHolder.className + ".class").build())
.addModifiers(Modifier.ABSTRACT)
.returns(Constants.VIEWMODEL)
.build()
);
}
final TypeSpec newClass = builder.build();
final JavaFile javaFile = JavaFile.builder(Constants.PACKAGE_NAME, newClass).build();
try {
javaFile.writeTo(System.out);
javaFile.writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
}
위 결과로 생성된 ViewModelModule 는 아래와 같은 형태를 가지게 됩니다.
@Module
public abstract class ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(MainViewModel.class)
abstract ViewModel bind_MainViewModel(MainViewModel mainViewModel);
}
이 ViewModelModule 를 사용하기 위해서는 아래 요소가 필요합니다.
이 요소를 구현한 것이 다음 코드입니다. 설명은 주석으로 갈음합니다.
@Singleton // 한번 의존성이 생성되고 난 후에는 기존 인스턴스를 그대로 사용
/*
* AAC 의 ViewModelProvider.Factory 를 상속하는 클래스를 제작.
* 생성자로는 Map<Class<*>, Provider<ViewModel>> 를 받는데, 이 생성자는 미리 @IntoMap 와 @ViewModelKey 를 부착하여
* Module 에 제공된 ViewModel 클래스의 클래스 객체와 그 ViewModel 의 생성자를 제공하는 Provider 객체를 각각 key, value로서 받는다.
* 따라서, Map에는 MainViewModel.class.java 라는 키에 Provider<MainViewModel> 가 제공됨
* 이 Provider 는 해당 ViewModel 에 대한 생성자를 제공할 수 있는 기능을 가지고 있기 때문에,
* 실제로 ViewModel 의 생성자가 수십개 이상 있어도 그 생성자가 Dagger 에 의해 제공된다면 실제로는 의존성만 가져오면 됨
*/
class DaggerViewModelFactory @Inject constructor(private val creators: Map<Class<*>,
@JvmSuppressWildcards Provider<ViewModel>>) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
/*
* creators 에서 주어진 key로 찾는데, 해당 값이 없으면
* creators 에 modelClass 가 접근 가능한 요소를 찾아서 값을 얻어낸다.
* 그래도 값이 없으면, IllegalArgumentException 예외를 발생시킨다.
* creator 의 실제 타입이 나오지 않았는데, Kotlin에서는 타입 추론이 가능하기 때문에
* 실제 타입을 명시하지 않아도 된다. 이 경우 추론된 타입은 Provider<T>, 즉 Provider<ViewModel> 이다.
*/
val creator = creators[modelClass] ?:
creators.asIterable().firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
?: throw IllegalArgumentException("unknown model class " + modelClass)
// 찾은 Provider<ViewModel> 반환
return try {
creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
@Module
public abstract class BaseBindsModule {
// 생성한 DaggerViewModelFactory 클래스를 @Binds 를 통해 @Module 에 설정
@Binds
abstract ViewModelProvider.Factory bindViewModelFactory(DaggerViewModelFactory factory);
}
마지막으로 ViewModel 에 @InjectViewModel 를 부착하고, Activity 에서 ViewModelProvider.of 로 가져옵니다.
@InjectViewModel
public class MainViewModel extends AndroidViewModel {
@Inject
public MainViewModel(@NonNull MainApplication application) {
super(application);
}
}
@InjectActivity
public class MainActivity extends BaseActivity {
@Inject ViewModelProvider.Factory mViewModelFactory;
private MainViewModel mViewModel;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
mViewModel = ViewModelProviders.of(this, mViewModelFactory).get(MainViewModel.class);
}
}
이 방법의 장점은 해당 ViewModel 의 생성자가 Dagger 에 의해 주입된다면 Activity 에서 ViewModel 의 인스턴스를 가져올 때 생성자를 신경쓰지 않아도 된다는 점입니다. 단순히 보기에는 작성할 코드가 많지만, 실제 구현체인 MainActivity, MainViewModel 를 제외하면 프로젝트 특성을 가진 코드가 아니므로 Base화를 하여 구성해도 문제가 없습니다.
어떻게 보면 최종 사용자 (End-Developer) 의 할 일을 많이 줄였지만, 아래의 개선점은 있습니다.
첫번째로, 위에서도 소개했던 Member-injections methods 의 자동화 여부 입니다. 코드가 적다고 해도 나름대로 일은 일이기 때문입니다.
두번째로, 주입될 의존성들은 앱 전역으로 inject 가 되는데, 이를 일부 범위에서 inject 되게 할 수 있는 Scope 기능을 적용하지 못합니다. 단, 이쪽은 특정 Scope 어노테이션을 부착하기만 하면 되므로 InjectActivity 의 파라미터로 제공하게 하면 문제가 없을거라 판단됩니다.
개선점을 찾게 되면 글을 업데이트 하도록 하겠습니다.
이 라이브러리를 통하여 자동화를 하게 되면, 각 Activity 에 대한 Scope 를 지정할 수 없다는 것이 문제가 되는 것은 알고 있지만, 어떻게 해야 좋은 방법일지는 아직까지 고민중입니다.