본문 바로가기

Android

Hilt 사용

개요

기존의 DI를 구성하기위해 구글에서 제공한 Degger2를 사용하여 의존성 주입을 하였지만
높은 학습비용 및 많은 보일러플레이트 코드를 생성한다는 단점때문에
조금더 편한 DI 프레임워크가 나오게된다 이게 Hilt이다.
Hilt는 Dagger를 쉽게 사용할수있도록 도와주는 도구이다.
💡Dagger 은 칼 종류이고 Hilt는 칼집 으로 네이밍 하였다.

 

프로젝트 세팅

의존성 추가

프로젝트 레벨

buildscript {
    repositories {
    }
    ext{
        hiltVersion = '2.38.1'
    }
    dependencies {
        classpath "com.google.dagger:hilt-android-gradle-plugin:${hiltVersion}"
    }
}

앱 레벨

plugins {
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}
dependencies {
    implementation "com.google.dagger:hilt-android:${hiltVersion}"
    kapt "com.google.dagger:hilt-compiler:${hiltVersion}"
}

Application 설정

@HiltAndroidApp
class HiltApplication :Application() {
}

@HiltAndroidApp 는 의존성 주입이 가능하도록 Hilt코드를 생성하게 하는 트리거 역활을 수행한다.
이 어노테이션이 붙은 클래스는 어플리케이션 컨테이너로써 앱의 생명주기와 밀접하게 연결된다.
어플리케이션 컨테이너는 앱의 상위 컨테이너로 다른 컨테이너는 이 상위 컨테이너에서 제공하는 클래스에 접근할수있다.

AndroidManifest에 등록.

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.SampleApplication"
    android:name=".hilt.HiltApplication">

기존 코드

class HiltActivity : AppCompatActivity() {
    private lateinit var dataService: DataService
    private lateinit var loginService: LoginService

    private val btn :Button by lazy { findViewById(R.id.hilt_btn) }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_hilt)

        dataService = DataService()
        loginService = LoginService()

        btn.setOnClickListener {
            loginService.login("id","pwd")
            dataService.getUserInfo()
        }
    }
}

class DataService {
    fun getUserInfo() = "user name!"
}

class LoginService {
    fun login(id:String,pwd:String) = true
}

activity에서 service에대해 인터턴스를 생성하는 모습이다.

Hilt적용 하기

클래스 필드 주입

@AndroidEntryPoint
class HiltActivity : AppCompatActivity() {

    @Inject lateinit var dataService: DataService
    @Inject lateinit var loginService: LoginService

    private val btn :Button by lazy { findViewById(R.id.hilt_btn) }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_hilt)

        btn.setOnClickListener {
            loginService.login("id","pwd")
            dataService.getUserInfo()
        }
    }
}

Activity 에서 hilt를 사용하려면 @AndroidEntryPoint 어노테이션을 사용해야한다.
이를 사용하면 해당 activity의 생명주기에 연결된 종속 컨테이너를 생성하고 인스턴스를 주입한다.
만약 프레그먼트에 @AndroidEntryPoint 를 사용하려하면 프레그먼트를 품고있는 엑티비티에도 추가해줘야한다.

@Inject 인스턴스가 붙은 필드들에 주입이된다.
위와같은 형태를 클래스 필드 주입이라고한다.
💡private 필드에는 주입이 되지않는다.

주입 지정

이제 주입해줄 인스턴스를 hilt에서 알게해야한다.

@Singleton
class DataService @Inject constructor(){
    fun getUserInfo() = "user name!"
}
class LoginService @Inject constructor() {
    fun login(id:String,pwd:String) = true
}

@Inject 어노테이션과 constructor() 통해 hilt에게 DataService,LoginService 에 대한 인스턴스 제공방법을 알리게된다.
constructor()는 생성자로 이 인스턴스를 생성할때 어떤 파라미터(의존)가 필요한지 알리게 되는데
위의 경우는 따로 넘길게 없기때문에 안에 비워져있는걸 볼수있다.
@Singleton 은 해당 인스턴스를 싱글톤으로 제공한다.

주입할 인스턴스에 의존 주입

위에서 본거처럼 아무런 의존관계가없는 서비스들은 constructor() 에 아무것도 넣지않았지만

class LoginService @Inject constructor(private val loginUseCase: LoginUseCase) {
    fun login(id:String,pwd:String) = loginUseCase.loginConnection(id,pwd)
}
class LoginUseCase {
    fun loginConnection(id:String, pwd:String)= true
}

위와같이 의존이 필요한 경우가 발생하게된다.
이때는 module 을 통해 이 인스턴스에 의존을 주입해야한다.

@InstallIn(ApplicationComponent::class)
@Module
object HiltModule {

    @Provides
    fun provideLoginUseCase() : LoginUseCase{
        return LoginUseCase()
    }    
}

@Module 은 Hilt에게 모듈임을 알리게된다.
@InstallIn 을 통해 어느 컨테이너에서 결합을 사용하는지를 알리게된다.

예를 들어 Application 컨테이너는 ApplicationComponent
Fragment컨테이너는 FragmentComponent 로 연결한다.

ViewModel 컨테이너는 ViewModelComponent 로 연결한다.
@Provides 는 Hilt에게 이 유형의 인스턴스를 제공해야할때마다 실행되며
LoginUseCase 를 LoginService 에서 필요로 하기때문에
이곳에서 LoginUseCase 를 주입하게된다.

위와같이 단순한 주입이 아닌 Room처럼 복잡한 생성이 필요한 경우에도

@Module
object DatabaseModule {
    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
        return Room.databaseBuilder(
            appContext,
            AppDatabase::class.java,
            "logging.db"
        ).build()
    }

    @Provides
    fun provideLogDao(database: AppDatabase): LogDao {
        return database.logDao()
    }
}

class LoginService @Inject constructor(private val logDao: LogDao) {
 
}

Module안에서 인스턴스를 생성하고 생성된 인스턴스로 메소드를 호출하여 그 결과를 주입하는것도 가능하다.

ViewModel 주입

💡알파버전에서는 해당 ViewModel 을 inejct 대상이라는것을 알리기위해 @ViewModelInject 을 사용했지만 이제 일정 버전 이상부터 @Inject로 동일하게 가능해지고 대신 @HiltViewModel 를 붙혀야한다.

기존 코드

class HomeFragment : Fragment() {
    private val viewModel : HomeViewModel by lazy{  
												 ViewModelProvider(this).get(HomeViewModel::class.java)}
}
class HomeViewModel: ViewModel() {}

Hilt 적용 코드

@AndroidEntryPoint
class HomeFragment : Fragment() {
    private val viewModel : HomeViewModel by viewModels()
    //val viewModel by viewModels<HomeViewModel>() 둘다 된다.
}

@HiltViewModel
class HomeViewModel @Inject constructor(): ViewModel() {

이곳을 보면 @AndroidEntryPoint 가 있는것을 볼수있는데
Fragment에 해당 어노테이션을 붙히기 위해서는 Fragment를 품고있는 Activity에도 추가해줘야한다.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {}

예외

M1이슈

에러 내용

Execution failed for task ':app:kaptDebugKotlin'.
> A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptWithoutKotlincTask$KaptExecutionWorkAction
   > java.lang.reflect.InvocationTargetException (no error message)

해결

dependencies {
	def roomVersion = "2.3.0"
	implementation("androidx.room:room-runtime:$roomVersion")
	kapt("androidx.room:room-compiler:$roomVersion")
	kapt("org.xerial:sqlite-jdbc:3.34.0")
}

viewModels()

Unresolved reference: viewModels

위와같이 viewModels 를 찾지못하는경우

implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'