I am new to Hilt and have negligible experience in Dagger 2 as well.
I am now doing a simple sample project for my self-learning, and trying to apply Hilt.
I use Retrofit to call APIs.
So, I am trying to refactor my code into using Hilt:
#Module
#InstallIn(SingletonComponent::class)
object WeatherAPIModule {
private const val BASE_URL = "https://api.openweathermap.org/data/2.5/"
#Provides
fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor {
return HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
}
#Provides
fun provideOkHttpClient(loggingInterceptor: HttpLoggingInterceptor): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
}
#Provides
fun provideGsonConverterFactory(): GsonConverterFactory {
return GsonConverterFactory.create()
}
#Provides
fun provideRetrofit(okHttpClient: OkHttpClient, gsonConverterFactory: GsonConverterFactory): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(gsonConverterFactory)
.build()
}
#Provides
fun provideWeatherService(retrofit: Retrofit): OpenWeatherService {
return retrofit.create(OpenWeatherService::class.java)
}
val weatherApiKey by lazy {
ApiKeyStore.getWeatherApiKey()
}
}
My question is, Should I use #Provides on Base URL and API Key as well?
Why do I ask
According to my understanding, if I have a class like this:
class SomeClass #Inject constructor() {
#Inject lateinit var someString: String
...
}
Hilt will try to find modules that provides String, and inject that String here. But clearly that's not what I want.
What should be the best way to do this?
Especially for weatherApiKey. I think I should not have leave it as an ordinary function, because that would lose the point of using Hilt at all.
Related
I have a Hilt module to provide Retrofit API in my app like so:
#Module
#InstallIn(SingletonComponent::class)
object NetworkDataModule {
#Provides
#Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(
HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
)
.build()
}
#Provides
#Singleton
fun provideMyApi(client: OkHttpClient): MyApi {
return Retrofit.Builder()
.baseUrl(MyApi.BASE_URL)
.addConverterFactory(MoshiConverterFactory.create())
.client(client)
.build()
.create()
}
}
And it is my API interface:
interface MyApi {
#GET("search")
suspend fun search(): searchDto
companion object {
const val BASE_URL = "https://myapi.com/"
}
}
The question is how the Retrofits create() extension function knows which class should it return despite I did not pass any information about the MyApi interface to it. The create function implementation is this:
inline fun <reified T> Retrofit.create(): T = create(T::class.java)
How does create(T::class.java) return MyApi in spite of I did not provide any type for T?
My retrofit version is 2.9.0
I'm learning about retrofit interceptors, for work purposes I'm using dagger-hilt for the injection of dependencies to fragments etc. I wrote a custom interceptor to check for connection errors and I'm trying to add it to the Retrofit.Builder():
#Provides
#Singleton
fun provideApi(): StoreApi {
return Retrofit.Builder()
.baseUrl(Constants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.build()
.create(StoreApi::class.java)
}
however, I have no clue how to pass that:
val okHttpClient = OkHttpClient()
.newBuilder()
.addInterceptor(ConnectivityInterceptor)
.build()
as a .client() to the retofit builder (even with dagger-hilt), any ideas?
You can setup a module something like this where you provide all the dependency requirements as functions and exposing them to dagger through #Provides then leave dagger to provide the dependencies as function arguments to build the dependency graph :
#Module class ApiModule {
#Provides
#Singleton
internal fun provideApi(retrofit: Retrofit): StoreApi {
return retrofit
.create(StoreApi::class.java)
}
#Provides
#Singleton
internal fun retrofit(client: OkHttpClient): Retrofit =
Retrofit.Builder()
.client(client)
.baseUrl(Constants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.build()
#Provides
#Singleton
internal fun client(connectivityInterceptor: ConnectivityInterceptor): OkHttpClient =
OkHttpClient.Builder()
.addInterceptor(connectivityInterceptor)
.build()
#Provides
#Singleton
internal fun interceptor(): ConnectivityInterceptor = ConnectivityInterceptor()
}
This is a trivial example based on the supplied code.
ApiModule.kt
#Module
class ApiModule {
#Provides
#Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
#Provides
#Singleton
fun provideUserApi(retrofit: Retrofit): HeroesApi {
return retrofit.create(HeroesApi::class.java)
}
#Provides
#Singleton
fun provideApiManager(): ApiManager {
return ApiManager()
}
}
ApiManager.kt
class ApiManager {
#Inject
lateinit var mRetrofit: Retrofit
fun getAllHeroes(): MutableLiveData<Result<List<Hero>>> {
val mHeroesApi = mRetrofit.create(HeroesApi::class.java)
return NetworkHandler<List<Hero>>().makeCall(mHeroesApi.getAllHeroes())
}
}
HeroesApi.kt
interface HeroesApi {
#GET("/marvel")
fun getAllHeroes(): Call<List<Hero>>
}
Error i am getting
Caused by: kotlin.UninitializedPropertyAccessException: lateinit property mRetrofit has not been initialized
at com.hardik.repository.network.ApiManager.getAllHeroes(ApiManager.kt:17)
at com.hardik.repository.Repository.getHeroesFromNetwork(Repository.kt:16)
at com.hardik.androidtemplate.usecase.GetHeroesUseCase.execute(GetHeroesUseCase.kt:14)
at com.hardik.androidtemplate.viewmodel.HeroListViewModel.<init>(HeroListViewModel.kt:9)
Let me know for more details
You don't need to inject Retrofit instance in your ApiManager. You already have Provide method for HeroesApi in your ApiModule so you can directly pass it. First change your ApiManager with:
class ApiManager(private val mHeroesApi: HeroesApi) {
fun getAllHeroes(): MutableLiveData<Result<List<Hero>>> {
return NetworkHandler<List<Hero>>().makeCall(mHeroesApi.getAllHeroes())
}
}
Then change your ApiModule
#Module
class ApiModule {
// Rest code same as it is already
#Provides
#Singleton
// Since you already have Provide method which provides HerosApi,
// Dagger will automatically inject this below.
fun provideApiManager(herosApi: HerosApi): ApiManager {
return ApiManager(herosApi)
}
}
OR
You can simply change your ApiManager to have HorseApi injected into constructor and you won't need to have Provide method for ApiManager even. For this, change ApiManager with following:
// Notice the #Inject before constructor
class ApiManager #Inject constructor(private val mHeroesApi: HeroesApi) {
fun getAllHeroes(): MutableLiveData<Result<List<Hero>>> {
return NetworkHandler<List<Hero>>().makeCall(mHeroesApi.getAllHeroes())
}
}
and then, you can remove Provide method for ApiManager from ApiModule because your ApiManager only needs HerosApi in constructor and you have defined Provide method which returns HerosApi so dagger already knows how to construct your ApiManager.
So your ApiModule will finally look like this:
#Module
class ApiModule {
#Provides
#Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
#Provides
#Singleton
fun provideUserApi(retrofit: Retrofit): HeroesApi {
return retrofit.create(HeroesApi::class.java)
}
// There's no need for providing ApiManager.
}
I have a class that uses Dagger 2 for dependency injection. Now I want to switch to Koin for dependency injection. There are modules in Koin and I want to make a module out of the class or whatever can be done.
#Module
class NetModule(private val baseUrl: String) {
#Provides
#Singleton
fun providesOkHttpClient(
httpLoggingInterceptor: HttpLoggingInterceptor): OkHttpClient = OkHttpClient.Builder().addInterceptor(
httpLoggingInterceptor).build()
#Provides
#Singleton
fun provideLoggingInterceptor(): HttpLoggingInterceptor {
val interceptor = HttpLoggingInterceptor(
HttpLoggingInterceptor.Logger { message -> Logger.d("NETWORK: $message") })
interceptor.level = HttpLoggingInterceptor.Level.NONE
return interceptor
}
#Provides
#Singleton
fun providesMoshi(): Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
#Provides
#Singleton
fun providesRetrofit(okHttpClient: OkHttpClient, moshi: Moshi): Retrofit {
return Builder().client(okHttpClient).baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
}
#Provides
#Singleton
fun providesApiInterface(retrofit: Retrofit): ApiInterface = retrofit.create(
ApiInterface::class.java)
}
Koin uses a DSL for describing modules. Usually you'd declare the module itself on a top-level. Since you need to provide baseUrl, you'd have to create a factory for it.
The #Provides annotation is completely irrelevant, but #Singleton needs to be translated and does so with single. To retrieve the dependencies, just call get().
fun netModule(baseUrl: String) = module {
single {
HttpLoggingInterceptor(
HttpLoggingInterceptor.Logger { message ->
Logger.d("NETWORK: $message")
}).apply {
level = HttpLoggingInterceptor.Level.NONE
}
}
single {
OkHttpClient.Builder()
.addInterceptor(get<HttpLoggingInterceptor>())
.build()
}
single {
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
single {
Retrofit.Builder()
.client(get())
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
}
single { get<Retrofit>().create(ApiInterface::class.java) }
}
Dagger 2 is generating multiple instances of retrofit interceptor despite marking it as singleton in dagger module. Now the problem is that AuthorizationInterceptor constructor gets called twice which I don't understand why and because of that the headers that I set after getting result from login API get sets to a different instance of Interceptor and while making call to some other API which requires authorizationToken the token is unset.
Here is my ApiModule
#Module
open class ApiModule {
#Provides
#Singleton
fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor {
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
return loggingInterceptor
}
#Provides
#Singleton
fun provideHeaderInterceptor(): Interceptor {
return AuthorizationInterceptor()
}
#Provides
#Singleton
fun provideHttpClient(interceptor: HttpLoggingInterceptor, headerInterceptor: Interceptor): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(interceptor)
.addInterceptor(headerInterceptor)
.build()
}
#Provides
#Singleton
fun provideMoshi(): Moshi {
return Moshi.Builder()
.build()
}
#Provides
#Singleton
fun provideRetrofit(client: OkHttpClient, moshi: Moshi, apiConfig: ApiConfig): Retrofit {
return Retrofit.Builder()
.baseUrl(apiConfig.baseUrl)
.client(client)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
}
#Provides
#Singleton
fun provideFrappApi(retrofit: Retrofit): FrappApi {
return retrofit.create(FrappApi::class.java)
}
Here is my AuthorizationInterceptor class
#Singleton
class AuthorizationInterceptor #Inject constructor() : Interceptor {
override fun intercept(chain: Interceptor.Chain?): Response {
val request = chain?.request()
val requestBuilder = request?.newBuilder()
if (request?.header("No-Authorization") == null && authorization.isNotEmpty()) {
requestBuilder?.addHeader("Authorization", authorization)
}
return chain?.proceed(requestBuilder!!.build())!!
}
private var authorization: String = ""
fun setSessionToken(sessionToken: String) {
this.authorization = sessionToken
}
}
You dont need to make a provide method if you do a constructor injection.
Remove the provideHeaderInterceptor method, then update the provideHttpClient method like below,
#Provides
#Singleton
fun provideHttpClient(interceptor: HttpLoggingInterceptor,
headerInterceptor: AuthorizationInterceptor): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(interceptor)
.addInterceptor(headerInterceptor)
.build()
}
Or if you dont like the solution above, you can remove the #Singleton and #Inject in your AuthorizationInterceptor class.