Diary #1, Dagger의 Child Fragment 관련

주의사항!

이 포스트는 주인장(@WindSekirun) 과 여러 사람들과 개발(특히 Android 관련이 많습니다)에 관련된 이야기를 할 때, 간단히 다루기 좋은 이야기를 있는 그대로 표현하는 포스트입니다. 대화 내용은 보기 좋게만 변경하고, 오탈자는 수정하지 않습니다. 만일 대화중 틀린 내용이나 보강할 내용이 있다면 댓글로 알려주시면 감사합니다!

아니면 텔레그램 WindSekirun 으로 주셔도 됩니다.

이번 주제

Dagger에서 ChildFragment를 ParentFragment가 관리해야 되는 것인가, 아니면 Activity가 관리해야 되는 것인가?

등장인물

  • Pyxis, 주인장
  • @Zeallat , 언제나 도움을 주셔서 매우 감사한 분입니다.

정답

등잔 밑이 어둡다고, Dagger 공식 레포 상에 답이 있습니다. 링크는 [여기] 로, DialogFragment의 베이스 클래스의 구현체입니다. 보면 onAttach에서 AndroidSupportInjection 을 통해 injection 하는 것을 확인할 수 있습니다.

즉 ChildFragment는 Parent Fragment가 관리하는 게 맞고, injection 은 위를 참고하면 됩니다

대회내용

  • Pyxis: 무튼
  • Pyxis: Dagger 에 스코프가 있을 때
  • Pyxis: Activity : Service: BroadcastReceiver: ContentProvider는
  • Pyxis: Application 스코프잖아요
  • Pyxis: 그래서 DispatchingAndroidInjector 를 Application에서
  • Pyxis: 구현하고
  • Pyxis: 그리고 Fragment는
  • Pyxis: 공식 컴포넌트가 아니고: Activity에 종속적이기 때문
  • Pyxis: 에
  • Pyxis: Activity에 종속적이고
  • Pyxis: 그리고 DialogFragment가 있어요
  • Pyxis: Fragment도 Activity 종속이고: 다이얼로그도 Activity종속이라 볼 수 있어요 (Window Token 관련 살펴보면 이렇게 결론 내릴 수 있음)
  • Pyxis: 여기서: DialogFragment 안에 ChildFragment가 있다고 할때
  • Pyxis: 이 Fragment는 Activity에 관리하는게 맞을까요
  • Pyxis: 아니면 parnet fragment (여기서는 DialogFragment)가 관리해야될까요
  • Zeallat: 스코프를요?
  • Pyxis: DispatchingAndroidInjector를 받아서
  • Zeallat: activity life cycle에 맞추느냐
  • Pyxis: HasSupportFragmentInjector 같은거
  • Pyxis: 구현하고
  • Zeallat: fragment lifecycle에 맞추느냐
  • Zeallat: 그건가여?
  • Pyxis: 라이프사이클 관련
  • Pyxis: 네 어떻게 보면 라이프사이클이네여
  • Pyxis: 저는 진짜 왠만하면
  • Pyxis: child fragment는 안만들려고 하거든요
  • Pyxis: 예전에 당한게 하도 많아서
  • Zeallat: 어떤 케이스이죠?
  • Pyxis: 그래서 저 경우를 아예 생각하고 있지 않았는데
  • Pyxis: DialogFragment 하나로
  • Pyxis: 여러개의 Fragment를 replace?
  • Pyxis: 하는 그런목적인거같아요
  • Pyxis: 지금 구성하는 앱이
  • Pyxis: 연속적으로 다이얼로그만 4~6개 뜨게 되있어서
  • Pyxis: 하나하나 dismiss해서 하는거보다 내부에서 관리하는게
  • Pyxis: 더 효율성있다고는 보거든요
  • Zeallat: 음
  • Zeallat: 그러니까
  • Zeallat: 페이지 하나에서 fragment(팝업)을 여러개 표시해야하는데
  • Zeallat: 그걸 replace로 하기위해서
  • Zeallat: dialog 하나 만들고 그안에서
  • Zeallat: child 여러개를 컨트롤한다는 이야기인가요?
  • Pyxis: Exactly
  • Zeallat: 그런데
  • Zeallat: 이 parent fragment가
  • Zeallat: 여러 activity에서 불리는 시츄에이션인가요?
  • Pyxis: 아뇨
  • Pyxis: 기획적으로는 하나의 activity
  • Pyxis: 에서만 불리는데
  • Zeallat: 그럼 왜 굳이 parent를 만들어요?
  • Pyxis: 한 화면내에서 여러번 불릴 가능성은
  • Pyxis: 있어요
  • Zeallat: 그냥 activity에서 replace 컨트롤 하면 안되나요?
  • Pyxis: activity는
  • Zeallat: 아니면 공통 view를 공유하나요?parent의?
  • Pyxis: 그대로 두고
  • Pyxis: parent의 공통 뷰를
  • Pyxis: 공유하죠
  • Pyxis: parent dialog에는 타이틀이랑 x버튼
  • Pyxis: 같은거 두고
  • Pyxis: 그 안 fragment에서는
  • Pyxis: N:1 관계로
  • Pyxis: (1이 ViewModel)
  • Pyxis: 상태 관리하고
  • Pyxis: 대충 어떤 시나리오인지
  • Pyxis: 예상가시나여?
  • Zeallat: 네 이해가요
  • Zeallat: parent fragment에서 단순 호출이아니고
  • Zeallat: parent의 일부 뷰에만
  • Zeallat: child를 표시한다는거죠?
  • Pyxis: 네
  • Pyxis: AndroidSupportInjection.inject 코드 보니까
  • Pyxis: parent fragment를 찾는거같은 코드가
  • Pyxis: 있더라고요
  • Pyxis: 그렇게 되면 제 생각은
  • Pyxis: ParentFragment 가 ChildFragment를 관리하는게
  • Pyxis: 관점에선 맞아보이는데
  • Zeallat: 일단 뭐 그런 케이스라면
  • Zeallat: parent fragment 죽으면
  • Zeallat: child도 다 죽어야죠
  • Pyxis: 그렇겠져
  • Zeallat: scope측면에서
  • Zeallat: parent에 종속적이여야하죠
  • Pyxis: 그렇죠
  • Pyxis: 그게 좀 더 하긴 하는데
  • Pyxis: AndroidSupportInjection 쓰니까
  • Pyxis: Non injected 로그 뜨면서
  • Pyxis: 인젝트가 안되더라고요
  • Pyxis: parent fragmentㅇ서
  • Pyxis: 에서
  • Pyxis: 여기서 parent fragment인 DialogFragment에 injection이 왜 필요한가
  • Pyxis: 하면
  • Pyxis: scope 의 생성은 HasSupportFragmnetInjector 인터페이스 기반이라
  • Pyxis: 서브컴포넌트도 이 기준으로 생성되고
  • Pyxis: 그래서 parent fragment가 child fragment를 가지기 위해서는
  • Pyxis: parent fragment가 HasSupportFragmentInjector를 구현하고
  • Pyxis: DIspatchingAndroidInjector를 반환해야 되는데
  • Pyxis: 구현하는거까진 문제가 없는데
  • Pyxis: DialogFragment의 onCreateView에서 AndroidSupportInjection 하니까
  • Pyxis: 죽더라고요
  • Pyxis: 그래서 일단 임시방편으로 AppComponent에다가
  • Pyxis: inject 메서드 만들어놓고
  • Pyxis: 그걸 호출하는 식으로 강제 인젝트는 했는데
  • Pyxis: 결론적으로
  • Pyxis: 이 접근방법이 맞는건지: 그리고 이 경우에 dialogfragment에 정상적으로 inject하는 방법이 어떤건지
  • Pyxis: 이 두개가 문제였어요
  • Pyxis: 일단 dagger-android 자체는 DialogFragment를 정식적으로 지원하고는 있어요
  • Pyxis: https://github.com/google/dagger/blob/master/java/dagger/android/DaggerDialogFragment.java
  • Pyxis: ?
  • Pyxis: 잠만
  • Pyxis: 이 링크가 답이네요
  • Pyxis: ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
  • Zeallat: 휴 오늘도 제가 Pyxis님 고민을 한건 해결해드렸군요
  • Zeallat: Pyxis님 저 없으면 어떻게 사시나요
  • Pyxis: ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
  • Zeallat: 하하
  • Pyxis: 혼자 막 정리해보니
  • Pyxis: 답이 나옴
  • Zeallat: 그래서 답이뭔가요?
  • Zeallat: 그냥 저 링크식으로
  • Pyxis: 네
  • Zeallat: android injection
  • Zeallat: 하면되나여
  • Pyxis: onAttach에서
  • Pyxis: 해주면 되는거같아요]
  • Zeallat: onCreateView가 아니고
  • Zeallat: onAttatch가 해담인가요
  • Zeallat: ㅋㅋ
  • Pyxis: 뭐 그러면 복잡하게 할 필요 없이
  • Pyxis: DialogFragment에다가 InjectFragmnet달아놓고
  • Pyxis: onAttach에다가 걸면 되겠네여
  • Pyxis: 담 버전부터 대응해봐야될듯

OkHttp Interceptor Multibindings with Dagger (ENG)

Introduction

Looking at the structure of the app I am currently using, the sub-project depends on the core module.
In the world, this structure called this a ‘Multi-Module projects’
Of course, the ‘Core’ module managed by the ‘JFrog artifactory’ server.

One reason for using Multi-Module projects structure is there are many boilerplate codes for an individual project.
Another reason is code can be recyclable with not depend on the specifics of individual projects.

So that, Dagger also managed both modules such as ‘Core’ module and sub-projects.
In ‘Core’ module, they have ‘@Module’ class, In sub-projects, they have project-specific ‘@Module’ class and ‘@Component’ class.

The thing with I am, ‘Retrofit’ or ‘OKHttpClient’ for networking module managed with ‘Core’ module, but Interceptor of OKHttpClient can have specifics of projects. Also, Interceptor might be 1 or more.

Solve this problem, Make interceptors into collections and inject when making an instance using ‘Multibinding’ feature of Dagger.

The language of example code is Kotlin and Java.

What is Multibindings?

First, we should explain about ‘Multibinding’ in Dagger.

Multibindings is a feature that has the power to collect the same type of instance with binding in a different module.
In Dagger, process with collections did not depend on individual bindings.

Dagger provides two kinds of Multibindings, one is ‘@IntoSet’ annotation, other is ‘@IntroMap’ annotation.

Two ‘Multibindings’ annotations provide a different solution for making collections.
One of the annotations, ‘@IntoSet’ provides the collection of instances with ‘Set’.
Another ‘@IntoMap’ provides the collection of instances with ‘Map<Class<*>, Provider<T>>’.

Today, I’ll use ‘@IntoSet’ annotation.

Make the Interceptor

First, I need Interceptor that provides into Dagger.

    class TestInterceptor : Interceptor {
        @Throws(IOException::class)
        override fun intercept(chain: Interceptor.Chain): Response? {
            val original = chain.request()
            val originalHttpUrl = original.url()
            val requestBuilder = original.newBuilder()
                .url(originalHttpUrl.newBuilder().build())
            return chain.proceed(requestBuilder.build())
        }
    }

Next, Provide Interceptor into ‘@Module’ annotated class.

    @Module
    public class AppInterceptorModule {
        @Provides
        @IntoSet
        public Interceptor provideTestInterceptor() {
            return new TestInterceptor();
        }
    }

The basic form of providing is same as other providers, except ‘@IntoSet’ annotations.
If you want to use ‘Multibindings’ feature in another class, You can set ‘Qualifier‘ annotation.

Inject when making an instance of OKHttpClient

    @Provides
        fun provideClient(interceptors: Set<@JvmSuppressWildcards Interceptor>): OkHttpClient {
            val builder = OkHttpClient().newBuilder()
            builder.readTimeout(Config.timeout.toLong(), TimeUnit.MILLISECONDS)
            builder.connectTimeout(Config.connectTimeout.toLong(), TimeUnit.MILLISECONDS)
            if (interceptors.isNotEmpty()) {
                interceptors.forEach {
                    builder.addInterceptor(it)
                }
            }
            return builder.build()
    }

Looking at the code of above, they have ‘Set’ parameters with named with ‘interceptors’.
Dagger will inject their collection of Interceptor into this parameter.

You can check the empty state of Set and add Interceptor into OKHttpClient using ‘builder.addInterceptor()’.

In this code, You might notice the ‘@JvmSuppressWildcards’ annotation in Type parameter of ‘Set’.
‘@JvmSuppressWildcards’ annotation is Kotlin annotations for interop with Java.

In default, Kotlin compiler converts ‘Set<Interceptor>’ into ‘Set<? extends Interceptor>’ type while Dagger needs `Set<Interceptor>. It can be a compile-time error.
In addition, If the type of Type parameters has a final modifier such as String, Kotlin compiler doesn’t generate wildcard.

Solve this problem, we can attach ‘@JvmSuppressWildcards’ to not convert to ‘Set’.

Looking generated code

When compiler builds success, we can check generated code in DaggerAppComponent class.

First, Dagger generated fields named ‘setOfInterceptorProvider’. this field contains an instance of Set.

    private Provider<Set<Interceptor>> setOfInterceptorProvider;

Here is code to assign ‘setOfInterceptorProvider’ field.

    this.setOfInterceptorProvider =
            SetFactory.<Interceptor>builder(2, 0)
                .addProvider(provideTestInterceptorProvider)
                .addProvider((Provider) logInterceptorProvider)
                .build();

In test projects, they have an interceptor in the ‘Core’ module and interceptor in sub-projects. so Dagger assigns the size of Set to 2.

Here is code to use ‘setOfInterceptorProvider’ field.

     this.provideClientProvider =
            BaseProvidesModule_ProvideClientFactory.create(
                builder.baseProvidesModule, setOfInterceptorProvider);

Then, Class named ‘BaseProvideModule_ProvideClientFactory’ uses ‘setOfInterceptorProvider’ to process binding into ‘provideClient’ methods.

public final class BaseProvidesModule_ProvideClientFactory implements Factory<OkHttpClient> {
    private final BaseProvidesModule module;
    private final Provider<Set<Interceptor>> interceptorsProvider;

    public BaseProvidesModule_ProvideClientFactory(BaseProvidesModule module, Provider<Set<Interceptor>> interceptorsProvider) {
        this.module = module;
        this.interceptorsProvider = interceptorsProvider;
    }

    public OkHttpClient get() {
        return provideInstance(this.module, this.interceptorsProvider);
    }

    public static OkHttpClient provideInstance(BaseProvidesModule module, Provider<Set<Interceptor>> interceptorsProvider) {
        return proxyProvideClient(module, (Set)interceptorsProvider.get());
    }

    public static BaseProvidesModule_ProvideClientFactory create(BaseProvidesModule module, Provider<Set<Interceptor>> interceptorsProvider) {
        return new BaseProvidesModule_ProvideClientFactory(module, interceptorsProvider);
    }

    public static OkHttpClient proxyProvideClient(BaseProvidesModule instance, Set<Interceptor> interceptors) {
        return (OkHttpClient)Preconditions.checkNotNull(instance.provideClient(interceptors), "Cannot return null from a non-@Nullable @Provides method");
    }
}

Conclusion

‘Multibindings’ is a special feature to process case that provides similar types of ‘Dagger’ or ‘Guice’ that based on code generations.

Also, ‘Multi-Module projects’ structure makes ‘Multibindings’ feature more special that they can help to manage the ‘Core’ module without depends on specifics of the project and manage sub-projects only have the project-specific code.

OkHttp Interceptor Multibindings with Dagger

도입

현재 사용하고 있는 앱의 구조를 살펴보면, 하위 프로젝트가 코어 모듈을 의존하고 있는 형태로, 흔히 말하는 Multi-Module projects이다. 물론, 코어 모듈은 현재 Artifactory 로 관리되어 버전 관리도 되고 있다.

이렇게 구성한 이유로는 1. 의외로 프로젝트마다 들어가는 중복 코드가 많고, 2. 프로젝트 특성에 종속되지 않는 코드여야 재활용이 가능하기 때문이다.

그런 이유로 Dagger 또한 코어 모듈과 하위 프로젝트에서 관리되는데, 코어 모듈에서는 Base~Module 만 가지고 있고, 하위 프로젝트에서 App~Module 와 Component 클래스를 가지고 있다.

문제는, Retrofit, OKHttpClient 등의 객체를 Base~Module, 즉 코어 모듈에서 관리하고 있는데 프로젝트마다 필요한 Interceptor 는 하위 프로젝트에서 들어간다는 점이다. 또, 이 Interceptor가 1개가 아닌 여러개가 될 수 있다.

이를 해결하기 위해 이 글에서는 Dagger 의 Multibindings 기능을 이용하여 Provide 된 Interceptor 등을 Set<Interceptor> 로 받아서 최종적으로 OKHttpClient를 만들 때 추가하려 한다.

언어는 평소대로 Kotlin, 사용부는 Java이다.

Multibindings란?

먼저, Multibindings에 대한 설명이 필요할 것 같다.

Multibindings 는 서로 다른 모듈에 객체가 바인딩되어 있어도 하나의 컬렉션으로서 객체에 바인딩할 수 있는 기능이다. Dagger에서는 이 컬렉션을 분석하여 개개인의 바인딩에 의존하지 않고 처리를 해준다.

종류는 두가지로, @IntoSet@IntoMap 가 있는데, 각각 이름과 같이 Set<T> 와 Map<Class<*>, Provider<T>> 를 제공한다.

이 중에서 오늘 사용할 것은 @IntoSet 이다.

제공될 Interceptor 제작

먼저, Dagger 에 Interceptor 를 제공할 Interceptor 를 만드는데, 예제로 할 주제가 명확하게 떠오르지 않아 원본을 바로 반환하는 기본 형태의 Interceptor 를 만든다.

class TestInterceptor : Interceptor {
    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response? {
        val original = chain.request()
        val originalHttpUrl = original.url()
        val requestBuilder = original.newBuilder()
            .url(originalHttpUrl.newBuilder().build())

        return chain.proceed(requestBuilder.build())
    }
}

그 다음, 제작한 Interceptor 를 Module 에 제공한다.

@Module
public class AppInterceptorModule {

    @Provides
    @IntoSet
    public Interceptor provideTestInterceptor() {
        return new TestInterceptor();
    }
}

평소와 같이 Provide를 하되 @IntoSet 라는 어노테이션을 추가적으로 부착하면 된다. 만일 Multibinding 기능을 다른 기능에도 활용하고 싶으면 Qualifier 어노테이션을 부착하면 된다.

필요한 객체에 제공하기

@Provides
    fun provideClient(interceptors: Set<@JvmSuppressWildcards Interceptor>): OkHttpClient {
        val builder = OkHttpClient().newBuilder()
        builder.readTimeout(Config.timeout.toLong(), TimeUnit.MILLISECONDS)
        builder.connectTimeout(Config.connectTimeout.toLong(), TimeUnit.MILLISECONDS)
        if (interceptors.isNotEmpty()) {
            interceptors.forEach {
                builder.addInterceptor(it)
            }
        }
        return builder.build()
}

자세히 보면 파라미터에 interceptors: Set<Interceptor> 가 보이는데, 이 쪽으로 Multibindings 으로 구성된 컬렉션이 추가된다. Set이므로 비어있지 않으면 간단히 forEach 로 builder 에 Interceptor를 추가하면 된다.

여기서 @JvmSuppressWildcards 란 어노테이션이 있는데, 코틀린 컴파일러는 기본적으로 Set<Interceptor> 를 Set<? extends Interceptor> 로 변환한다. 이 때, 대거가 제공하는 컬렉션 객체는 Set<Interceptor> 이므로 Set<? extends Interceptor> 를 찾을 수 없다면서 오류가 나온다.

단, 해당 타입 파라미터가 final 이면 Wildcards 가 생성되지 않는데, Set<String> 가 그렇다.

이 때에는 @JvmSuppressWildcards 를 붙여 Set<? extends Interceptor> 가 아닌 Set<Interceptor> 로 구성되게 하면 된다.

생성된 코드 살펴보기

위 작업까지 마치고 빌드가 성공했을 경우, DaggerAppComponent에 관련 부분이 생성된 것을 확인할 수 있다.

먼저, setOfInterceptorProvider 라는 필드가 생성되는데, 이 곳이 Set<Interceptor> 를 보관하는 곳이다.

private Provider<Set<Interceptor>> setOfInterceptorProvider;

값을 할당하는 부분은 다음과 같다.

this.setOfInterceptorProvider =
        SetFactory.<Interceptor>builder(2, 0)
            .addProvider(provideTestInterceptorProvider)
            .addProvider((Provider) logInterceptorProvider)
            .build();

테스트에 사용된 프로젝트에서는 코어 모듈에 Interceptor 가 1개, 제작한 Interceptor 1개로 총 두 개가 선언되어있어 Set<Interceptor>를 생성하는 SetFactory.Builder 에 2가 기재되있는 것을 확인할 수 있다.

만들어진 setOfInterceptorProvider 를 사용하는 곳은 바로 밑에 나온다.

 this.provideClientProvider =
        BaseProvidesModule_ProvideClientFactory.create(
            builder.baseProvidesModule, setOfInterceptorProvider);

그리고 해당 BaseProvidesModule_ProvideClientFactory 에서 setOfInterceptorProvider 의 값을 얻어 바인딩을 진행한다.

public final class BaseProvidesModule_ProvideClientFactory implements Factory<OkHttpClient> {
    private final BaseProvidesModule module;
    private final Provider<Set<Interceptor>> interceptorsProvider;

    public BaseProvidesModule_ProvideClientFactory(BaseProvidesModule module, Provider<Set<Interceptor>> interceptorsProvider) {
        this.module = module;
        this.interceptorsProvider = interceptorsProvider;
    }

    public OkHttpClient get() {
        return provideInstance(this.module, this.interceptorsProvider);
    }

    public static OkHttpClient provideInstance(BaseProvidesModule module, Provider<Set<Interceptor>> interceptorsProvider) {
        return proxyProvideClient(module, (Set)interceptorsProvider.get());
    }

    public static BaseProvidesModule_ProvideClientFactory create(BaseProvidesModule module, Provider<Set<Interceptor>> interceptorsProvider) {
        return new BaseProvidesModule_ProvideClientFactory(module, interceptorsProvider);
    }

    public static OkHttpClient proxyProvideClient(BaseProvidesModule instance, Set<Interceptor> interceptors) {
        return (OkHttpClient)Preconditions.checkNotNull(instance.provideClient(interceptors), "Cannot return null from a non-@Nullable @Provides method");
    }
}

정리

Multibindings 기능은 코드를 생성하는 것에 기반을 두는 Dagger 나 Guice가 가지는 특별한 기능으로 여러 개의 비슷한 객체가 제공될 수 있는 경우에 좀 더 쉽게 처리할 수 있게 해준다.

특히, 코어 모듈과 하위 프로젝트를 따로 관리하는 입장으로서는 코어 모듈은 프로젝트 종속되지 않게 관리를, 하위 프로젝트에서는 프로젝트 특성을 띈 코드만 가지게 할 수 있다는 점이 이 Multibindings 를 한 층 더 특별히 만들어 주는 것 같다.