오늘도 더 나은 코드를 작성하였습니까?

Android Code Lab(Dagger2) 정리4 본문

DI/Dagger2

Android Code Lab(Dagger2) 정리4

hik14 2021. 12. 21. 17:23

Registration Fragment는 여전히 ​​수동 종속성 주입을 사용하고 있으므로 지금 마이그레이션해 보자.

 

EnterDetailsFragment와TermAndConditionsFragment가 모두 Dagger에 의해 주입되기를 원하기 때문에

AppComponent 인터페이스에 추가하여 Dagger에 알려야 합니다.

 

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)
}

 

Dagger로 부터 주입받기를 원하는 field가 무엇인지 확인해야된다.

 

EnterDetailsFragment- ViewModel

 

@Inject로 필드에 주석을 달고 private 가시성 수정자를 제거한다.

또한 기존의 수동으로 주입하던 코드들을 삭제 한다. 

 

EnterDetailsFragment.kt

class EnterDetailsFragment : Fragment() {
    @Inject
    lateinit var registrationViewModel: RegistrationViewModel
    
    @Inject
    lateinit var enterDetailsViewModel: EnterDetailsViewModel
    ...
}

 

Application 클래스의 appComponent 인스턴스를 사용하여 Fragments를 주입할 수 있습니다.

Fragment의 경우 super.onAttach를 호출한 후 onAttach 메소드를 사용하여 컴포넌트를 주입합니다.

 

*  Activity에 Dagger를 통한 주입을 할경우 super.onCreate()가 호출되기 이전

    Fragment에 Dagger를 통한 주입을 할경우super.onAttach()가 호출된 후에 한다.

 

Dagger가 알아야 할 남은 것은 EnterDetailsViewModel의 인스턴스를 제공하는 방법입니다.

 

*EnterDetailsViewModel 의 경우 어떤 종속성도 가지고 있지 않기 때문에 @Inject 주석만 달면된다.

RegistrationViewModel에는 이미 @Inject로 주석이 달렸고, RegistrationActivity는 이전에 이를 필요로 했습니다.

 

동일한 작업을 TermsAndConditionsFragment에도 한다.

*registrationViewModel은 생성 방법을 Dagger가 알고있다.

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? {
         ...
         // Remove following line
         registrationViewModel = (activity as RegistrationActivity).registrationViewModel
         ...
    }

무슨 일이에요? 앱 실행후 Registration을 완료하면 에러가 터질것이다.

 

문제는 RegistrationViewModel의 서로 다른 객체가 RegistrationActivity, EnterDetailsFragment 및TermAndConditionsFragment에 주입되고 있다는 것입니다

 

Activity 및 Fragments에 대해 동일한 인스턴스(RegistrationViewModel)가 주입되어야 한다.

 

RegistrationViewModel에 @Singleton으로 주석을 추가하면 어떻게 될까요?

그것은 지금 문제를 해결하지만 미래에 문제를 일으킬 것입니다.

 

- Registration 흐름이 완료된 후 항상 RegistrationViewModel의 인스턴스가 메모리에 있는 것을 원하지 않습니다.

다양한 Registration 흐름에 대해 서로 다른 RegistrationViewModel 객체가 필요합니다.

- 사용자가 등록 및 등록 취소하는 경우 이전 등록의 데이터가 존재하지 않기를 바랍니다.

 

Registration Fragments가 Activity에서 주입된 동일한 ViewModel을 재사용하기를 원하지만,

Activity가 변경되면(등록을 제거후 재등록) 다른 인스턴스를 주입해야된다.

 

 

즉, RegistrationViewModel의 범위를 RegistrationActivity로 제한해야한다.

 

Registration 흐름을 위한 새 Component를 생성하고 ViewModel의 scope를 새 등록 Component로 지정해야된다.

이를 달성하기 위해 Dagger 하위 구성요소를 사용합니다.

Dagger Subcomponents

RegistrationComponent는 AppComponent의 객체들에 접근 할수 있어한다. RegistrationViewModel이 UserRepository에 종속성을 가지고 있기 때문이다. New Component가 다른 Component의 일부를 사용하기를 원한다는 것을 Dagger에 알리는 방법은 Dagger Subcomponent를 사용하는 것입니다.

 

즉, New Component(예: RegistrationComponent)는 공유 리소스를 포함하는 Component(예: AppComponent) Subcomponent 여야 합니다.

 

Subcomponent

-  SuperComponent 의 객체 그래프를 상속 및 확장한다.

SuperComponent 에 제공된 모든 객체는 Subcomponent에도 제공

Subcomponent의 객체는 SuperComponent에 제공하는 객체에 종속될 수 있다

 

RegistrationComponent는 Registration과 관련된 구체적인 정보를 포함하고 있어야한다.

 

1. 등록과 관련된 AppComponent의 주입 메서드를 추가(RegistrationActivity, EnterDetailsFragment, TermsAndConditionsFragment)

 

2.Subcomponent의 인스턴스를 만드는 데 사용할 수 있는 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관련 View(activity,fragment) 클래스를 삽입할 수 있는 메서드를 제거한다.

왜냐하면 이러한 클래스는 더 이상 사용되지 않고 해당 클래스는 RegistrationComponent를 사용하기 때문입니다.

 

RegistrationActivity가 RegistrationComponent의 인스턴스를 생성하려면, AppComponent 인터페이스에서 해당 Factory를 노출해야 합니다.

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

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

    // Expose RegistrationComponent factory from the graph
    // 반환 유형으로 해당 클래스가 있는 함수를 선언하여 RegistrationComponent 팩토리를 노출합니다
    fun registrationComponent(): RegistrationComponent.Factory

    fun inject(activity: MainActivity)
}

 

***

Dagger 그래프와 상호 작용하는 두 가지 방법

 

1. Unit을 반환하고 클래스를 매개변수로 사용하는 Method를 선언하면 해당 클래스에 필드 주입이 가능합니다

(예: fun inject(activity: MainActivity))

 

2. Type을 반환하는 Method를 선언하면 그래프에서 그 Type을 검색할 수 있습니다

(예: fun registrationComponent(): RegistrationComponent.Factory)

 

AppComponent에 RegistrationComponent가  자신의 Subcomponent 라는 것을 알게 하여 그에 대한 코드를 생성할 수 있도록 해야 합니다. Dagger 모듈을 생성한다.

// This module tells AppComponent which are its subcomponents
@Module(subcomponents = [RegistrationComponent::class])
class AppSubcomponents
@Singleton
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent { ... }

현재 Application Graph

Registration 관련된 View class(RegistrationActivity, EnterDetailsFragment, TermsAndConditionsFragment)는 이제 RegistrationComponent 에서 주입된다.

 

RegistrationViewModel 및 EnterDetailsViewModel는 RegistrationComponent를 사용하는 클래스에서만 요청된다.

2개의 ViewModel은 더이상 AppComponent가 아닌 RegistrationComponent의 구성 요소이다. 

 

Scoping Subcomponents

 

RegistrationActivity와 EnterDetailsFragment, TermsAndConditionsFragment간에 동일한 RegistrationViewModel 객체를 공유해야 하기 때문에 SubComponent 를 만들었다

 

Component와 클래스에 동일한 범위 주석으로 주석을 추가하면 해당 유형이 Component에서 고유한 인스턴스를 갖게 됩니다.

그러나 AppComponent에서 @Singleton 범위주석을 사용하고 있기 때문에 @Singleton 주석을 사용할 수 없습니다.

다른 것을 만들어야 합니다.

 

이 Scope를 @RegistrationScope라고 부를 수 있지만 이는 좋은 방법이 아닙니다. Scope 주석의 이름은 그것이 달성하는 목적에 대해 명시해서는 안됩니다. 

 

주석은 sibling component에서 (예: LoginComponent, SettingsComponent 등)에서 재사용할 수 있으므로 수명에 따라 이름을 지정해야 합니다.

 

 

Scoping rules:

 

주입 객체의 type이 Scope 주석으로 표시되면 동일한 Scope 주석이 지정된 Component에서만 사용할 수 있습니다.

ComponentScope 주석으로 표시되면 해당 Scope주석이 있는 유형 또는 Scope주석이 없는 유형만 제공할 수 있습니다.

-  SubComponent는 SuperComponent 중 하나에서 사용하는 Scope 주석을 사용할 수 없습니다.

 

 

ActivityScope.kt

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

RegistrationViewModel.kt

// Scopes this ViewModel to components that use @ActivityScope
@ActivityScope
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {
    ...
}

RegistrationComponent.kt

// Classes annotated with @ActivityScope will have a unique instance in this Component
@ActivityScope
@Subcomponent
interface RegistrationComponent { ... }

 

RegistrationComponent는 항상 동일한 RegistrationViewModel 인스턴스를 제공합니다.

Subcomponents lifecycle

App이 메모리에 있는 한 동일한 그래프 인스턴스를 사용하기를 원하기 때문에 AppComponent가 App의 수명 주기에 연결하였다.

RegistrationComponent의 수명 주기는 어떻게 됩니까?

 

Subcomponent가 필요한 이유 중 하나는 RegistrationActivity와 Fragments 간에 RegistrationViewModel의 동일한 인스턴스를 공유해야 했기 때문이다. 또한 새로운 Registration 흐름가 생성될때 마다  RegistrationViewModel의 새로운 인스턴스를 원합니다.

 

RegistrationActivity의 lifetime이  RegistrationComponent에게 올바른 lifetime입니다

 

새로운 RegistrationActivity를 생성할 때마다 RegistrationComponent의 해당 인스턴스를 사용할 수 있는 새

RegistrationComponent 및 Fragments 객체를 생성합니다.

 

RegistrationComponent는 RegistrationActivity 수명 주기에 연결되므로 Application 클래스에서 appComponent에 대한 참조를 유지한 것과 같은 방식으로 RegistrationActivity는 RegistrationComponent에 대한 참조를 유지해야 합니다.

이렇게 하면  Fragments에서도 RegistrationComponent에 액세스할 수 있습니다

class RegistrationActivity : AppCompatActivity() {

    // Stores an instance of RegistrationComponent so that its Fragments can access it
    lateinit var registrationComponent: RegistrationComponent
    ...
}

super.onCreate를 호출하기 전에 onCreate 메소드에서 RegistrationComponent의 새 인스턴스를 만들고 appComponent에 RegistrationActivity에 주입하는 대신 registrationComponent를 주입한다.

 

1. appComponent의 RegistrationActivty factory메소드를 통해 객체 생성.

2. registrationComponent 변수에 참조를 저장한다.

3. inject() 메소드를 통해 주입을 요청한다. 

 

class RegistrationActivity : AppCompatActivity() {
    ...
    lateinit var registrationComponent: RegistrationComponent

    override fun onCreate(savedInstanceState: Bundle?) {

        registrationComponent = (application as MyApplication).appComponent.registrationComponent().create()
        registrationComponent.inject(this)

        super.onCreate(savedInstanceState)
        ...
    }
    ...
}

* registrationComponent 변수는 Dagger에서 해당 변수를 제공하는 것이 아니기 때문에  @Inject로 주석 처리하지 않는다.

 

registrationComponent는 RegistrationActivity에서 사용할 수 있으며 하위 프래그먼트들 또한 주입을 요청할 수 있습니다.

RegistrationActivity의 registrationComponent를 사용하도록 Fragments의 onAttach 메소드를 교체한다.

 

EnterDetailsFragment.kt

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

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

TermsAndConditionsFragmet.kt

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

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

 

 RegistrationViewModel이 RegistrationComponent로 Scope가 지정되고, 이를 RegistrationViewModel에서 주황색 점으로 표기. 

'DI > Dagger2' 카테고리의 다른 글

Android Code Lab(Dagger2) 정리6  (0) 2021.12.22
Android Code Lab(Dagger2) 정리5  (0) 2021.12.22
Android Code Lab(Dagger2) 정리3  (0) 2021.12.21
Android Code Lab(Dagger2) 정리2  (0) 2021.12.21
Android Code Lab(Dagger2) 정리1  (0) 2021.12.20