본문 바로가기
안드로이드

D.I (feat Hilt) 기본 - 1

by 삽질러 2021. 11. 10.

Dependency Injection

 

Dagger -> Hilt 

 

구성요소

 

annotation

@Inject

@Component

@Subcomponent

@Module

@Binds

@Scope

 

 

필요한 프로젝트 

git clone https://github.com/googlecodelabs/android-dagger

  

위의 깃 프로젝트를 다운받아서 사용한다. 

 

class RegistrationActivity : AppCompatActivity() {

    lateinit var registrationViewModel: RegistrationViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        registrationViewModel = RegistrationViewModel((application as MyApplication).userManager)

        ...
    }

 

위와 같은 코드에서 registrationViewModel을 D.I 해보자. 

 

먼저 Inject하고자 하는 RegistrationViewModel 클래스를 살펴봐야한다. 

 

class RegistrationViewModel(val userManager: UserManager) {
	...
}

RegistrationViewModel 인스턴스 생성을 위해서는 UserManager 클래스가 필요하다. 

 

UserManager를 살펴보자.

 

class UserManager(private val storage: Storage) {
	...
}

마찬가지로 UserManager 클래스는 Storage 인스턴스를 필요로 한다. 

 

interface Storage {
    fun setString(key: String, value: String)
    fun getString(key: String): String
}

 

파고들어온 가장 마지막 연결고리는 Storage Interface였다.

 

마지막 지점이 Class가 아닌 interface이므로 Storage를 구현하는 실제 클래스를 찾아야하고 

 

이 프로젝트의 경우에는 SharePreferencesStorage 클래스이다.

 

class SharedPreferencesStorage(context: Context) : Storage {
	...
}

SharedPreferencesStorage역시 Context를 생성자에서 필요로 하지만 context의 경우에는 다른 방식이 필요하므로 여기에서 추적을 마무리한다.

 

결과적으로 

 

RegistrationActivity -> RegistrationViewModel -> UserManager -> Storage(실제로는 SharedPreferencesStorage)

 

위와같은 의존성을 가지고있음을 알수있다. 

 

따라서 SharedPreferencesStorage부터 작업을 시작해야한다.

 

class SharedPreferencesStorage @Inject constructor(context: Context) : Storage {
	...
}

위와 같이 단순하게 생성자앞에 @Inject를 표시해 준다. 

이 클래스가 다른곳에서 Dagger를 통해 주입될 것임을 표시해 준다. 

 

일반적인 class라면 이렇게 @Inject annotation을 추가하는것만으로 간단하게 끝나지만. 

 

이경우 UserManager은 생성을 위해 Storage Interface의 인스턴스를 필요로 하므로 Storage의 인스턴스를 요구할때 SharedPreferenceStorage를 전달해주도록 표시해주는 과정이필요하다. 

 

@Module
abstract class StorageModule {

    @Binds
    abstract fun provideStorage(storage : SharedPreferencesStorage):Storage
}

provideStorage라는 함수의 이름은 중요치않고 파라미터(SharedPreferencesStorage)와 리턴값(Storage)가 중요하다.

 

@Binds annotation을 통해 해당 리턴값이 요구될때 파라미터가 전달되는것을 Dagger에게 명시해준다. 

 

 

이후에는 아까 파악했던 의존성 순서의 역순으로 작업해준다.  

 

RegistrationActivity -> RegistrationViewModel -> UserManager -> Storage(실제로는 SharedPreferencesStorage)

 

SharedPreferencesStorage를 dagger를통해 얻어올 수 있으므로 UserManager를 생성할수있고 

마찬가지로 RegistrationViewModel과 RegistrationActivity 까지 생성 가능하다. 

 

class UserManager @Inject constructor(private val storage: Storage) {
}

class RegistrationViewModel @Inject constructor(val userManager: UserManager) {
}

class RegistrationActivity : AppCompatActivity() {

    @Inject
    lateinit var registrationViewModel: RegistrationViewModel
    
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        //registrationViewModel = RegistrationViewModel((application as MyApplication).userManager)
    }

}

 

registrationViewModel이 Dagger에 의해 주입될 예정이므로 인스턴스 초기화 부분의 코드를 없앤다. 

 

 

이제 제반작업이 끝났다. 

 

어떤 과정을 통해서 생성에 필요한 인스턴스를 구해올것인지를 표시해 주는 작업이 끝났으므로 

 

실질적인 주입을 시작한 차례이다. 

 

Dagger의 실질적인 시작은 @Component에서 시작한다. 

@Component
interface AppComponent {
    // Classes that can be injected by this Component
    fun inject(activity: RegistrationActivity)
}

inject함수를 통해 RegistrationActivity가 의존성 주입을 필요로 한다는것을 Dagger에게 알릴수있다. 

 

// Definition of a Dagger component that adds info from the StorageModule to the graph
@Component(modules = [StorageModule::class])
interface AppComponent {
    
    // Classes that can be injected by this Component
    fun inject(activity: RegistrationActivity)
}

 

또한 아까 만들어둔 Storage를 교환하는 모듈을 Component에서 식별가능할수있게 등록해주는 과정이 필요하다. 

 

 

이제 남은 문제는

1. interface인 AppComponent를 어떻게 얻어올것인가

2. 아까 그냥 넘어간 Context를 어떻게 구할까

두가지의 문제가 남아있다. 

 

interface AppComponent {

    // Factory to create instances of the AppComponent
    @Component.Factory
    interface Factory {
        // With @BindsInstance, the Context passed in will be available in the graph
        fun create(@BindsInstance context: Context): AppComponent
    }

    fun inject(activity: RegistrationActivity)
}

Context는 안드로이드 시스템에 의해 제공되고 이는 @BindsInstance를 통해 나타낼수있다. 

 

 

open class MyApplication : Application() {

    // Instance of the AppComponent that will be used by all the Activities in the project
    val appComponent: AppComponent by lazy {
        // Creates an instance of AppComponent using its Factory constructor
        // We pass the applicationContext that will be used as Context in the graph
        DaggerAppComponent.factory().create(applicationContext)
    }

    open val userManager by lazy {
        UserManager(SharedPreferencesStorage(this))
    }
}

 

진입점인 Application클래스에서 AppComponent를 생성한다. 

 

 

class RegistrationActivity : AppCompatActivity() {

    // @Inject annotated fields will be provided by Dagger
    @Inject lateinit var registrationViewModel: RegistrationViewModel

    override fun onCreate(savedInstanceState: Bundle?) {

        // Ask Dagger to inject our dependencies
        (application as MyApplication).appComponent.inject(this)
        
        
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_registration)

    }

    ...
}

 

application에서 appComponent를 참조해 와서 inject(this)를 통해 의존성주입을 시작한다. 

 

 

 

1. 궁금증 : super.onCreate 이전에 appComponent.inject를 해야한다는데. 잘 이해는 가지않음. 

프레그먼트 restoration을 피하기 위해서라는데... restore phase에서 무언가 있나봄...?

 

 

참조 : https://developer.android.com/codelabs/android-dagger

'안드로이드' 카테고리의 다른 글

.aar 라이브러리 생성  (1) 2021.11.24
D.I (feat Hilt) 기본 2 - Scope  (0) 2021.11.12
안드로이드 모바일 - 웨어러블 데이터 전송  (0) 2021.11.10