힐트란?
Hilt는 2020년에 Jetpack 제품군에 합류한 강력하고 사용하기 쉬운 종속성 주입 프레임워크입니다. Android 팀이 Dagger2 팀과 접촉하여 개발한 Android 전용 의존성 주입 프레임워크입니다. Dagger2와 비교할 때 Hilt의 가장 분명한 특징은 단순성이며 Android 전용 API를 제공합니다.
프로젝트에 Hilt 도입
이 섹션에서는 Java 17을 사용하는 새로운 Jetpack Compose 프로젝트를 예로 들어 개발 도구는 Android Studio 2023.1.1 Canary 버전을 사용합니다. 2023년 5월 현재 정보입니다.
첫 번째 단계는 gradle/libs.versions.toml 파일을 열고 Hilt의 Gradle 플러그인 관련 구성을 추가하는 것입니다.
[versions]
hilt = "2.46.1"
[plugins]
hiltAndroid = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
그런 다음 프로젝트의 build.gradle.kts 파일을 열고 플러그인을 가져옵니다.
plugins {
alias(libs.plugins.hiltAndroid) apply false
}
두 번째 단계는 libs.versions.toml에 Hilt의 플러그인 및 종속 라이브러리를 추가하는 것입니다.
[libraries]
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
Hilt는 컴파일 시간 주석을 기반으로 구현되므로 kotlin-kapt 플러그인을 추가해야 합니다. 앱의 build.gradle.kts 파일에 다음 구성을 추가합니다.
plugins {
kotlin("kapt")
}
android {
compileOptions {
// 这里设置为 Java 8 或以上即可
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
implementation(libs.hilt.android)
kapt(libs.hilt.compiler)
}
이제 Hilt가 프로젝트에 성공적으로 도입되었습니다.
Hilt 기본 사용법
준비
Hilt를 사용할 때 애플리케이션 클래스를 사용자 지정해야 합니다. 그렇지 않으면 Hilt가 제대로 작동하지 않습니다. 사용자 지정 Application 클래스에 코드를 작성할 필요는 없지만 @HiltAndroidApp 주석을 추가해야 합니다.
@HiltAndroidApp
class MyApplication : Application() {
}
다음으로 AndroidManifest.xml에 MyApplication을 등록합니다.
<application
android:name=".MainApplication">
</application>
준비 작업이 완료되었으며 다음 작업은 특정 비즈니스 로직에 따라 종속성 주입을 위해 Hilt를 사용하는 것입니다.
진입 지점
Hilt는 Dagger2의 작업을 단순화하므로 브리지 레이어 논리를 작성하기 위해 @Component 주석을 사용할 필요가 없으며 삽입 기능을 몇 가지 Android 고정 진입점(Application, Activity, Fragment, View)에서만 시작하도록 제한합니다. , 서비스, BroadcastReceiver.
이 중 Application 진입점만 @HiltAndroidApp 주석을 사용하여 선언하고 나머지 모든 진입점은 @AndroidEntryPoint 주석을 사용하여 선언합니다. 예를 들어 Activity에서 종속성 주입을 수행하려는 경우 다음과 같이 선언하기만 하면 됩니다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
매개변수 없이 의존성 주입
다음과 같이 클래스를 정의하고 해당 생성자에서 @Inject 주석을 선언합니다.
class MusicPlayer() @Inject constructor() {
fun init() {
Log.d("MusicPlayer", "init")
}
}
Inject in Activity 위에서 작성한 init() 메서드를 성공적으로 호출할 수 있습니다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var musicPlayer: MusicPlayer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
musicPlayer.init()
}
}
매개변수를 사용한 의존성 주입
다음과 같이 플레이어 구성 요소가 의존하는 시스템 오디오 드라이버를 나타내는 위의 MusicPlayer 클래스 생성자에 AudioDriver 매개 변수를 추가합니다.
class MusicPlayer() @Inject constructor(val audioDriver: AudioDriver) {
fun init() {
Log.d("MusicPlayer", "init, audioDriver=$audioDriver")
}
}
AudioDriver 클래스를 선언할 때 생성자에 @Inject 주석도 추가합니다.
class AudioDriver @Inject constructor() {}
코드를 수정하지 않고도 init() 메서드를 성공적으로 호출할 수 있고 audioDriver의 hashCode를 성공적으로 출력할 수 있습니다.
인터페이스 종속성 주입
오디오를 재생할 때 필요한 오디오 디코더를 나타내는 IDecoder 인터페이스를 정의합니다. 인터페이스에서 구현되는 두 가지 방법이 있으며 각각 디코더를 생성 및 파괴하고 메모리를 해제하는 데 사용됩니다.
interface IDecoder {
fun create()
fun destroy()
}
WAV 파일을 디코딩하기 위해 WavDecoder를 구현하고 생성자에 @Inject 주석을 추가합니다.
class WavDecoder @Inject constructor() : IDecoder {
override fun create() {
Log.d("WavDecoder", "create")
}
override fun destroy() {
Log.d("WavDecoder", "destroy")
}
}
또한 MP3 파일을 디코딩하기 위해 Mp3Decoder를 구현하려면 @Inject 주석도 선언해야 합니다.
class Mp3Decoder @Inject constructor() : IDecoder {
override fun create() {
Log.d("Mp3Decoder", "create")
}
override fun destroy() {
Log.d("Mp3Decoder", "destroy")
}
}
DecoderModule이라는 새 추상 클래스를 만들고 이 모듈에서 추상 함수를 정의하여 IDecoder 인터페이스의 필수 인스턴스를 제공합니다.
@Module
@InstallIn(ActivityComponent::class)
abstract class DecoderModule {
@Binds
abstract fun bindDecoder(wavDecoder: WavDecoder): IDecoder
}
방금 제공된 디코더를 호출하도록 MusicPlayer 클래스의 코드를 수정합니다.
class MusicPlayer() @Inject constructor(val audioDriver: AudioDriver) {
@Inject
lateinit var decoder: IDecoder
fun init() {
decoder.create()
Log.d("MusicPlayer", "init, audioDriver=$audioDriver")
decoder.destroy()
}
}
이때 다시 init() 메소드를 호출하면 TAG가 WavDecoder인 로그를 볼 수 있다.
동일한 유형의 다른 인스턴스 주입
@Qualifer 인터페이스는 동일한 유형의 클래스 또는 인터페이스의 다른 인스턴스를 주입하는 데 사용됩니다. 다음과 같이 두 개의 주석을 개별적으로 정의합니다.
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindWavDecoder
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindMp3Decoder
DecoderModule로 돌아가서 두 개의 추상 함수를 정의하고 두 함수 위에 방금 정의한 두 개의 주석을 추가합니다.
@Module
@InstallIn(ActivityComponent::class)
abstract class DecoderModule {
@BindWavDecoder
@Binds
abstract fun bindWavDecoder(wavDecoder: WavDecoder): IDecoder
@BindMp3Decoder
@Binds
abstract fun bindMp3Decoder(mp3Decoder: WavDecoder): IDecoder
}
MusicPlayer 클래스로 돌아가서 플레이어가 동시에 두 가지 디코딩 형식을 지원하도록 할 수 있습니다.
class MusicPlayer() @Inject constructor(val audioDriver: AudioDriver) {
@BindWavDecoder
@Inject
lateinit var wavDecoder: IDecoder
@BindMp3Decoder
@Inject
lateinit var mp3Decoder: IDecoder
fun init() {
wavDecoder.create()
mp3Decoder.create()
Log.d("MusicPlayer", "init, audioDriver=$audioDriver")
wavDecoder.destroy()
mp3Decoder.destroy()
}
}
타사 클래스의 종속성 주입
OkHttp가 제공하는 MainActivity에 OkHttpClient를 주입하려는 경우 생성자에 @Inject 주석을 추가할 수 없습니다. 이 경우 @Module 주석이 있는 비추상 클래스(여기서는 NetworkModule이라고 함)를 정의해야 합니다.
다음과 같이 이 클래스에서 메서드를 정의하고 @Provides 주석을 추가하고 함수 본문에 OkHttpClient 인스턴스를 제공합니다.
@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()
}
}
MainActivity로 돌아가서 @Inject를 사용하여 OkHttpClient를 삽입하면 성공적으로 실행됩니다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var okHttpClient: OkHttpClient
}
개발자의 편의를 위해 NetworkModule에 Retrofit 유형의 인스턴스를 제공하고 다음 코드를 작성합니다.
@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("http://example.com/")
.client(okHttpClient)
.build()
}
}
메소드 provideRetrofit()의 okHttpClient 매개변수는 Hilt가 provideOkHttpClient() 메소드를 사용하여 자동으로 생성합니다. 이때 MainActivity에서 Retrofit을 다시 주입하면 정상적으로 실행될 수도 있습니다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var retrofit: Retrofit
}
Hilt 내장 구성 요소
@Module을 사용하여 주입된 클래스는 @InstallIn 주석을 사용하여 주입 범위를 지정해야 합니다. Hilt는 다양한 시나리오에 삽입하는 데 사용되는 총 7가지 구성요소 유형을 제공합니다.
구성 요소 이름 | 사출 범위 |
---|---|
응용 프로그램 구성 요소 | 애플리케이션 |
활동 유지 구성 요소 | 모델 보기 |
활동구성요소 | 활동 |
FragmentComponent | 파편 |
ViewComponent | 보다 |
ViewWithFragmentComponent | @WithFragmentBindings를 사용하여 정의된 뷰 |
서비스 구성 요소 | 서비스 |
위에서 정의한 NetworkModule을 전체 프로젝트에서 사용하려면 다음과 같이 수정하면 됩니다.
@Module
@InstallIn(ApplicationComponent::class)
class NetworkModule {
}
Hilt 구성 요소 범위
기본적으로 Hilt는 각 의존성 주입 동작에 대해 다른 인스턴스를 생성합니다. 이전의 7개 내장 구성요소에 해당하는 Hilt는 다음과 같은 7개의 구성요소 범위 주석도 제공합니다.
구성 요소 범위 | 해당 내장 구성 요소 |
---|---|
@하나씩 일어나는 것 | 응용 프로그램 구성 요소 |
@ActivityRetainedScope | 활동 유지 구성 요소 |
@ActivityScoped | 활동구성요소 |
@FragmentScoped | FragmentComponent |
@ViewScoped | ViewComponent |
@ViewScoped | ViewWithFragmentComponent |
@ServiceScoped | 서비스 구성 요소 |
NetworkModule에서 전역적으로 제공되는 Retrofit 및 OkHttpClient 인스턴스를 하나만 생성하려면 @Singleton 주석을 추가하기만 하면 됩니다.
@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {
@Singleton
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()
}
@Singleton
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("http://example.com/")
.client(okHttpClient)
.build()
}
}
범위 주석은 이전에 추가한 AudioDriver 클래스와 같은 주입 가능한 클래스 바로 위에 선언할 수도 있습니다.
@Singleton
class AudioDriver @Inject constructor() {
}
이는 AudioDriver가 전역적으로 동일한 인스턴스를 공유하고 AudioDriver 클래스가 전역적으로 종속성 주입될 수 있음을 의미합니다.
위의 그림과 같이 클래스에 대해 특정 스코프 주석을 선언한 후 주석의 화살표가 가리키는 곳이면 어디든지 클래스에 대해 종속성 주입을 수행할 수 있으며 동시에 동일한 인스턴스를 해당 클래스 내에서 공유할 수 있습니다. 범위.
프리셋 한정자
위에서 정의한 AudioDriver 클래스에 컨텍스트 매개변수가 필요한 경우 매개변수 앞에 @ApplicationContext 주석을 추가해야 하며 Hilt는 AudioDriver 클래스에 애플리케이션 유형 컨텍스트를 제공하고 코드를 컴파일하고 전달할 수 있습니다.
@Singleton
class AudioDriver @Inject constructor(@ApplicationContext val context: Context) {
}
활동 또는 다른 유형의 컨텍스트가 필요한 경우 Hilt에서 사전 설정한 다른 한정자를 사용할 수 있습니다.
@Singleton
class AudioDriver @Inject constructor(@ActivityContext val context: Context) {
}
이때 AudioDriver 클래스가 Singleton으로 Qualifier 범위와 일치하지 않기 때문에 코드를 컴파일할 때 오류가 보고됩니다.
활동 및 애플리케이션의 두 가지 유형에 대해 Hilt에는 미리 설정된 주입 기능이 있습니다. 클래스가 Activity 또는 Application에 의존하는 경우 Hilt는 다음과 같이 주석을 추가하지 않고 클래스를 자동으로 식별할 수 있습니다.
class AudioDriver @Inject constructor(val application: Application) {
}
class AudioDriver @Inject constructor(val activity: Activity) {
}
두 유형은 Application 및 Activity여야 하며 해당 하위 유형이 선언되더라도 컴파일이 실패합니다.
HiltViewModel 사용
먼저 다음과 같이 libs.versions.toml에서 관련 종속성을 선언합니다.
[versions]
hilt-lifecycle-viewmodel = "1.0.0-alpha03"
[libraries]
androidx-hilt-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hilt-lifecycle-viewmodel" }
build.gradle.kts에 종속성을 추가합니다.
dependencies {
kapt(libs.androidx.hilt.compiler)
}
@HiltViewModel 주석을 통해 ViewModel을 제공합니다.
@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel() {
}
그런 다음 @AndroidEntryPoint 주석이 달린 Activity 또는 Fragment는 ViewModelProvider 또는 viewModels() 확장을 사용하여 평소와 같이 ViewModel 인스턴스를 가져올 수 있습니다.
class MainActivity : AppCompatActivity() {
private val mainViewModel: MainViewModel by viewModels()
}