Dependency Injection
의존성 주입은 외부에서 객체를 생성해서 전달하는 개념으로, 코드의 재사용성을 높여주고 객체 간의 결합도를 낮춰주기 때문에 유지보수에 장점이 있다.
Hilt
Google
이 Dagger
를 기반으로 만든 의존성 주입(Dependency Injection
) 라이브러리로, Koin
이 Kotlin
에 특화된 심플한 DI
라이브러리라면, Hilt
는 안드로이드에 특화된 DI
라이브러리이다.
Koin
은 런타임 시점에서 에러가 발생하는 반면에, Hilt
는 컴파일 시점에서 에러가 발생하기 때문에 안정성이 더 높고, ViewModelModule
클래스를 안만들어도 자동으로 코드를 생성해주기 때문에 코드 관리 측면에서 장점이 있다.
Hilt Component
Hilt
는 컴포넌트들을 이용해서 필요한 Dependency
를 클래스에 제공할 수 있게 해주는데, @AndroidEntryPoint
어노테이션을 추가하면 해당 클래스에 Dependency
를 제공해 줄 수 있는 컴포넌트를 생성해준다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() { /* ... */ }
Hilt
의 컴포넌트들은 계층을 가지고 있으며 각 컴포넌트들은 부모로부터 Dependency
를 받을 수 있다.
Hilt Anotation
@HiltAndroidApp
Hilt
를 사용하기 위해서는 @HiltAndroidApp
어노테이션을 Application
클래스에 추가해야하며, 이를 통해 Hilt
는 애플리케이션의 생명주기에 맞게 적절한 Hilt
의 구성 요소를 인스턴스화하고 멤버 변수를 주입한다.
@HiltAndroidApp
어노테이션에 의해서 SingletonComponent
가 생성되는데, 앱이 살아있는 동안 Dependency
를 제공하는 역할을 하는 애플리케이션 레벨의 컴포넌트이다.
@HiltAndroidApp
class App : Application() { /* ... */ }
@Inject
Hilt
에 의해 주입 받을 수 있는 객체의 클래스라는 것을 알려주기 위해 생성자에 @Inject
어노테이션을 추가하게 되면 이 클래스는 컴포넌트들에서 사용이 가능해지고, @Inject
어노테이션을 붙여서 원하는 field
에 객체를 주입 받을 수 있게 된다.
class TestClass @Inject constructor() {
fun test(): String = "Test"
}
// Constructor Injection
class DoTestClass @Inject constructor(
private val testClass: TestClass
) {
fun doTest(): String = testClass.test()
}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var doTestClass: DoTestClass // // Field Injection
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
doTestClass.doTest()
}
}
@InstallIn
@InstallIn
어노테이션은 해당 Hilt
모듈이 사용될 안드로이드 클래스를 Hilt
에 알려주는 역할을 한다.
// ActivityComponent 는 안드로이드 액티비티에서 사용될 모듈임을 선언한 것
@Module
@InstallIn(ActivityComponent::class)
class Module { /* ... */ }
@Binds
인터페이스는 생성자가 없어 @Inject
어노테이션을 사용하여 Dependency
를 제공할 수 없기 때문에 @Binds
어노테이션을 사용하여 인터페이스에 대한 Dependency
를 제공할 수 있다.
@InstallIn(SingletonComponent::class)
@Module
abstract class RepoModule {
@Binds
@Singleton
abstract fun createUserRepo(impl: UserRepositoryImpl): UserRepository
}
interface UserRepository {
fun getInfo(name: String): UserModel
}
@Singleton
class UserRepositoryImpl @Inject constructor(
private val userDataSource: UserDataSource
) : UserRepository {
override fun getInfo(name: String) = userDataSource.getInfo(name)
}
@HiltViewModel
class MainViewModel @Inject constructor(
private val userRepository: UserRepository
): ViewModel() {
private val name = "y-mg"
init {
userRepository.getName(name)
}
}
@Binds
어노테이션을 사용하기 위해서는 모듈은abstract class
, 함수는abstract function
이어야 한다.- 함수의 반환형은 구현하고자 하는 인터페이스의 타입이고, 파라미터는 실제 제공하고자 하는 인터페이스의 구현체이다.
@Provides
외부 라이브러리는 자신이 만든 클래스가 아니기 때문에 @Inject
어노테이션을 사용하여 Dependency
를 제공할 수 없기 때문에 Builder
패턴 등을 통해 인스턴스를 생성하고 @Provides
어노테이션을 사용해서 Dependency
를 제공할 수 있다.
@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {
@Singleton
@Provides
fun createGson(): Gson = GsonBuilder()
.setLenient()
.create()
@Singleton
@Provides
fun createHttpClient(): OkHttpClient {
return OkHttpClient.Builder().apply {
retryOnConnectionFailure(true)
}.build()
}
@Singleton
@Provides
fun createRetrofit(
gson: Gson,
okHttpClient: OkHttpClient
): Retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient)
.build()
}
- 함수의 반환형은 제공하고자 하는 인스턴스의 타입이고, 파라미터는 인스턴스 생성에 필요한
Dependency
, 함수의 내부는 실제 인스턴스의 구현으로 이루어져 있다.
@HiltViewModel
Hilt
에서는 ViewModel
에 쉽게 의존성을 주입할 수 있도록 지원해주며, SavedStateHandle
에 대한 주입도 지원해준다.
@HiltViewModel
class MainViewModel @Inject constructor(
private val userRepository: UserRepository,
savedStateHandle: SavedStateHandle
): ViewModel() { /* ... */ }
SaveStateHandle
ViewModel
의 SavedStateHandle
은 ViewModel
의 저장된 상태를 관리하기 위한 모듈이다.
ViewModel
은 액티비티 재생성과 같은 Configuration
변경의 상황에서도 데이터가 안전하게 저장되지만, 프로세스 종료와 같은 시스템에서 시작된 이벤트를 처리해야 할 때는 SavedStateHandle
을 사용하여 백업으로 활용해야 한다.
SavedStateHandle
객체는 Key-Value
형태로 저장되고, 이러한 값들은 프로세스가 중단된 이후에도 유지되며 동일한 객체를 통해 계속 사용할 수 있다.
@ApplicationContext
객체 생성 시 ApplicationContext
가 필요한 경우가 있는데 @ApplicationContext
어노테이션을 사용하면 Hilt
가 알아서 생성해주기 때문에 바로 사용이 가능하다.
@Module
@InstallIn(SingletonComponent::class)
class Module {
@Singleton
@Provides
fun test(
@ApplicationContext context: Context
): Test = Test(context)
}