Photo by Anis Rahman on Unsplash

 

 

 

프로젝트에 Hilt를 적용하다 막히셨나요? 🤷‍♂️🤷‍♂️

Hilt에 사용되는 @Binds, @Provides 적용에 

도움되는 내용을 준비했습니다.

(도움이 되었으면... 좋겠네요..❗️)

 

지난번에 Hilt적용 방법과 간단한 내용에 대하여 알아봤습니다.

 

Android DI Hilt 적용하기 (Koin vs Hilt)

Photo by Anis Rahman on Unsplash 다들 "단검"🗡 많이 사용해 보셨나요?? "단검 칼자루"⚔️는 많이 만져보셨나요?? 갑자기 시작부터 이상한 소리인가 하시겠지만 저는 요즘 핫한 Android DI "Dagger"와 "Hilt"에

salmonpack.tistory.com

 


 

✔️ Hilt 모듈 (@Binds, @Provides)

✔️ @InstallIn

✔️ Qualifier / Predefined qualifiers

 


 

✔️ Hilt 모듈 (@Binds, @Provides)

 

지난번 적용했던 Hilt 의존성 주입을 하지 못하는 경우도 있습니다.

 

예를들면 인터페이스를 생성자 주입하지 못하고 

외부 라이브러리 Retrofit 클래스 같은 경우 

말 그대로 외부 라이브러리 이기때문에 프로젝트 내부에서 직접적으로 종속 관계 삽입이 불가능 합니다.

이럴 때는 Hilt모듈을 이용하여 Hilt에 결합 정보를 제공할 수 있습니다.

 


 

✔️ @Binds

 

interface MyService {
    fun getMyMethods(str: String)
}

위와 같은 인터페이스를 생성하고 MyService를 상속받는 Class들을 생성하여 getMyMethods를 사용하려고 합니다.

 

class MyServiceRepository @Inject constructor(
    private val weatherRepository: WeatherRepository
): MyService {

    override fun getMyMethods(str: String) {
        weatherRepository.getData(str)
    }
}


class MySecondServiceRepository @Inject constructor(
    private val weatherRepository: WeatherRepository
): MyService {

    override fun getMyMethods(str: String) {
        weatherRepository.getDataList(str)
    }
}

 

MyService를 상속받아 사용하는

MyServiceRepository, MySecondServiceRepository가 있습니다.

 

 

@HiltViewModel
class MainViewModel @Inject constructor(
    private val myServiceRepository: MyService
): BaseViewModel() {
   
   ...
   
   myServiceRepository.getMyMethods(str)
   
}

 

생성자에 myServiceRepository를 받아 getMyMethods를 호출하고 있습니다.

얼핏 보기에는 아무 문제없이 보이는 소스이지만

여기에는 큰 문제가 하나 있습니다. 

MyService는 인터페이스 입니다. 어디서 누구든 상속받을수 있죠!

 

MainViewModel에서 생성자의 myServiceRepository: MyService를 보면 

Hilt는 혼란스러울 것 입니다. 

왜냐면 MyService를 상속받은 

MyServiceRepository, MySecondServiceRepository 둘중에 선택할 수 없기 때문입니다. 

여기서 오늘의 키워드 @Binds가 활약을 합니다.

 

@Module
@InstallIn(SingletonComponent::class)
abstract class MyModule {

    @Binds
    @Singleton
    @MyRepository
    abstract fun bindMyRepository(impl: MyServiceRepository): MyService

    @Binds
    @Singleton
    @MySecondRepository
    abstract fun bindMySecondRepository(impl: MySecondServiceRepository): MyService

}

@Qualifier
annotation class MyRepository

@Qualifier
annotation class MySecondRepository

.

위와 같이 @Qualifier를 사용한 annotation class를 정의한 후 

@Binds와 같이 정의한 @MyRepository 또는 @MySecondRepository를 통하여 

상속받은 MyService를 구분 시킵니다. 

 

(고객님 당황하셨죠?? 😅 갑자기 등장한 @InstallIn과 @Qualifier는 아래에서 다뤄보겠습니다.)

 

@HiltViewModel
class MainViewModel @Inject constructor(
    @MySecondRepository private val myServiceRepository: MyService // 또는 @MyRepository
): BaseViewModel() {
   
   ...
   
   myServiceRepository.getMyMethods(str)
   
}

 

이제 다시 MainViewModel의 생성자 앞에 정의해준 annotation을 사용하여

MyServiceRepository 또는 MySecondServiceRepository를 구분하여 주면

끝!!! 

 

어때요 참 쉽죠?!  👨‍🎨🎨

 

(사실 저도 이해하는데 오랜 시간이 걸렸습니다 ㅠㅠ...)

 

 

 

✔️ @Provides

 

@Binds가 인터페이스에 사용되었다면 

@Provides는 외부 라이브러리에 자주 사용됩니다. 

외부 라이브러리에 경우 생성한 프로젝트 안에서 구현된 것이 아니기 때문에

직접적인 종속관계 형성에 어려움이 있습니다.

그렇기 때문에 @Provides를 사용합니다.

Android에서 자주 사용되는 통신 라이브러리 Retorfit 사용 예제를 준비했습니다.

 

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun httpLoggingInterceptor(): LoggingInterceptor =
        LoggingInterceptor.Builder() // 전송로그
            .setLevel(Level.BASIC)
            .log(Platform.INFO)
            .tag("log")
            .build()

    @Provides
    @Singleton
    fun provideOkHttpClient(httpLoggingInterceptor: LoggingInterceptor): OkHttpClient =
        if (PRINT_LOG) {
            OkHttpClient.Builder()
                .cookieJar(JavaNetCookieJar(CookieManager()))       // 쿠키 매니저 연결
                .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)  // 쓰기 타임아웃 시간 설정
                .writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)      // 읽기 타임아웃 시간 설정
                .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)        // 연결 타임아웃 시간 설정
                .cache(null)                                 // 캐시사용 안함
                .addInterceptor { chain ->
                    chain.proceed(
                        chain.request()
                            .newBuilder()
                            .build()
                    )
                }
                .addInterceptor(httpLoggingInterceptor)
                .addNetworkInterceptor(StethoInterceptor()) // Stetho 로그
                .build()

        }
        
    /**
     * 기본 설정하여 Retrofit을 반환
     *
     * @return 설정이 반영된 Retrofit
     */
    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit =
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(okHttpClient)
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())  // Rx를 사용할 수 있도록 아답터 적용
            .addConverterFactory(ScalarsConverterFactory.create())      // ScalarConverter 적용
            .addConverterFactory(GsonConverterFactory.create())         // GsonConverter 적용
            .build()
            
            
    @Provides
    @Singleton
    fun baseApi(retrofit: Retrofit): BasicApi = retrofit.create(BasicApi::class.java)
}

 

interface BasicApi {

    @GET
    fun getApi(
        @Url url: String,
        @QueryMap params: Map<String, Any?> = mapOf(),
        @HeaderMap headers: Map<String, Any?> = mapOf()
    ): Flowable<JsonObject>

}

 

class WeatherRepository @Inject constructor(
    private val baseApi: BasicApi,
    private val weatherNowDao: WeatherNowDao
) { 

    ...
    
    fun getNow(
        params: Map<String, Any?> = mapOf(),
        header: Map<String, Any?> = mapOf()
    ): Flowable<JsonArray> {
        return baseApi.get(		// REST API GET 방식 호출
        ...
    
    }
    
    ...
    
}

 

저와 같은 경우 WeatherRepository에서 BasicApi를 받아 GET 방식으로 API 호출하고 있습니다.

 


 

✔️ @InstallIn

 

드디어 위의 예제에서 지속적으로 나왔던

@InstallIn에 대하여 이야기 하려고 합니다.

 

 

@InstallIn 어노테이션을 사용하여 해당 Hilt 모듈이 사용될

Android Class를 Hilt에 알려주는 역할을 합니다.

 

@Module
@InstallIn(ActivityComponent::class)
class ModuleClass { 
    ... 
 }

 

예를들어 위와같이 소스가 있다면

해당 Hilt모듈은 Android Activity에서 사용될 모듈임을 선언한 것 입니다.

 

 


 

✔️ Qualifier / Predefined qualifiers

 

@Qualifier의 경우 위에서 @Binds를 살펴볼때 나왔습니다.

 

@Module
@InstallIn(SingletonComponent::class)
abstract class MyModule {

    @Binds
    @Singleton
    @MyRepository
    abstract fun bindMyRepository(impl: MyServiceRepository): MyService

    @Binds
    @Singleton
    @MySecondRepository
    abstract fun bindMySecondRepository(impl: MySecondServiceRepository): MyService

}

@Qualifier
annotation class MyRepository

@Qualifier
annotation class MySecondRepository

 

Hilt는 동일한 유형에 대해 여러 결합자를 제공합니다.

Hilt 구현체가 중복될 경우

@Qualifier를 통하여 구분시켜 주입을 시도합니다.

 

 

[Android] Hilt @Qualifier - Retrofit 서로다른 baseUrl 구분하기

오늘의 주제는 바로 Hilt를 사용하여 Retorfit을 사용할 때 baseUrl이 다른 두 종류의 Retrofit Builder를 사용하기 입니다. Android Hilt 적용하기2 (@Binds, @Provides, @ApplicationContext) Photo by Anis Rahman on Unsplash 프

salmonpack.tistory.com

 

이를 한정자라고 하며

사전의 정의되어 있는 한정자는  Predefined qualifiers이며

대표적인 예로는 @ApplicationContext, @ActivityContext를 제공합니다.

 

class AnalyticsAdapter @Inject constructor(
    @ActivityContext private val context: Context,
    private val service: AnalyticsService
) { ... }

 

Context 객체가 필요한 경우

상황에 따라 @ApplicationContext, @ActivityContext 한정자를 사용합니다.

 

 


여기까지 저의 긴 글을 읽어주셔서 감사합니다.

제가 습관적으로 코딩을 하는 그날까지 습관적으로 코딩을 하기 위해 글 작성을 꾸준하게 해보겠습니다.

 

 

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기