DaggerAutoInject - Contributing to AndroidInjection with Annotation

WindSekirun (wind.seo)·2022년 4월 26일
0

이 글은 기존 운영했던 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 로 오게 되면 작성해야 하는 코드는 매우 많아집니다.

  • MainActivity 에 대한 Subcomponent 를 만드는데, 이 Subcomponent 는 AndroidInjector 를 상속하고 있어야 하고, abstract 클래스인 Builder 를 구현해야 함
  • 만든 MainActivitySubCompoent.Builder 를 @Binds, @IntoMap, @Activitykey 등의 메서드로 제공하는데, 내부적으로 Map<Class<?>, Provider > 를 가지고 있어, 필요한 때에 Provider를 제공해야 함
  • 위 두 가지를 모두 포함한 클래스를 만들고, 해당 클래스를 @Module로 설정한 다음 Module 의 파라미터로 MainActivitySubcomponet 를 제공해야 함.

위 세 가지를 모두 반영한 것이 아래 코드입니다.

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 의 구현 방법

ActivityModule / ServiceModule / FragmentModule / BroadcastReceiverModule / ContentProviderModule 에 대해서는 모두 같은 구현 방법을 취합니다.

  1. @InjectActivity 를 모두 찾아 ContributesHolder 란 객체에 담고, Map<ClassName, ContributesHolder> 로 가지고 있는다.
  2. 각 어노테이션 별 TypeSpec (클래스를 생성할 스펙) 를 만들고 map 를 반복문에 통과시켜서 @ContributesAndroidInjector abstract SimpleName contributes_SimpleName(); 라는 메서드를 생성한다.
  3. TypeSpec 를 Java 파일로 만든다.

위 과정을 모두 포함하는 것이 다음 코드입니다.

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 를 사용하려 한다면 컴파일 단계에서 오류가 나옵니다.

ViewModelModule 의 구현 방법

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 를 사용하기 위해서는 아래 요소가 필요합니다.

  1. ViewModelModule 로 통하여 제공된 Map<Class<?>, Provider> 를 사용하여 실제 ViewModel 의 객체를 반환할 Factory 클래스. 이 클래스는 ViewModelProvider.Factory 를 상속하여 Android Architecture components 의 ViewModelProvider.of 로 가져올 수 있게 합니다.
  2. 1번에서 생성할 Factory 클래스를 주입할 Module 클래스.

이 요소를 구현한 것이 다음 코드입니다. 설명은 주석으로 갈음합니다.

@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 를 지정할 수 없다는 것이 문제가 되는 것은 알고 있지만, 어떻게 해야 좋은 방법일지는 아직까지 고민중입니다.

profile
Android Developer @kakaobank

0개의 댓글