Hilt

안드로이드 의존성 주입 라이브러리

Hilt

Dependency Injection


의존성 주입은 외부에서 객체를 생성해서 전달하는 개념으로, 코드의 재사용성을 높여주고 객체 간의 결합도를 낮춰주기 때문에 유지보수에 장점이 있다.


Hilt


GoogleDagger 를 기반으로 만든 의존성 주입(Dependency Injection) 라이브러리로, KoinKotlin 에 특화된 심플한 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

ViewModelSavedStateHandleViewModel 의 저장된 상태를 관리하기 위한 모듈이다.

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