ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Hilt 사용
    Android 2022. 9. 5. 00:19
    test

    개요

    기존의 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'