https://dagger.dev/hilt/migration-guide


7.1 Migration – Guide

Hilt로 마이그레이션 하는 것은 코드베이스 상태와 코드베이스가 따르는 관행 또는 패턴에 따라 매우 다양할 수 있다. 이 페이지는 앱 마이그레이션시 발생할 수 있는 몇가지 일반적인 문제에 대한 권고사항을 제공한다. 이 페이지는 여러분이 이미 기본 Hilt API를 이해하고 있다고 가정한다. 그렇지 않은 경우 Hilt를 위한 ‘Quick Start’를 먼저 참조하자. 이 페이지는 또한 Dagger에 대한 일반적인 이해를 가정하고 있다. 왜냐하면 이 페이지는 Dagger를 코드베이스로 이미 사용하고 있어 이를 마이그레이션 하려는 사람들에게만 필요하기 때문이다. 코드베이스가 Dagger를 사용하지 않는 경우, Dagger 설정으로부터 마이그레이션이 필요 없는 ‘Quick Start’ 가이드 문서를 통해 앱에 Hilt를 추가하자.

리팩토링 Tip : 클래스 코드를 수정할 때, 사용하지 않거나 더이상 존재하지 않는 import들이 파일에서 삭제 되었는지 확인하자.

0. 마이그레이션 계획하기

Hilt로 마이그레이션 할 때, 작업을 단계별로 구성하고 싶을 것이다. 이 가이드는 대부분의 경우에 알맞는 일반적인 접근 방식을 제시하지만 경우에 따라서 다를 수는 있다. 권장하는 접근 방식은 Application 또는 @Singleton 컴포넌트에서 시작여 점진적으로 확장해 나가는 방법이다. Application 그리고 @Singleton 이후에 Activity를 마이그레이션 한 후 Fragment들을 작업하자. 전반적으로 점진적인 마이그레이션을 해야 한다. 상대적으로 적은 코드베이스가 있는 경우에도 마이그레이션을 점진적으로 수행하면 단계별로 진행 상황을 확인할 수 있다.

컴포넌트 계층 비교하기

가장 먼저해야 할 일은 현재 컴포넌트 계층 구조를 Hilt의 계층 구조와 비교하는 것이다. 어떤 컴포넌트를 어떤 Hilt 컴포넌트에 매핑할지 결정해야 한다. 이 방법은 비교적 간단해야 하지만, 명확한 매핑이 없는 경우 Dagger 컴포넌트를 구성하듯 사용자화 컴포넌트를 구성할 수 있다. 이러한 컴포넌트는 Hilt 컴포넌트의 하위 항목이 될 수 있다. 그러나 Hilt는 컴포넌트 계층 사이에 삽입하는 것은 허용하지 않는다 (예: Hilt 컴포넌트의 상위컴포넌트를 변경 하는 것). 이 가이드 다음에 나올 내용인 ‘사용자화 컴포넌트’ 섹션을 확인하자. 이 가이드의 나머지 부분에서는 컴포넌트가 모든 Hilt 컴포넌트에 직접 매핑되는 마이그레이션을 한다고 가정한다.

또한 코드에서 컴포넌트 의존성을 사용하는 경우 다음에 나올 ‘컴포넌트 의존성’ 섹션을 먼저 읽도록 하자. 이 가이드 문서의 나머지 부분에서는 서브 컴포넌트를 사용한다고 가정한다.

dagger.android의 @ContributesAndroidInjector를 사용 중이고 컴포넌트 계층 구조가 확실하지 않은 경우, 계층 구조를 Hilt 컴포넌트와 대략적으로 맞추도록 하자.

Hilt가 언제 클래스들을 주입하는지 알자

Hilt가 각 Android 클래스에 대한 클래스를 언제 주입하는지 ‘컴포넌트의 생애’에서 확인할 수 있다. 이것들은 코드가 현재 주입하는 위치와 유사해야 하지만 그렇지 않은 경우 코드에 차이가 있는 경우를 염두에 두자.

마이그레이션 개요

마이그레이션이 끝나면 코드를 다음과 같이 변경해야 한다:

  • 모든 @Component / @Subcomponent (또는 dagger.android @ContributesAndroidInjector를 사용하는 경우) 해당 사용방법은 제거해야 한다.
  • 모든 @Module 클래스는 @InstallIn 어노테이션을 추가해야 한다.
  • 모든 Application / Activity / Fragment / View / Service / BroadcastReceiver 클래스에는 @AndroidEntryPoint 어노테이션을 추가해야 한다.
  • 컴포넌트를 인스턴스화 하거나 전파하는 코드 (예: 컴포넌트를 노출시키기 위한 Activity의 인터페이스)를 제거해야 한다.
  • 모든 dagger.android에 대한 참조를 제거해야 한다.

1. Application 마이그레이션 하기

가장 먼저 변경해야 할 것은 Application 및 @Singleton 컴포넌트를 생성되는 Hilt의 ApplicationComponent로 마이그레이션하는 것이다. 이를 위해 먼저 현재 컴포넌트에 설치된 모든 것이 Hilt의 ApplicationComponent에 설치되어 있는지 확인해야 한다.

컴포넌트 마이그레이션하기

Application을 마이그레이션 하기 위해, 기존에 존재하던 @Singleton 컴포넌트의 모든 것을 ApplicationComponent로 마이그레이션 한다.

a. 모듈 다루기

먼저 모든 모듈을 ApplicationComponent에 설치해야 한다. 컴포넌트에 현재 설치된 각 모듈에 @InstallIn (ApplicationComponent.class) 어노테이션을 추가 한다. 많은 모듈이 있는 경우 지금 모든 모듈을 변경하는 대신 모든 현재 모듈을 포함하는 단일로 통합하는 @Module 클래스를 만들고 설치할 수 있다. 그러나 이는 @UninstallModules와 같은 Hilt 기능을 최대한 활용하려면 나중에 통합된 모듈을 분리해야 하기 때문에 임시 솔루션 일 뿐이다.

// 이 컴포넌트는 이렇게 시작해서
@Component(modules = [
    FooModule::class,
    BarModule::class,
    ...
])
interface MySingletonComponent {
}

// 다음과 같이 바뀐다.
@InstallIn(ApplicationComponent::class)
@Module(includes = [
    FooModule::class,
    BarModule::class,
    ...
])
interface AggregatorModule {}
Warning : @InstallIn 어노테이션을 달지 않은 모듈은 Hilt에서 사용되지 않는다. 어노테이션이 없는 모듈이 발견되면 기본적으로 Hilt에서 오류가 발생하지만 이 오류는 비활성화 할 수 있다.

b. 확장한 인터페이스 또는 모듈 다루기

@EntryPoint를 사용하여 현재 컴포넌트를 확장하는 모든 인터페이스에 대해 비슷한 처리를 할 수 있다.

컴포넌트의 인터페이스는 일반적으로 주입 방법을 추가하거나 바인딩 또는 서브컴포넌트와 같은 타입에 접근할 때 사용된다. Hilt로 마이그레이션이 완료되면, Hilt가 이를 생성하거나 Hilt 도구로 대체하기 때문에 이 중 많은 것들이 필요하지 않게 된다. 그러나 마이그레이션을 위해, 이 섹션에서는 코드가 계속 작동하도록 현재 동작을 유지하는 방법에 대해 설명한다. 하지만 이러한 모든 방법을 살펴보고, 마이그레이션을 진행하는 것이 여전히 필요한지 확인하자.

@EntryPoint로 모든 것을 옮기기

@EntryPoint 및 @InstallIn(ApplicationComponent.class)을 컴포넌트를 확장하는 인터페이스에 추가한다. 인터페이스가 많은 경우 하나로 통합한 인터페이스를 작성하여 모듈처럼 모든 인터페이스를 수집하자. 컴포넌트 인터페이스에 직접 정의된 모든 메소드는 통합된 인터페이스 또는 통합된 인터페이스를 확장한 인터페이스로 옮길 수 있다.

예제 코드:

// 이 컴포넌트는 이렇게 시작해서
@Component
@Singleton
interface MySingletonComponent : FooInjector, BarInjector {
    fun inject(myApplication: MyApplication)

    fun getFoo() : Foo
}

// 다음과 같이 바뀐다.
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface AggregatorEntryPoint : FooInjector, BarInjector {
    // 이는 예시로 이동시킨 것이다. 그러나 다음에 나올 내용에서는
    // inject 메서드를 제거할 수 있음을 보여준다.
    fun inject(myApplication: MyApplication)

    fun getFoo() : Foo
}

Inject 메서드

Hilt는 내부에서 Application 클래스 주입을 처리하므로 Application에 대한 주입 메소드가 있으면 제거 할 수 있다. @AndroidEntryPoint를 사용하도록 나중에 마이그레이션되므로 다른 Android 타입의 주입 메소드도 결국 제거해야 한다.

@Component
@Singleton
interface MySingletonComponent {
    // Hilt는 Application을 관리하므로 이 부분은 삭제될 수 있다.
    fun inject(myApplication: MyApplication)

    // @AndroidEntryPoint를 사용한다면 이 부분은 삭제될 수 있다.
    fun inject(fooActivity: FooActivity)
}

인터페이스에 접근하기

코드는 컴포넌트를 직접적으로 반환하거나 인터페이스 타입 중 하나를 반환하는 메서드를 가지므로 다른 코드는 주입 메서드 또는 접근자 메서드를 얻을 수 있게 된다. 마이그레이션 할 때 이 코드가 계속 동작하려면 EntryPoints 클래스를 사용하여 참조 할 수 있다. 마이그레이션이 계속됨에 따라 이러한 메소드를 제거하고 호출 코드가 Hilt EntryPoints API를 직접 사용하도록 해야 한다.

// 이와 같은 코드로 시작 한다면
class MyApplication : Application() {
    fun component(): MySingletonComponent {
        return component
    }
}

// 통합적인 Entry point 를 추가한 후, 코드는 다음과 같은 형태를 갖는다.:
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface AggregatorEntryPoint : LegacyInterface, ... {
}

@HiltAndroidApp
class MyApplication : Application() {
    // 반환형이 AggregatorEntryPoint로 변경되었지만,
    // 이전 컴포넌트가 사용하던 인터페이스를 모두 구현하므로 괜찮다.
    fun component(): AggregatorEntryPoint {
        // EntryPoints를 사용하여 AggregatorEntryPoint의 인스턴스를 가져온다.
        return EntryPoints.get(this, AggregatorEntryPoint::class.java)
    }
}

c. 스코프

Hilt로 컴포넌트를 마이그레이션 할 때, Hilt 스코프 어노테이션을 사용하는 바인딩으로 마이그레이션 하는 작업 또한 필요하다. ApplicationComponent의 경우 @Singleton 어노테이션을 사용한다. ‘컴포넌트 생애’ 에서 어떤 어노테이션이 어떤 컴포넌트에 해당하는지 확인할 수 있다. @Singleton을 사용하지 않고 자체 스코프 어노테이션이 있는 경우, 스코프 별칭(scope alias)를 사용하여 해당 어노테이션이 Hilt 스코프 어노테이션과 동등하다고 Hilt에게 알려줄 수 있다. 이렇게 하면 프로세스 후반에 스코프 어노테이션을 마이그레이션하고 제거 할 수 있다.

d. 컴포넌트 인자 다루기

컴포넌트 초기화 코드가 숨겨져 있으므로, Hilt 컴포넌트는 컴포넌트 인자를 취할 수 없다. 일반적으로 Application 인스턴스 (또는 다른 컴포넌트의 경우 Activity / Fragment 인스턴스)를 Dagger 그래프로 가져 오는데 사용된다. 이러한 경우 ‘컴포넌트가 제공하는 기본 바인딩’에 나열된 Hilt의 사전 정의된 바인딩을 사용하도록 전환해야 한다.

컴포넌트가 빌더를 통한 모듈 인스턴스를 넘겨 받거나 @BindsInstance를 통해 어떤 인자를 갖는다면, 이를 다루기 위해 ‘컴포넌트 인자’ 섹션을 읽도록 하자. 이러한 것들을 다루면 @Component.Builder 인터페이스를 사용하지 않게 되므로 삭제가 가능하다.

e. 통합적인 모듈 및 인터페이스 정리하기

통합적인 모듈 또는 Entry point를 사용한 경우, 결국에는 해당 모듈 및 Entry point 클래스를 제거해야 한다. 포함 된 모든 모듈과 구현된 인터페이스에 개별적으로 통합 모듈에 사용된 동일한 @InstallIn 어노테이션을 추가하여 진행할 수 있다.

@InstallIn(ApplicationComponent::class)
@Module(includes = [FooModule::class, ...])
interface AggregatorModule {
}

// 위에 있는 목록에서 FooModule을 제거하고, 직접적으로 @InstallIn 어노테이션을 추가 하자.
@InstallIn(ApplicationComponent::class)
@Module
interface FooModule {
}

애플리케이션에 Hilt 추가하기

이제 ‘Quick Start’에 설명 한대로 @HiltAndroidApp을 Application 클래스에 추가 할 수 있다. 그 외에도, 컴포넌트의 인스턴스를 작성하거나 저장하는 데 관련된 코드가 비어 있어야 한다. @Component 클래스와 @ Component.Builder 클래스를 아직 삭제하지 않은 경우 삭제할 수 있다.

dagger.android의 Application

Application이 DaggerApplication으로 확장되거나 HasAndroidInjector를 구현하는 경우, 모든 dagger.android의 Activity / Fragment가 마이그레이션 될 때까지 이 코드를 유지해야 한다. 이는 마이그레이션의 마지막 단계 중 하나다. dagger.android의 이 부분은 의존성을 얻는 부분이 제대로 작동하는지 확인하기 위함이다 (예 : Activity가 자신에게 의존성을 주입하려고 할 때). 차이점은 이제 앞의 단계에서 제거된 컴포넌트 대신 Hilt ApplicationComponent로 만족한다는 것이다.

예를 들어, Hilt의 Activity와 dagger.android의 Activity를 모두 지원하는 마이그레이션 된 dagger.android의 Application은 다음과 같다.

@HiltAndroidApp
class MyApplication : HasAndroidInjector {
    @Inject 
    lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Object>

    override fun androidInjector() = dispatchingAndroidInjector
}

또는 DaggerApplication을 사용중인 경우 다음을 수행 할 수 있다. @EntryPoint 클래스는 Dagger 컴포넌트가 AndroidInjector<MyApplication>을 구현하도록 한다. 이것은 이전 Dagger 컴포넌트가 전에 수행했던 작업 일 수 있다.

@HiltAndroidApp
class MyApplication : DaggerApplication() {
    @EntryPoint
    @InstallIn(ApplicationComponent::class)
    interface ApplicationInjector : AndroidInjector<MyApplication>

    override fun applicationInjector(): AndroidInjector<MyApplication> {
        return EntryPoints.get(this, ApplicationInjector::class.java)
    }
}

다른 모든 dagger.android 사용법을 마이그레이션하고 이 코드를 제거 할 준비가 되었으면 단순히 Application을 확장하고 대체된 메소드 및 DispatchingAndroidInjector 클래스를 제거하도록 하자.

빌드 확인하기

이 시점에서 앱을 중지하고 빌드 / 실행 할 수 있는지 확인하자. 아마도 앱이 Hilt의 ApplicationComponent를 성공적으로 사용하고 있을 것이다.

2. Activity와 Fragment (그리고 다른 클래스들) 마이그레이션 하기

이제 Application이 Hilt를 지원하므로 Activity 마이그레이션을 시작한 다음 Fragment 마이그레이션을 진행 할 수 있다. 앱을 마이그레이션하는 동안 @AndroidEntryPoint Activity와 @AndroidEntryPoint를 사용하지 않는 Activity를 함께 사용하는 것은 괜찮다. Activity 내의 Fragment에 대해서도 마찬가지다. Hilt를 비-Hilt 코드와 혼합하는 유일한 제약조건은 상위 개념에 달려 있다. Hilt의 Activity는 Hilt의 Application에 포함되고, Hilt의 Fragment는 Hilt의 Activity에 포함되어야 한다. Fragment 마이그레이션을 진행하기 전에 모든 Activity에 대해 마이그레이션을 먼저 진행하는 것을 권장하지만, 문제가 있는 경우 선택적 주입(optional injection)으로 해당 제약조건을 완화 할 수 있다.

Activity와 Fragment를 마이그레이션 하는 것은 기술적 측면에서 Application 컴포넌트와 매우 유사하다. 현재 컴포넌트에서 모든 모듈을 가져와 @InstallIn 모듈과 함께 적절한 컴포넌트에 설치해야 한다. 마찬가지로 현재 컴포넌트의 확장 인터페이스를 모두 가져와 @InstallIn Entry point가 있는 적절한 컴포넌트에 설치하도록 한다. 자세한 내용은 위의 ‘컴포넌트 마이그레이션 하기’ 섹션을 다시 살펴보자. 또한 Activity 및 Fragment에 대해 고려해야 할 몇 가지 추가사항도 다음 나올 섹션에서 살펴보자.

Note : dagger.android의 @ContributesAndroidInjector를 사용하는 경우, ‘컴포넌트 마이그레이션 하기’ 섹션을 따르면 @ContributesAndroidInjector의 모듈이 마이그레이션을 해야 하는 모듈이다. @EntryPoint로 마이그레이션 할 인터페이스는 없다.

단일 컴포넌트의 차이점을 알자

Hilt의 설계 결정 중 하나는 모든 Activity에 단일 컴포넌트와 모든 Fragment에 단일 컴포넌트를 사용하는 것이다. 이 부분에 관심이 있다면 그 이유를 ‘단일 컴포넌트’편에서 자세히 확인할 수 있다. 이것이 중요한 이유는 dagger.android의 기본적인 설정과 같이 각 Activity에 대해 별도의 컴포넌트가 있는 경우 Hilt로 마이그레이션 할 때 컴포넌트를 단일 컴포넌트로 병합하기 때문이다. 코드베이스에 따라 문제가 발생할 수 있다.

흔하게 겪는 두가지 문제는 이렇다.

바인딩 충돌

두 Activity에서 동일한 바인딩 키를 다르게 정의한 경우에 발생한다. 병합되면 중복된 바인딩을 얻게 된다. 이는 Hilt의 전역적 바인딩 키 공간에 대한 제한 사항이며 단일 정의를 갖도록 해당 바인딩을 재정의해야 한다. 일반적으로 이는 나쁘지 않으며 주입된 Activity를 기반으로 논리를 수행하여 동작한다. 예제는 ‘컴포넌트 인자’ 섹션을 참조하자.

특정 Activity 타입에 의존 하는 것

병합된 컴포넌트로 인해 컴포넌트가 BarActivity (또는 다른 Activity)에 사용될 때 FooActivity 바인딩을 충족 할 수 없으므로 FooActivity 또는 BarActivity에 대한 바인딩이 더 이상 의미가 없는 경우가 많다. 일반적으로 코드는 꼭 실제 하위 타입의 Activity에 의존하지 않으며 Activity 또는 FragmentActivity와 같은 공통 하위 타입만 필요하다. 보다 일반적인 유형을 사용하려면 하위 타입을 사용하는 코드를 리팩토링해야 한다. Hilt에서 자동으로 제공하지 않는 공통 하위 타입이 필요한 경우 캐스팅하여 바인딩을 제공 할 수 있지만 (예 : ‘Component 인자’) 조심해야 한다.

일반적인 하위 타입 사용법을 교체하는 예제:

// 이 클래스는 FragmentManager를 얻기 위해서 Activity만을 사용하고 있는데,
// FooActivity 대신 FragmentActivity 클래스를 사용할 수도 있다.
class Foo @Inject constructor(private val activity: FooActivity) {
    fun doSomething() {
        activity.getSupportFragmentManager()...
    }
}

// Hilt용으로 Foo 클래스를 마이그레이션할 때는 FragmentActivity로 변경한다.
class Foo @Inject constructor(private val activity: FragmentActivity) {
    fun doSomething() {
        activity.getSupportFragmentManager()...
    }
}

Activity/Fragment에 Hilt 추가하기

이제 ‘Quick Start’ 가이드에 설명된 대로 Activity 또는 Fragment에 @AndroidEntryPoint 어노테이션을 추가 할 수 있다. 기본 클래스는 필드 주입을 수행하더라도 어노테이션을 추가 할 필요가 없다 (가장 하위 클래스로 직접 인스턴스화 되는 상황이 아닌 한).

@AndroidEntryPoint
class FooActivity : AppCompatActivity() {
    @Inject lateinit var foo: Foo
}
Note : Activity에 필드 주입이 필요하지 않더라도 @AndroidEntryPoint를 사용하는 Fragment가 첨부되어 있으면 @AndroidEntryPoint를 사용하도록 Activity를 마이그레이션해야 한다.

Dagger

이제 컴포넌트 초기화 코드 또는 주입 인터페이스가 있으면 제거 할 수 있다.

dagger.android

이 클래스에 @ContributesAndroidInjector를 사용중인 경우 지금 제거 할 수 있다. AndroidInjection / AndroidSupportInjection에 대한 호출이 있으면 그것도 제거 할 수있다. 클래스가 HasAndroidInjector를 구현하고 비-Hilt Fragment의 상위 클래스가 아닌 경우 해당 코드를 지금 제거 할 수 있다.

Activity 또는 Fragment가 DaggerAppCompatActivity, DaggerFragment 또는 이와 유사한 클래스에서 확장 된 경우, 이를 제거하고 비 Dagger 등가물 (예 : AppCompatActivity 또는 일반 Fragment)로 바꿔야한다. 여전히 dagger.android를 사용하는 하위 Fragment 또는 View가 있는 경우 DispatchingAndroidInjector를 주입하여 HasAndroidInjector를 구현해야 한다. (다음에 나올 예제 참조)

dagger.android에서 모든 하위 항목을 마이그레이션 한 후 나중에 다시 HasAndroidInjector 코드를 제거하자.

간단한 dagger.android 예제

다음 예제는 Activity를 마이그레이션 하면서 Hilt 및 dagger.android Fragment를 모두 지원할 수 있도록 한다.

초기 상태:

class MyActivity : DaggerAppCompatActivity() {
    @Inject lateinit var foo: Foo
}

@Module
interface MyActivityModule {
    // 스코프 어노테이션을 가지고 있다면, 스코프 별칭 섹션을 확인하자
    @ContributesAndroidInjector(modules = [ FooModule::class, ... ])
    fun bindMyActivity(): MyActivity
}

Hilt와 dagger.android Fragment를 모두 허용하는 중간 상태 :

@AndroidEntryPoint
class MyActivity : AppCompatActivity(), HasAndroidInjector {
    @Inject lateinit var foo: Foo

    // 모든 하위 항목이 마이그레이션이 되면 아래의 코드는 삭제한다.
    @Inject lateinit var androidInjector: DispatchAndroidInjector<Object>

    override fun androidInjector() = androidInjector
}

// 모듈의 목록이 매우 짧다면 이런 통합 모듈은 필요가 없다.
// 모듈의 includes 목록에서 FooModule과 같이 모든 모듈에 대해
// @InstallIn(ActivityComponent.class) 어노테이션을 추가하자.
@Module(includes = [ FooModule::class, ...])
@InstallIn(ActivityComponent::class)
interface MyActivityAggregatorModule

최종 상태:

@AndroidEntryPoint
class MyActivity : AppCompatActivity() {
    @Inject lateinit var foo: Foo
}

// 각 Activity 모듈은 @InstallIn(ActivityComponent::class)가 달려있다.

빌드 상태 확인하기

Activity 또는 Fragment를 마이그레이션 한 후 앱을 중지하고 빌드 / 실행할 수 있어야 한다. 각 클래스를 마이그레이션 한 후 올바른 길을 가고 있는지 확인하도록 하자.

3. 다른 안드로이드 컴포넌트

View, Service 및 BroadcastReceiver 타입은 위와 동일한 공식을 따라야하며, 지금 마이그레이션 할 준비가 되었다. 모든 것을 옮기면 끝이다.

이것만 기억하자:

  • 사용하고 있는 HasAndroidInjector 을 정리하자.
  • 남은 통합 모듈 또는 Entry point 인터페이스를 정리하자. 일반적으로 Hilt와 함께 @Module (includes =)을 사용할 필요가 없으므로 이 모듈을 제거하고 포함된 모듈에 @InstallIn 주석을 추가하면 된다.
  • 필요한 경우 이전 스코프 어노테이션을 스코프 별칭으로 마이그레이션 하자
  • 컴포넌트 인자 바인딩을 일치시키기 위해 배치해야 하는 @Binds 어노테이션을 마이그레이션 하자.

무엇을 할까?

한정자

프로젝트에서 사용하고 있는 한정자는 여전히 유효하고, 그것들은 Dagger에서 사용하던 방식과 동일하게 Hilt에서 사용된다.

앱에서 서로 다른 Context를 구별하기 위해 자체 @ApplicationContext 및 @ActivityContext 한정자가 있는 경우 @Binds를 추가하여 함께 매핑한 다음 남는 시간에 Hilt 한정자로 바꿀 수 있다.

@InstallIn(ApplicationComponent::class)
@Module
interface ApplicationContextModule {
    @Binds
    @my.app.ApplicationContextfun bindAppContext(
        @dagger.hilt.android.qualifiers.ApplicationContext context: Context
    ):Context
}

컴포넌트 인자

Hilt를 사용할 때는 컴포넌트를 인스턴스화하는 코드가 숨겨져 있으므로 모듈 인스턴스 또는 @BindsInstance 호출을 사용하여 자체 컴포넌트 인자를 추가 할 수 없다. 컴포넌트에 이러한 코드가 있으면 코드를 리팩토링하여 사용하지 않도록 한다. Hilt는 각 컴포넌트에 기본 바인딩 세트가 있으며 ‘컴포넌트가 제공하는 기본 바인딩’에서 볼 수 있다. 컴포넌트 인자가 무엇인지에 따라 일부 기본 바인딩에 의존하도록 할 수 있다. 때로는 약간의 재설계가 필요하지만 대부분의 경우 다음 전략을 사용하여 이 방법으로 해결할 수 있다. 그렇지 않은 경우 사용자화 컴포넌트 사용을 고려해야 한다.

예를 들어, 가장 간단한 경우는 바인딩을 전혀 전달할 필요가없고 일반적인 정적 @Provides 메소드인 경우다. 또 다른 간단한 경우는 인자가 사용자화 BaseFragment 타입과 같은 기본 바인딩의 변형 일 수 있다. Hilt는 모든 Fragments가 BaseFragment의 인스턴스가 될 것임을 알 수 없으므로 BaseFragment로 바인딩 된 실제 타입이 필요한 경우 캐스팅하여 진행한다.

@Component.Builderinterface Builder {
    @BindsInstance
    fun fragment(fragment: BaseFragment): Builder
}

@InstallIn(FragmentComponent::class)
@Module
object BaseFragmentModule {
    @Provides
    fun provideBaseFragment(fragment: Fragment) : BaseFragment {
        return fragment as BaseFragment
    }
}

다른 경우에는 인자가 Activity의 Intent와 같은 기본 바인딩 중 하나에 대한 것 일 수 있다.

@Component.Builderinterface Builder {
    @BindsInstance
    fun intent(intent: Intent): Builder
}

@InstallIn(ActivityComponent::class)
@Module
object IntentModule {
    @Provides
    fun provideIntent(activity: Activity) : Intent {
        return activity.getIntent()
    }
}

마지막으로, 다른 Activity 또는 Fragment 컴포넌트에 대해 다르게 구성된 경우 일부를 다시 설계해야 할 수도 있다. 예를 들어, Activity에 새 인터페이스를 사용하여 오브젝트를 제공 할 수 있다.

@Component.Builderinterface Builder {
    @BindsInstance
    fun foo(foo: Foo): Builder  // Activity별로 Foo는 다르다
}

// Activity가 구현하여 사용자화 Foo를 제공하는 인터페이스를 정의한다.
interface HasFoo {
    fun getFoo() : Foo
}

@InstallIn(ActivityComponent::class)
@Module
object FooModule {
    @Provides
    fun provideFoo(activity: Activity) : Foo? {
        if (activity is HasFoo) {
            return activity.getFoo()
        }
        return null
    }
}

사용자화 컴포넌트

Hilt 컴포넌트에 매핑되지 않은 다른 컴포넌트가 있는 경우, Hilt 컴포넌트로 단순화 될 수 있는지 먼저 고려해야 한다. 그렇지 않은 경우 컴포넌트를 수동 Dagger 컴포넌트로 유지할 수 있다. 컴포넌트 의존성 또는 서브컴포넌트를 사용하려면 아래 섹션을 살펴보자.

컴포넌트 의존성

컴포넌트 의존성 @EntryPoint로 연결될 수 있다.

예를 들어 ApplicationComponent에서 컴포넌트 의존성이 없는 경우 필요한 메소드를 @EntryPoint로 어노테이션이 달린 인터페이스로 분리하여 작동을 유지할 수 있다.

// 컴포넌트 의존성과 함께 시작하면
@Component
interface MyApplicationComponent {
    // MyCustomComponent에서 이 바인딩들이 노출 된다
    fun getFoo(): Foo
    fun getBar(): Bar
    fun getBaz(): Baz
    ...
}

@Component(dependencies = [MyApplicationComponent::class])
interface MyCustomComponent {
    @Component.Builder
    interface Builder {
        fun appComponent(appComponent: MyApplicationComponent): Builder

        fun build(): MyCustomComponent
    }
}

// 다음 클래스들과 함께 Hilt로 마이그레이션 될 수 있다.
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface CustomComponentDependencies {
    fun getFoo(): Foo
    fun getBar(): Bar
    fun getBaz(): Baz
    ...
}

@Component(dependencies = [CustomComponentDependencies::class])
interface MyCustomComponent {
    @Component.Builderinterface Builder {
        fun appComponentDeps(deps: CustomComponentDependencies): Builder
        fun build(): MyCustomComponent
    }
}

사용자화 컴포넌트를 빌드 할 때 EntryPoint를 사용하여 CustomComponentDependencies의 인스턴스를 얻을 수 있다.

DaggerMyCustomComponent.builder()
    .appComponentDeps(
        EntryPoints.get(
            applicationContext,
            CustomComponentDependencies::class.java))
    .build()

서브 컴포넌트

서브 컴포넌트는 Dagger에서 주입 가능한 서브 컴포넌트 빌더를 사용하여 일반 서브 컴포넌트를 설치하는 것과 같은 방식으로 Hilt 컴포넌트의 하위에 추가 할 수 있다. 상위의 적절한 @InstallIn을 사용하여 서브 컴포넌트를 모듈에 설치하자.

예를들어 ApplicationComponent의 하위 항목인 FooSubcomponent를 가지고 있다면, 다음과 같이 설정 할 수 있다.

@InstallIn(ApplicationComponent::class)
@Module(subcomponents = FooSubcomponent::class)
interface FooModule {}

Hilt 컴포넌트에 매핑되는 컴포넌트를 위한 컴포넌트 의존성

현재 컴포넌트 의존성을 사용하고 컴포넌트가 Hilt 컴포넌트에 상대적으로 잘 매핑되어 있으면 마이그레이션 할 때 컴포넌트 의존성과 서브 컴포넌트간의 차이점을 염두에 두어야 한다. Hilt가 서브 컴포넌트를 사용하기로 선택한 몇 가지 이유를 설명하는 ‘서브 컴포넌트 vs 컴포넌트 의존성’ 섹션을 확인하자.

알아야 할 주요 차이점은 바인딩이 상위 항목으로부터 자동으로 상속된다는 점이다. 이는 바인딩을 노출시키기 위한 추가 메서드를 제거하고, 상위 및 하위 컴포넌트에서 정의된 바인딩에 대해 발생할 수 있는 중복 바인딩을 처리 할 가능성이 있음을 의미한다. 바인딩을 노출시키는 추가 메서드를 제거하는 것은 기술적으로 빌드를 깨지 않기 때문에 선택 사항이지만, 죽은 코드들을 정리 할 수 있으므로 제거하는 것을 권장한다. ‘b. 확장한 인터페이스 또는 모듈 다루기’ 섹션에서 설명한대로 안전하게 마이그레이션 할 수 있다.

다음은 노출된 바인딩의 예제다.

@Component
interface MySingletonComponent {
    // 이런 바인딩들은 컴포넌트 의존성을 위해 노출 될 수 있다.
    // 이 바인딩들을 제거하는 것을 고려해보자.
    fun getFoo(): Foo
    fun getBar(): Bar
    fun getBaz(): Baz
    ...
}

앞의 단계를 따라 컴포넌트를 마이그레이션 할 때, 컴포넌트에 상위 Hilt와 동등한 컴포넌트에 대한 의존성이 있는 경우 나머지 컴포넌트들을 제거하므로 해당 의존성을 제거하자.

// 나머지 컴포넌트들을 마이그레이션 가이드에 따라 마이그레이션 하므로 
// 이 의존성들을 제거하자
@Component(dependencies = [MySingletonComponent::class])
interface MyActivityComponent {
    ...
}
카테고리: Dagger2

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.