본문 바로가기

안드로이드

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

개요

해당 게시글은 구글 코드랩 문서의 번역 게시글입니다.

 

Scoping Subcomponents

RegistrationViewModel을 Activity와 Fragment 사이에서 공유하기 위해 subcomponent를 생성했다. 이전에 작업한 것 처럼 Component와 클래스에 @Singleton 등 으로 같은 Scope를 지정하면 해당 타입은 Component에서 고유한 인스턴스가 된다.

 

하지만 AppComponent에서 @Singleton을 이미 사용하고 있기 때문에 @Singleton을 사용할 수 없는 상황이다. 

 

이 경우에 새로운 scope를 @RegistratinoScope라고 부를 수 있지만 이는 적절하지 않다. Scope 명명 기법은 그것이 수행하는 목적을 나타내서는 안된다. Annotation은 다른 컴포넌트에 의해 재사용될 수 있기 때문이다. 따라서, 수명에 따라 이름을 짓는게 가장 적절하다. 그 이유로 @ActivityScope라고 scope를 네이밍한다.

 

Scoping rules

  • 한 타입이 Scope annotation으로 지정되면 동일한 scope으로 지정된 Component에서만 사용할 수 있다.
  • Component가 특정 scope annotation으로 지정되면 해당 annotation이 있는 타입 또는 annotation이 없는 타입만 제공할 수 있다.
  • subcomponent는 상위 component가 사용하는 scope annotation을 사용할 수 없다.

 

di/ActivityScope.kt

@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class ActivityScope

RegistrationViewModel을 RegistrationComponent의 범위로 지정하기 위해 각각에게 @ActivityScope를 지정해준다.

 

RegistrationViewModel.kt

// 이 ViewModel을 scope을 @ActivityScope을 사용하는 컴포넌트로 지정한다.
@ActivityScope
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {
    ...
}

RegistrationComponent.kt

// @ActivityScope로 처리된 클래스는 이 컴포넌트 내에서 고유한 객체로 존재한다
@ActivityScope
@Subcomponent
interface RegistrationComponent { ... }

이제 RegistrationComponent는 항상 같은 RegistrationViewModel을 제공한다.

 

 

Subcomponent lifecycle

AppComponent는 앱이 메모리에 상주하는 동안 같은 그래프 객체를 제공하기 위해 앱의 생명주기에 붙어있다. Registration Component는 어떨까? 해당 Component가 필요한 이유 중 하나는 RegistrationViewModel을 Registration 관련 View 클래스에서 공유하기 위함이다. 또한 새로운 Registration Flow가 생성될 경우 새로운 RegistrationViewModel 객체를 생성하기 위함도 있다.

 

그럼으로 RegistrationActivity가 RegistrationComponent의 생명주기이다. 새 액티비티가 생성될 때 마다  RegistrationComponent 객체를 사용할 수 있는 새로운 RegistrationComponent와 프래그먼트를 만든다.

 

RegistrationActivity.kt

class RegistrationActivity : AppCompatActivity() {

    //RegistrationComponent 객체를 저장하고 프래그먼트들이 접근 가능하게 한다.
    lateinit var registrationComponent: RegistrationComponent
    ...
}

onCreate 내에서 super.onCreate()이 호출되기 전에 RegistrationComponent 객체를 생성한다. 그리고 기존 appComponent에 주입하는 대신 RegistrationComponent에 주입한다.

 

class RegistrationActivity : AppCompatActivity() {
    ...

    override fun onCreate(savedInstanceState: Bundle?) {

        // Remove lines 
        (application as MyApplication).appComponent.inject(this)

        // Add these lines

        // Creates an instance of Registration component by grabbing the factory from the app graph
        registrationComponent = (application as MyApplication).appComponent.registrationComponent().create() 
        // Injects this activity to the just created registration component
        registrationComponent.inject(this)

        super.onCreate(savedInstanceState)
        ...
    }
    ...
}
주의할 점으로 registrationComponent는 @Inject로 처리할 필요가 없다. 해당 변수가 대거에 의해 제공되는게 아닌 직접 생성하는 것이기 때문이다.

 

이를 통해 RegistrationComponent가 RegistrationActivity에서 이용가능하고 해당 객체를 fragment에서 주입하는데 사용할 수 있다.

 

EnterDetailsFragment.kt

class EnterDetailsFragment : Fragment() {
    ...
    override fun onAttach(context: Context) {
        super.onAttach(context)

        (activity as RegistrationActivity).registrationComponent.inject(this)
    }
    ...
}

TermsAndConditionsFragment.kt

class TermsAndConditionsFragment : Fragment() {
    ...
    override fun onAttach(context: Context) {
        super.onAttach(context)

        (activity as RegistrationActivity).registrationComponent.inject(this)
    }
}

 

이제 앱을 다시 실행시키면 이전과 같이 새로 Registration Flow를 시작할 경우 예상되로 작동하는 것을 확인할 수 있다. 추가로 Settings의 경우 아직 Dagger로 리팩토링하지 않았기 때문에 정상적으로 동작하지 않는다.

 

현재 다이어그램 상태

이전과 다른 점은 RegistrationViewModel이 RegistrationComponent의 scope로 지정되었다. 이를 주황색 점으로 표시하였다. (참고로 EnterDetailsVM은 오직 EnterDetailsFragment에서만 사용되며 항상 같은 객체임을 보장받을 필요가 없기 때문에 Scoping 처리가 없음)