본문 바로가기

안드로이드

[Android] 7. 구글 코드랩 Dagger를 이용한 리팩토링 - Subcomponents

개요

해당 게시글은 구글 코드랩을 번역한 게시글입니다.

 

Subcomponents

Registraion Flow

아직 Registration 관련 프래그먼트는 리팩토링이 되지 않았다. 관련된 프래그먼트는 EnterDetailsFragmentTermsAndConditionsFragment이다. 해당 프래그먼트들은 대거에 의해 의존성 주입을 받아야한다.

 

AppComponent.kt

@Singleton
@Component(modules = [StorageModule::class])
interface AppComponent {
    ...
    fun inject(activity: RegistrationActivity)
    fun inject(fragment: EnterDetailsFragment)
    fun inject(fragment: TermsAndConditionsFragment)
    fun inject(activity: MainActivity)
}

 

EnterDetailsFragment에서는 어떤 필드들의 주입이 필요하는 지 확인해보면, RegistrationViewModel, EnterDetailsViewModel이 주입을 필요로 한다.

 

EnterDetailsFragment.kt

class EnterDetailsFragment : Fragment() {

    @Inject
    lateinit var registrationViewModel: RegistrationViewModel
    
    @Inject
    lateinit var enterDetailsViewModel: EnterDetailsViewModel

    ...
}

또한 기존 초기화 코드를 삭제하는 과정이 필요하다.

class EnterDetailsFragment : Fragment() {

    override fun onCreateView(...): View? {
        ...
        // 아래 두개 라인을 삭제하세요
        registrationViewModel = (activity as RegistrationActivity).registrationViewModel
        enterDetailsViewModel = EnterDetailsViewModel()

        ...
    }
}

 

이제 Application 클래스의 AppComponet 객체를 통해 의존성을 주입받을 수 있다. 프래그먼트에서는 onAttach에서 super.onAttach()가 호출된 이후에 주입하는게 적절하다.

class EnterDetailsFragment : Fragment() {

    override fun onAttach(context: Context) {
        super.onAttach(context)

        (requireActivity().application as MyApplication).appComponent.inject(this)
    }
}

사실 이는 Dagger Android를 활용한다면 알아서 처리해주게 된다.

이제 남은 작업은 EnterDetailsViewModel의 생성자에 @Inject 처리를 해주는 일만 남았다. RegistraionViewModel은 이미 처리했기 때문이다.

 

EnterDetailsViewModel.kt

class EnterDetailsViewModel @Inject constructor() { ... }

EnterDetailsFragment는 이제 작업이 끝났고, TermsAndConditionsFragment에 유사한 작업을 진행해주면 된다.

 

TermsAndConditionsFragment.kt

class TermsAndConditionsFragment : Fragment() {

    @Inject
    lateinit var registrationViewModel: RegistrationViewModel

    override fun onAttach(context: Context) {
        super.onAttach(context)

        (requireActivity().application as MyApplication).appComponent.inject(this)
    }

    override fun onCreateView(...): View? {
         ...
         // 아래 라인을 제거하세요
         registrationViewModel = (activity as RegistrationActivity).registrationViewModel
         ...
    }
}

 

이제 앱을 실행시켜보자. 하지만 에러가 발생한다. 그 이유는 RegistrationViewModel이 각각 새로 생성되어 RegistrationActivity, EnterDetailsFragment, TermsAndConditionsFragment에 주입되었기 때문이다. 하지만 우리가 원하는 것은 위 세 가지 뷰에 같은 ViewModel이 주입되기를 원하는 상황이다.

 

만약 RegistrationViewModel에 @Singleton 처리를 하면 어떨까? 이는 당장은 해결할 수 있지만 향후 문제가 될 가능성이 농후하다.

  • RegistrationViewModel이 등록 절차가 끝난 이후에도 계속 메모리에 상주하게 되는건 원치 않는다.
  • 서로 다른 등록 흐름에 대해 서로 다른 RegistrationViewModel 인스턴스가 생성되어야 하며, 사용자가 등록/등록 취소를 하는 경우 이전 등록 데이터가 존재하지 않기를 원한다.

Registration 관련 프래그먼트들이 액티비티에서 주입된 같은 ViewModel을 재사용하지만 액티비티가 변경되면 서로 다른 객체가 필요한 상황이다. 따라서 RegistraionViewModel을 RegistraionActivity로 Scope를 지정할 필요가 있다. 그러기 위해서는 Registraion Flow에 대한 새로운 Component를 만들고 해당 ViewModel을 Registraion Component로 Scope를 지정할 수 있다. 이를 구현하기 위해 Dagger Subcomponent를 사용한다.

 

Dagger Subcomponent

RegistraionComponent는 UserRepository에 의존하기 때문에 AppComponent에서 객체에 접근할 수 있어야 한다. 대거에게 새 Component가 다른 Component의 일부를 사용하도록 하는 방법을 Dagger Subcomponent라고 한다. 새로운 Component인 RegistraionComponent는 AppComponent의 Subcomponent여야 한다.

 

Subcomponents는 상위 Component의 그래프를 상속하는 Component이다. 따라서, 상위 Component의 객체는 Subcomponent에서도 모두 제공된다. 이러한 방식으로 Subcomponet는 상위 Component에 의존할 수 있다.

 

registration 패키지에 RegistrationComponent.kt를 새로 생성한다. 그리고 여기에 RegistrationComponent 인터페이스를 새로 만들고 @Subcomponent로 처리한다.

 

registration/RegistrationComponent.kt

// Dagger Subcomponent로 정의
@Subcomponent
interface RegistrationComponent {

}

이 컴포넌트는 registration과 관련된 정보를 포함하고 있어야 한다.

  • Inject 메소드들을 AppComponent에서 RegistrationComponent로 이동시킨다.
  • 해당 Component를 초기화할 수 있는 subcomponent Factory를 생성한다.
@Subcomponent
interface RegistrationComponent {

    // Factory to create instances of RegistrationComponent
    @Subcomponent.Factory
    interface Factory {
        fun create(): RegistrationComponent
    }

    // Classes that can be injected by this Component
    fun inject(activity: RegistrationActivity)
    fun inject(fragment: EnterDetailsFragment)
    fun inject(fragment: TermsAndConditionsFragment)
}

 

AppComponent에서는 registration 관련된 메소드를 지우고, RegistrationActivity가 RegistrationComponent를 생성하도록 하기 위해 Factory를 노출해야 한다.

@Singleton
@Component(modules = [StorageModule::class])
interface AppComponent {

    @Component.Factory
    interface Factory {
        fun create(@BindsInstance context: Context): AppComponent
    }

    // RegistrationComponent factory를 그래프에서 노출
    fun registrationComponent(): RegistrationComponent.Factory

    fun inject(activity: MainActivity)
}

 

Dagger Graph와 상호작용하는 두 가지 방법

  • Unit을 반환하는 메소드를 선언하고 의존성 주입이 필요한 클래스를 매개변수로 처리하여 필드 인젝션을 가능하게 한다. (e.g. fun inject(activity: MainActivity)
  • 특정 타입을 반환하는 메소드를 선언하여 그래프에서 타입을 검색하도록 한다 (e.g. fun registrationComponent() : Registrationomponent.Factory).

 

이제 AppComponent가 RegistrationComponent가 Subcomponent임을 알 수 있게 해야하는 코드가 필요하다. 그러기 위해서는 Dagger Module이 필요하다.

 

AppSubcomponents.kt를 di 패키지 내부에 생성한다. 이 파일에서는 @Module로 처리된 AppSubcomponents 클래스를 정의한다. subcomponent 인터페이스들(i.g. RegistrationComponent)에 대한 정보를 알게 하기 위해 subcomponent 인터페이스들을 목록에 추가할 수 있다.

 

di/AppSubcomponents.kt

// 이 모듈은 AppComponent에게 subcomponents가 무엇인지 알려준다.
@Module(subcomponents = [RegistrationComponent::class])
class AppSubcomponents

해당 모듈은 역시 AppComponent에 추가할 필요가 있다.

 

AppComponent.kt

@Singleton
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent { ... }

AppComponent는 이제 RegistrationComponent가 subcomponent임을 알게 된다.

Registration과 관련된 View 클래스들은 RegistrationComponent에 의해 주입된다. RegistrationViewModel, EnterDetailsViewModel은 RegistrationComponent를 사용하는 클래스에서만 요청되므로 AppComponent의 일부가 아닌 RegistrationComponent의 일부이다.