본문 바로가기

개발/안드로이드

Android Hilt에 대해서 알아보자

반응형

 

배우기 나름 쉽고 빌드하면서 오류를 찾아 낼수 있는 DI Tool 인 Hilt에 대해서 알아보자

 


# 환경 설정

hilt의 경우는 version에 민감하므로 전체적으로 version을 잘 맞춰줘야 할 필요가 있다.
그렇지 않으면 오류를 쏟아 낸다.

 

 

각각 아래 파일들의 버젼을 확인하고 추가 할 부분은 추가해 주자.

 

 

gradle-wrapper.properties

distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip

 

 

project level build.gradle

dependencies {  
 ...
 classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'  //2022.3.18일 기준 
}

 

 

app level build.gradle

//hilt  
plugins {  
 ...
 id 'dagger.hilt.android.plugin'  
 id 'kotlin-kapt'  
}


dependencies {
...
implementation "com.google.dagger:hilt-android:2.38.1"  
kapt "com.google.dagger:hilt-android-compiler:2.38.1"
implementation 'androidx.fragment:fragment-ktx:1.4.1'  //viewModels 사용을 위해
}

 

 

application class 정의

@HiltAndroidApp
class MyApplication:Application() 

 

 

AndroidManifest.xml파일에 application class 적용

<application  
 ...
 android:name=".MyApplication"
             >

    ...

</application>

 


 

# 준비 서두.

DI의 장점

프로그램을 시작한지 얼마 되지 않았다면 DI에 대해서 부정적인 생각이 많을 것이다.

배우기 어렵기만 하고 굳이 필요한가? 라는 의문을 가지게 될텐데

프로그램에 대해서 공부하고 테스트에 관심이 생기면 자연스럽게 더 나은 코드를 고민하게 되고 DI라는 결론 까지 찾아오게 될것이다.

 

DI가 익숙해지고 Test가 자연스러워 지는건 또 다른 이야기 이지만 말이다.
혹은 DI와 테스트를 프로그램 시작할때 부터 배워서 자연스럽게 안드로이드의 DI에대한 관심으로 찾아왔다면 가장 좋겠다.

 

안드로이드의 경우는 마치 공식과도 같이 사용하고 있으니 인터넷의 좋은 코드를 찾아 참고 하면 좋을 듯 하다. 

 


# 간단한 DI에 대해서 알아보자

Hilt의 기본 사용법인 Injection은 @Inject라고 하는 Annotation으로 이루어진다.

 

@Inject

주입되어야 할 변수에 @Inject를 선언해 주고 변수에 주입될 Class의 construct에 @Inject를 선언해 준다.

프로그램이 시작되면 적절한 타이밍에 알아서 주입을 해준다. 

 

기본적인 방법
ex

//MainActiviy
@AndroidEntryPoint  // <-- di의 목적지 
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var musicMng:MusicManager


}


//
@ActivityScoped
class MusicManager @Inject construct() {

}

 

 

만약에 주입하는 클래스의 생성자에 전달해야 할 값이 있다면?
ex

@ActivityScoped  
class MusicManager @Inject constructor(private val manager: SomeBody){  
    var name:String = ""  

}  


class SomeBody @Inject constructor(){  
    var name:String = ""  
}

여기까지 했다면 내가 원하는 클래스를 만들어서 DI를 통해 주입을 받을 수 있다.

 


# @Provides 사용 방법은??

그런데 만약에 내가 만든 클래스가 아닌 외부 라이브러리를 통해 사용하는 object의 주입은 어떻게 할까??

 

Retrofit 환경이 이미 설정되어 있다고 가정하자.

Retrofit은 어떻게 주입할까?

 

 

이렇게 외부 라이브러리를 사용할 때 이용할 수 있는것이 @Provides 이다.

간단하게 특정 String을 주입해보자 String도 프로그래머가 직접 만든 Class는 아니니 construct에 @Inject annotation은 없을 것이다.

 

 

위에서 만들었던 MusicManager를 수정하고 적용해 보자

ex

@ActivityScoped  
class MusicManager @Inject constructor(private val manager: SomeBody, private val area: String) {  
    var name: String = ""  
}  

@Module  
@InstallIn(ActivityComponent::class)  
class MyModule {  

     @Provides  
     fun providesArea(): String {  
            return "Seoul"  
     }  
}

여기에서는 새로운 annotation @Module 과 @InstallIn 이 눈에 들어올텐데 간단하게 설명하자면 다음과 같다.

 

 

@Module로 정의된 class는 Hilt에게 인스턴스 제공방법을 알려준다.
@InstallIn의 경우는 어떤 Scope의 범위에서 사용되어야 하는지를 지정하는 annotation이다.

아래 링크를 통해 구글에서 자세한 내용들은 참고 하자.

 


@InstallIn 상세 설명

 

Hilt를 사용한 종속 항목 삽입  |  Android 개발자  |  Android Developers

Hilt를 사용한 종속 항목 삽입 Hilt는 프로젝트에서 수동 종속 항목 삽입을 실행하는 상용구를 줄이는 Android용 종속 항목 삽입 라이브러리입니다. 수동 종속 항목 삽입을 실행하려면 모든 클래스

developer.android.com

 

 

 

만약 여기에서 더 나아가 제공해야 하는 인스턴스의 종류가 같다면 어떻게 해야할까?
예를 들면 area에 "Seoul"을 adress에 "Jongro" 라면 말이다.
Hilt는 어떻게 area와 adress를 구분하여 알맞게 area에 "Seoul", adress에 "Jongro"라고 주입할 수 있을까?
정답은 정해주지 않으면 모른다! 이다.

 

 

각 해당역할에 맞게 같은 형식의 instance를 주입하는 예제를 이어서 보도록 하자.

@Area, @Adress라는 annotation을 먼저 정의 하고 각 Provide function에 해당하는 annotation을 선언 

실제 주입되는 생성자의 인자 앞에도 선언해서 각각 맞는 값들이 주입 될 수 있게 연결 해 준다. 

@ActivityScoped  
class MusicManager @Inject constructor(  
    private val manager: SomeBody,  
 @MyModule.Area private val area: String,  
 @MyModule.Adress private val adress: String  
) {  
    var name: String = ""  
}  

@Module  
@InstallIn(ActivityComponent::class)  
class MyModule {  

 @Qualifier  
 @Retention(AnnotationRetention.BINARY)  
 annotation class Area  

 @Qualifier 
 @Retention(AnnotationRetention.BINARY)  
 annotation class Adress  


 @Area 
 @Provides fun providesArea(): String {  
   return "Seoul"  
 }  

 @Adress  
 @Provides fun providesAdress(): String {  
   return "Jongro"  
 }

}

 

 


# Bind를 사용하는 방법은??

실제 @Inject 가 일어나야 하는 곳

//Activity
@Inject  
lateinit var myBindService: MyBindService

 

 

1. 주입되어야 할 객체의 interface를 선언한다. 

2. 위의 interface를 상속받아 실제 구체화 될 class를 정의 한다. 

3. Module을 정의한다. interface를 구체화한 객체를 주입받아 interface형으로 반환한다. 여기서 주의 할 점은 module클래스도 fuction도 모두 abstract 형태라는 점이다. 

 

예제를 통해서 자세하게 살펴보자. 

//Bind를 구성하는 곳 

//1. 
interface MyBindService {  
    fun doByService()  
}  

//2.
class MyBindServiceImpl @Inject constructor(@ApplicationContext private val context: Context) : MyBindService {  
    override fun doByService() {  
        Toast.makeText(context, "fromBindInstance", Toast.LENGTH_SHORT).show()  
    }  
}  

//3.
@Module  
@InstallIn(ActivityComponent::class)  
abstract class MyBindModule {  
  @Binds  
  abstract fun myBindInstance(instance: MyBindServiceImpl): MyBindService  
}

# Bind와 Provider는 무엇이 다른가?

우선 결론을 이야기하자면
@Binds는 직접 컨트롤 할 수 있는 Class를 통해 instance 생성이 가능하고
@Provides 는 직접 컨트롤 할 수 없는 Class의 instance DI가 가능 하다.

위에 예제 소스를 자세히 다시 한번 살펴 보자. @Inject를 통해 주입가능하게 만든 class 와 시스템이 제공하는 String 클래스의 주입을 말이다.
예제에서는 String 클래스를 예를 들고 있지만 Retrofit이나 Room의 외부 라이브러리를 사용한다고 생각해 보자.

...
@Provides fun providesArea(): String {  
   return "Seoul"  
 } 




...
@Binds  
  abstract fun myBindInstance(instance: MyBindServiceImpl): MyBindService  


 


@EntryPoint 란?

현재 Hilt에서 지원하는 Android Component는  다음과 같다.

 

Hilt를 사용한 종속 항목 삽입  |  Android 개발자  |  Android Developers

Hilt를 사용한 종속 항목 삽입 Hilt는 프로젝트에서 수동 종속 항목 삽입을 실행하는 상용구를 줄이는 Android용 종속 항목 삽입 라이브러리입니다. 수동 종속 항목 삽입을 실행하려면 모든 클래스

developer.android.com

 

 

그렇다면 Hilt가 지원하지 않는 Android Component이외의 클래스에서는 어떻게 해야 할까??
그때 사용할 수 있는 것이 @EntryPoint annotation 이다.

@EntryPoint를 이용하여 구현한 interface에서 적절한 instance를 주입받고
EntryPointAccessors.fromApplication()를 통해서 해당 EntryPoint로 구체화 시킨 Interface를 통해 instance에 접근 할 수 있다.

class MyClass {  

    @EntryPoint  
    @InstallIn(SingletonComponent::class)  
    interface MyClassInterface {  
        fun getFoo(): Foo  
        fun getBar(): Bar  
    }  

    fun doSomething(context: Context) {  
    	// Android Component가 아니여도 
        // EntryPointAccessors.fromApplication를 통해서 주입 받을 수 있다. 
        val myClassInterface =  
            EntryPointAccessors.fromApplication(context, MyClassInterface::class.java)  
        val foo = myClassInterface.getFoo()  
        val bar = myClassInterface.getBar()  
    }  
}  

class Bar @Inject constructor(){  
    val name = "BB"  
}  

class Foo @Inject constructor(){  
    val name = "FF"  
}

 


안드로이드에서 활용하는 방법은??


android 의 jetpack을 따라가다 보면 많이 접할 수 있는 키워드 들중 빠지지 않는 키워드는 DI, MVVM일 것이다. 그렇다 아무래도 우리가 많이 사용하게 될 부분은 ViewModel이 될것이다.

그럼 ViewModel에서는 어떻게 사용할 수 있는지 한번 살펴 보자.

우선 activity에서 선언하는 ViewModel의 주입에 대해서 살펴 보면 아래와 같이 정의 될수 있다.

//Activity 
private val myViewModel by viewModels<MyViewModel>()

 

by viewModels가 가능하게 해주는 dependencies (app level의 gradle에 추가 해주면 된다.)

implementation 'androidx.fragment:fragment-ktx:1.4.1'

 

 

단 ViewModel에서는 다음과 같이 @HiltViewModel annotation을 선언 해줘야 한다.

@HiltViewModel  
class MyViewModel @Inject constructor(private val myInterfaceAPI: MyInterfaceAPI) : ViewModel() {

    ...

}

 

 

당연하겠지만 ViewModel에서 주입받는 Api 인터페이스의 경우는 Retrofit을 예로 든다면 다음과 같을 것이다.

interface MyInterfaceAPI {  
    companion object {  
        private const val BASE_URL = "https://somewhere.com"  

         fun create(): MyInterfaceAPI {  
            return Retrofit.Builder()  
                .baseUrl(BASE_URL)  
                .addConverterFactory(ScalarsConverterFactory.create())  
                .addConverterFactory(GsonConverterFactory.create())  
                .build()  
                .create(MyInterfaceAPI::class.java)  
        }  
    }  



    @GET("somethingdoit/{dodo}")  
    suspend fun getSummoner(  
        @Path("dodo") something: String  
    ): DoneSomeThing  
}  


@Module  
@InstallIn(SingletonComponent::class)  
class APIModule {  
    @Singleton  
    @Provides 
    fun provideMyAPI(): MyInterfaceAPI {  
        return MyInterfaceAPI.create()  
    }  
}

 

결론.

Dagger - 컴파일시 code를 만들어 의존성 주입이냐!

 

Koin - 가볍고 쉽게 의존성을 주입하는 대신에 Runtime시에 Error를 확인 할 수 있느냐?!! (kotlin만 지원)

 

Hilt - 쉽게 쓸수 있는데 컴파일 하면서 오류를 미리 찾아 낼수 있느냐?! (마치 Dagger처럼, 그러나 버젼을 잘못 맞춰 주기만 해도 프로젝트 빌드가 되지 않는다..;; 난감난감.. )

 

 

그럼 우리는 어떤 Tool을 이용하여 DI를 해야 할까??
쉽게 배울수 있는 Tool을 사용해야 할까? 아니면 좀 어렵더라도 오류를 쉽게 찾을 수 있는 Tool을 사용해야 할까? 쉽게 배울수 있으면서 오류도 쉽게 찾을수 있는 그런 Tool을 사용할 것인가??

쉽게 결정할 수 있는 문제는 아니라고 생각한다 그리고 우리는 이미 모두 답을 알고 있다. 하고있는 프로젝트와 어울리는 Tool이 무엇인지...

 

기존의 Dagger보다는 쉽고 Build 하면서 확인 할 수 있는 나름 쓰기 편한 DI 툴! Hilt를 알아봤다. 빠르게 안정화 되었지만 아직 gradle세팅하면서 version에 예민하다. 


참고 내용

 

lateinit 과 lazy

 

@Retention

Annotation 의 Scope 를 제한하는데 사용되고 파라미터에는 3가지가 있다.

  • SOURCE
    compile time 에만 유용하며 빌드된 binary 에는 포함되지 않는다.
    개발중에 warning 이 뜨는 걸 보이지 않도록 하는 @suppress 와 같이 개발 중에만 유용하고, binary 에 포함될 필요는 없는 경우에 사용한다.
  • BINARY
    compile time 과 binary 에도 포함되지만 reflection 을 통해 접근할 수는 없다.
  • RUNTIME: compile time 과 binary 에도 포함되고, reflection 을 통해 접근 가능하다.
    Custom Annotation 에 @Retention 을 표시해주지 않을경우, 디폴트로 RUNTIME 이 된다.

반응형