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

Android Code Lab(Dagger2) 정리6 본문

DI/Dagger2

Android Code Lab(Dagger2) 정리6

hik14 2021. 12. 22. 17:18

Dagger에 @Inject로 생성자에 주석을 추가하여 SettingsActivity 종속성(예: SettingsViewModel)의 인스턴스를 생성하는 방법을 알려줍니다.

 

SettingsViewModel.kt

class SettingsViewModel @Inject constructor(
    private val userDataRepository: UserDataRepository,
    private val userManager: UserManager
) { ... }

AppComponent 인터페이스에서 SettingsActivity를 매개변수로 사용하는 함수를 추가하여 Dagger에서 SettingsActivity를 주입.

 

AppComponent.kt

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

 

SettingsActivity.kt

class SettingsActivity : AppCompatActivity() {

    @Inject
    lateinit var settingsViewModel: SettingsViewModel

    override fun onCreate(savedInstanceState: Bundle?) {

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

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)
        setupViews()
    }
}

이대로 App을 실행하면 Settomg에서 Notification refresh 기능이 동작하지 않는 것을 확인할 수 있습니다.

MainActivity와 SettingsActivity에서 동일한 UserDataRepository 인스턴스를 재사용하지 않기 때문이다.

 

@Singleton으로 주석을 달아 UserDataRepository의 범위를 AppComponent로 지정하면 해결되나?

 

사용자가 Logout 또는  Registration 을 취소하면 동일한 UserDataRepository 인스턴스를 메모리에 유지하면  않기 때문에 하면 안된다. 해당 데이터는 로그인한 사용자에게만 보여줘야한다.

 

사용자가 로그인되어 있는 동안 지속되는 Component를 만들 필요가 있다!

사용자가 로그인한 후 액세스할 수 있는 모든 Activity는 이 Component에 의해 주입된다.

 

UserComponent.kt 

// Definition of a Dagger subcomponent
@Subcomponent
interface UserComponent {

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

    // Classes that can be injected by this Component
    fun inject(activity: MainActivity)
    fun inject(activity: SettingsActivity)
}

AppSubcomponents.kt

@Module(subcomponents = [RegistrationComponent::class, LoginComponent::class, UserComponent::class])
class AppSubcomponents

 

UserComponent의 Lifetime을 담당하는 것은 무엇입니까?

LoginComponent 및 RegistrationComponent는 각각의 Activity의 수명에 연결 되지만, UserComponent는 둘 이상의Activity을 주입할 수 있으며 Activity의 수가 앞으로도 증가할 수 있다.

 

ComponentLifetime을 사용자가 login 및 logout할 때를 알고 있는 무언가에 연결해야 합니다. 

 

UserManager!

등록, 로그인, 로그아웃 시도를 처리하므로 UserComponent 인스턴스가 있어야 합니다.

 

UserManager가 UserComponent의 새 인스턴스를 생성해야 하는 경우 UserComponent 팩토리에 액세스해야 한다.

팩토리를 생성자 매개변수로 추가하면 Dagger는 UserManager의 인스턴스를 생성할 때 이를 제공합니다.

 

수동 종속성 주입에서는 사용자의 세션 데이터(userDataRepository)가 UserManager에 저장되었습니다.

그것은 사용자가 로그인했는지 여부를 결정했습니다. 대신 UserComponent로 동일한 작업을 수행할 수 있습니다.

 

@Singleton
class UserManager @Inject constructor(
    private val storage: Storage,
    private val userComponentFactory: UserComponent.Factory
) {
    //Remove line
    var userDataRepository: UserDataRepository? = null

    // Add or edit the following lines
    var userComponent: UserComponent? = null
          private set

    fun isUserLoggedIn() = userComponent != null

    fun logout() {
        userComponent = null
    }

    private fun userJustLoggedIn() {
        userComponent = userComponentFactory.create()
    }
}

 

사용자가 UserComponent 팩토리의 create 메소드를 사용하여 로그인할 때 userComponent의 인스턴스를 생성합니다. 그리고 logout()이 호출될 때 인스턴스를 제거합니다

 

MainActivity와 SettingsActivity가 동일한 인스턴스를 공유할 수 있도록 UserDataRepository의 Scope가 UserComponent로 지정되어야 한다.

 

Activity에 lifetime을 연결되는 Component 에 주석을 달기 위해 @ActivityScope 범위 주석을 사용했기 때문에 모든 애플리케이션이 아닌 Multi Activity 을 포함할 수 있는 Scope가 필요합니다. 

 

 

새로운 Scope는 사용자가 로그인했을 때의 수명을 시작하므로 LoggedUserScope라고 부른다.

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

UserComponent가 항상 동일한 UserDataRepository 인스턴스를 제공할 수 있도록 이 주석으로 UserComponent와 UserDataRepository에 주석을 추가한다.

 

UserComponent.kt

// Scope annotation that the UserComponent uses
// Classes annotated with @LoggedUserScope will have a unique instance in this Component
@LoggedUserScope
@Subcomponent
interface UserComponent { ... }

UserDataRepository.kt

// This object will have a unique instance in a Component that 
// is annotated with @LoggedUserScope (i.e. only UserComponent in this case).
@LoggedUserScope
class UserDataRepository @Inject constructor(private val userManager: UserManager) {
    ...
}

MyApplication에는 수동 종속성 주입 구현에 필요한 userManager 인스턴스를 저장했습니다.

하지만 이제 앱은 Dagger 사용하도록 완전히 리팩토링되었으므로 더 이상 필요하지 않습니다

 

MyApplication.kt

open class MyApplication : Application() {

    val appComponent: AppComponent by lazy {
        DaggerAppComponent.factory().create(applicationContext)
    }
}

MainActivity 및 SettingsActivity가 더 이상 AppComponent에 의해 주입되지 않으므로 주입 메서드를 제거하고 UserComponent를 사용합니다. MainActivity 및 SettingsActivity가 UserComponent의 인스턴스에 액세스하는 데 필요하므로 그래프에서 UserManager를 노출합니다

 

AppComponent.kt

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

    // Factory to create instances of the AppComponent
    @Component.Factory
    interface Factory {
        //  @BindsInstance, graph 외부객체인 context 를 AppComponent 에 포함 시켜 그래프에 주입한다.
        fun create(@BindsInstance context: Context): AppComponent
    }

    fun userManager(): UserManager
    
    // Expose RegistrationComponent factory from the graph
    fun registrationComponent(): RegistrationComponent.Factory
    fun loginComponent(): LoginComponent.Factory
}

SettingsActivity에서 @Inject로 ViewModel에 주석을 달고(Dagger에 의해 주입되기를 원하기 때문에) private modifier를 제거

 

SettingsActivity.kt

class SettingsActivity : AppCompatActivity() {

    @Inject
    lateinit var settingsViewModel: SettingsViewModel

    override fun onCreate(savedInstanceState: Bundle?) {


        val userManager = (application as MyApplication).appComponent.userManager()
        userManager.userComponent!!.inject(this)

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)

        setupViews()
    }
}

MainActivity.kt

class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var mainViewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {

        setContentView(R.layout.activity_main)
        super.onCreate(savedInstanceState)

        // 로그인 되어있으면 -> 메인페이지
        // 로그인 안되어있으면 -> 등록한적이 있음 -> 로그인 -> 메인
        //                          없음 -> 등 록  -> 메인
        val userManager = (application as MyApplication).appComponent.userManager()

        if (!userManager.isUserLoggedIn()) {
            if (!userManager.isUserRegistered()) {
                startActivity(Intent(this, RegistrationActivity::class.java))
                finish()
            } else {
                startActivity(Intent(this, LoginActivity::class.java))
                finish()
            }
        } else {
            userManager.userComponent!!.inject(this)
            setupViews()
        }
    }
}

 

조건부 필드 주입(사용자가 로그인한 경우에만 주입할 때 MainActivity.kt에서 하는 것처럼)을 수행하는 것은 매우 위험합니다.

개발자는 조건을 알고 있어야 하며, 주입된 필드와 상호 작용할 때 NullPointerException이 발생할 위험이 있습니다.

 

이 문제를 피하기 위해 사용자의 State에 따라 Registration, Login 또는 Main으로 라우팅되는 SplashScreen을 생성하여 간접 참조를 추가할 수 있습니다

 

 

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

Android Code Lab(Dagger2) 정리7  (0) 2021.12.22
Android Code Lab(Dagger2) 정리5  (0) 2021.12.22
Android Code Lab(Dagger2) 정리4  (0) 2021.12.21
Android Code Lab(Dagger2) 정리3  (0) 2021.12.21
Android Code Lab(Dagger2) 정리2  (0) 2021.12.21