How does Hilt inject ViewModel parameters - android

In the documentation on Hilt, it shows this example of injecting a viewmodel into an activity:
#HiltViewModel
class ExampleViewModel #Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: ExampleRepository
) : ViewModel() {
...
}
#AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
private val exampleViewModel: ExampleViewModel by viewModels()
...
}
But what if ExampleRepository itself has a constructor that requires parameters? How would the code in the activity be different? How do you tell Hilt what parameters to pass to Repository?

there is multiple ways but I'll mention one I use
for parameters that are from custom type like retrofit api service or OKHttp
you need to provide it like below
#Module
#InstallIn(SingletonComponent::class)
object NetworkModule {
#Provides
fun provideOkHttpClient(
#ApplicationContext context: Context,
networkManager: NetworkManager,
authenticator: AuthInterceptor,
preferenceHelper: PreferenceHelper
): OkHttpClient {
val httpLogging = HttpLoggingInterceptor()
httpLogging.level = HttpLoggingInterceptor.Level.BODY
val httpClient = OkHttpClient.Builder()
.addInterceptor(authenticator)
.connectTimeout(5, TimeUnit.MINUTES)
.callTimeout(5, TimeUnit.MINUTES)
.readTimeout(2, TimeUnit.MINUTES)
.writeTimeout(2, TimeUnit.MINUTES)
if (BuildConfig.BUILD_TYPE == Constants.STAGING_RELEASE)
httpClient.addInterceptor(httpLogging)
httpClient.addInterceptor(ChuckInterceptor(context))
val httpCacheDirectory = File(context.cacheDir, "responses")
val cacheSize: Long = 10 * 1024 * 1024
val cache = Cache(httpCacheDirectory, cacheSize)
httpClient.cache(cache)
return httpClient.build()
}
}
in this way when a parameter of type OkHttpClient is needed, this function will return it

How do you tell the Hilt? With annotations. There is one REALLY good presentation from Jake Wharton on how Dependency injection works in Dagger2(on which Hilt is based on, so the idea is the same).
Most of what Dagger2/Hilt/DI is a Service Locator. Hilt is a compile-time thing so it goes over all of your files with those annotations and takes note of what is provided where and what needs what and generates files that do all of that logic "under the hood".
Even in your example:
#HiltViewModel
class ExampleViewModel #Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: ExampleRepository
) : ViewModel() {
...
}
You tell Hilt that ExampleViewModel is a #HiltViewModel. You also say that you want Hilt to create it for you - with #Inject - and you say that you want it to have two parameters savedStateHandle and repository. Now Hilt will try to locate in your files if there is a ExampleRepository with #Inject or if some module #Provides it explicitly.
class ExampleRepository #Inject constructor() {
...
}
or
#Module
#InstallIn(SingletonComponent::class)
object StorageModule {
#Provides
fun provideExampleRepository(): ExampleRepository = ExampleRepository()
}

Related

Cannot create instance of ViewModel with Hilt

It looks like I have problems with using Hilt.
I get the following error.
Maybe I need to add something to AppModule or something. I'm not sure...
I use the following dependencies:
implementation "com.google.dagger:hilt-android:2.43.2"
annotationProcessor "com.google.dagger:hilt-android-compiler:2.43.2"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03"
annotationProcessor "androidx.hilt:hilt-compiler:1.0.0"
My ViewModel class looks like this:
#HiltViewModel
class CurrencyViewModel #Inject constructor(
private val repository: CurrencyConverterImpl,
private val dispatchers:DispatcherProvider
): ViewModel(){
The activity is like this:
#AndroidEntryPoint
class CurrencyActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: CurrencyViewModel by viewModels()
AppModule:
#Module
#InstallIn(SingletonComponent ::class)
object AppModule {
#Singleton
#Provides
fun provideCurrencyApi(): CurrencyApi = Retrofit.Builder()
.baseUrl(Utils.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(CurrencyApi::class.java)
#Singleton
#Provides
fun provideCurrencyConverter(api: CurrencyApi): CurrencyConverter = CurrencyConverterImpl(api)
#Singleton
#Provides
fun provideDispatchers(): DispatcherProvider = object : DispatcherProvider {
override val main: CoroutineDispatcher
get() = Dispatchers.Main
override val io: CoroutineDispatcher
get() = Dispatchers.IO
override val default: CoroutineDispatcher
get() = Dispatchers.Default
override val unconfined: CoroutineDispatcher
get() = Dispatchers.Unconfined
}
}
UPDATE:
It looks like Hilt didn't like that I put something to the constructor. It needed a constructor without parameters.
But the question is how do I pass the CurrencyConverter repository and DispatcherProvider to ViewModel?
If I pass any parameter I also get this exception:
In your CurrencyViewModel constructor, try replacing:
private val repository: CurrencyConverterImpl
with:
private val repository: CurrencyConverter
Your #Provides function is providing the interface, so you need to inject the interface. Besides, that improves testability of the viewmodel, as you can supply a test double (e.g., mock or fake) in unit tests.

Hilt: Components and Scopes in Android client-server app

Base app architecture for Android client-server app is:
UI (Activity + Fragment or Jetpack Compose)
ViewModel (also is part of UI actually)
Data (repositories to get data from database (Room) and API (Retrofit))
Hilt has different Components and Scopes for Android https://dagger.dev/hilt/components.html
For my base app architecture I have set DI using Hilt in the following way:
Database
#InstallIn(SingletonComponent::class)
#Module
object DatabaseModule {
#Provides
#Singleton // only this one is marked as #Singleton to get the same instance all the time (logically for Database...)
fun provideDataBase(#ApplicationContext context: Context): AppDataBase {
return AppDataBase.getInstance(context)
}
#Provides
fun provideUsersDao(dataBase: AppDataBase): UsersDao = dataBase.getUsersDao()
}
Network (Datasource)
#InstallIn(SingletonComponent::class)
#Module
object NetworkModule {
#Provides
#Singleton // also as Room instance, it should return the same instance of http client
fun providesOkHttpClient(
httpLoggingInterceptor: HttpLoggingInterceptor,
...
): OkHttpClient = OkHttpClient.Builder()
.addInterceptor(headerInterceptor)
...
.build()
#Provides
#Singleton
fun provideRetrofit(
gson: Gson,
client: OkHttpClient,
queryConverterFactory: Converter.Factory
): Retrofit = Retrofit.Builder()
...
.client(client)
.build()
//... some other stuff needed to setup OkHttp and Retrofit
#Provides
fun provideUserApi(retrofit: Retrofit): UserApi = retrofit.create(UserApi::class.java)
#Provides
fun provideUserDataSource(
userApi: UserApi,
networkErrorConverterHelper: NetworkErrorConverterHelper
): UserDataSource = UserDataSourceImpl(userApi, networkErrorConverterHelper)
}
Repository
#InstallIn(SingletonComponent::class)
#Module
interface RepositoryModule {
#Binds
fun bindUserRepositoryImpl(impl: UserRepositoryImpl): UserRepository
}
class UserRepositoryImpl #Inject constructor(
private val userDataSource: UserDataSource,
private val userDbManager: UserDbManager
) : UserRepository {
override fun observeCachedUsers(): Flow<List<User>> ...
override fun fetchUsers(): Flow<Resource<Unit>> = ...
}
ViewModel
#HiltViewModel
class UsersViewModel #Inject constructor(
private val userRepository: UserRepository,
) : ViewModel() {
...
}
Activity
#AndroidEntryPoint
class UsersActivity : AppCompatActivity() {
val userViewModel: UserViewModel by viewModels()
...
}
So basically everywhere I have used SingletonComponent for Hilt modules and #Singleton annotation for some providers so it would return the same instance (of Database, Http client)
I'm trying to understand how good (correct) all of that and in which cases I could use other components/scopes based on this sample.
When app has only one activity and uses Fragments or Jetpack Compose to implement all screens then I guess I can use ActivityRetainedComponent instead of SingletonComponent for data modules, in this case Hilt releases all instances in memory when a user presses the back button to exit the app (activity is destroyed), in case of SingletonComponent these instances live until process of the app dies, so basically difference in this case is not big but still...

Hilt - Dependency cycle crash

If I just use the AuthRepository class in a single UseCase it's fine. However, if I try to use it in both AuthUseCase and RefreshTokenUseCase as in the example, I get an error.
Any suggestions other than using Lazy<> ?
Any help will be appreciated.
-
Error
-
App_HiltComponents.java:139: error: [Dagger/DependencyCycle] Found a dependency cycle:
public abstract static class SingletonC implements App_GeneratedInjector,
^
AppRepository is injected at
RefreshTokenTokenUseCase(authRepository)
RefreshTokenTokenUseCase is injected at
AppAuthenticator(refreshTokenTokenUseCase)
......
...
..
AuthUseCase(authRepository)
AuthUseCase is injected at
MainViewModel(authUseCase, …)
MainViewModel is injected at
MainViewModel_HiltModules.BindsModule.binds(vm)
My Code
-
#Module
#InstallIn(SingletonComponent::class)
object NetworkModule {
#Singleton
#Provides
fun provideRetrofit(): Retrofit =
Retrofit.Builder()
.baseUrl(Data.url)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
#Module
#InstallIn(SingletonComponent::class)
object ApiModule {
#Singleton
#Provides
fun provideAuthAPI(
retrofit: Retrofit
): AuthAPI = retrofit.create(AuthAPI::class.java)
}
#Singleton
class AuthRepository #Inject constructor(
private var authAPI: AuthAPI,
) {
}
#Singleton
class AuthUseCase #Inject constructor(
private val authRepository: AuthRepository
) : UseCase<Response?, AuthUseCase.Params>() {
}
#Singleton
class RefreshTokenUseCase #Inject constructor(
private val authRepository: AuthRepository
) : UseCase<String?, RefreshTokenUseCase.Params>() {
}
You can use Provider<T> instead of Lazy and than call .get() on it.
#Singleton
class RefreshTokenUseCase #Inject constructor(
private val authRepositoryProvider: Provider<AuthRepository>
) : UseCase<String?, RefreshTokenUseCase.Params>() {
fun getRefreshToken() = authRepositoryProvider.get().getRefreshToken() //example of usage
}
This means RefreshTokenUseCase will be created before AuthRepository is created and later on it will receive singleton AuthRepository instance it needs.
For more complete explanation check this SO post.

Hilt not finding service

I'm trying to understand HILT and I'm creating a sample project, I'm used to creating modules and components, but now with a hilt, there's the InstallIn() and perhaps I have to play with them to do the same as before. For instance, I'm used to creating a module per feature as :
LoginModule (contains: ViewModel, activity/fragment, data source, use case, etc...)
ProductModule (contains: ViewModel, activity/fragment, data source, use case, etc...)
From now I've created a NetworkModule with hilt :
#Module
#InstallIn(ApplicationComponent::class)
object NetworkModule {
#Provides
fun provideRetrofit(httpClient: OkHttpClient): Retrofit =
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(httpClient)
.build()
#Provides
fun provideOkHttpClient(
httpLoggingInterceptor: HttpLoggingInterceptor,
#ErrorInterceptorOkHttpClient errorInterceptor: Interceptor,
): OkHttpClient = OkHttpClient()
.newBuilder()
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(errorInterceptor)
.build()
#ErrorInterceptorOkHttpClient
#Provides
fun provideErrorInterceptor(): Interceptor = ErrorInterceptor()
#Provides
fun provideHttpLogginInterceptor(): HttpLoggingInterceptor {
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.level = when {
BuildConfig.DEBUG -> HttpLoggingInterceptor.Level.BODY
else -> HttpLoggingInterceptor.Level.NONE
}
return loggingInterceptor
}
#Provides
fun provideMyService(retrofit: Retrofit): MyService =
retrofit.create(MyService::class.java)
}
And then I have the feature or let's say part of the app that is the home where I want to show some things from now a List, and I've created this :
#Module
#InstallIn(FragmentComponent::class)
object MainActivityModule {
#Provides
fun proviceDataSource(
myService: MyService,
myMapper: MyMapper,
): MyDatasource = MyDatasourceImpl(myService, myMapper)
#Provides
fun provicesMyMapper(): MyMapper = MapperImpl()
}
And in my Activity, I've added the
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
And in Fragment, I don't have anything maybe this is the thing that I'm doing wrong?
And when to use the ApplicationComponent SingletonComponent , etc?
Since I was using dagger (not dagger-android) I had a ComponentFactory where I had all of this Components as (LoginComponent, GoogleMapComponent, ReferalCodeComponent) all of those were a feature (talking about multi-module project) and there I was creating the factory of these components, how's the way of doing this now with Hilt?
When using multiple modules, if I follow my idea, I will define multiple modules in a library and provide them to the interface module for use.
Your question is how to use the factory method you defined in the interface module? I don't understand your question very well on this point.
If you use the MVVM architecture, you can inject these components into the ViewModel . The Flow provided by these components (such as LoginComponent) can be accessed in the ViewModel. Of course, the parameters injected through the ViewModel can also easily access the corresponding methods of the components you provide.
Below is the ViewModel in my Demo:
#HiltViewModel
class NickNameViewModel #Inject constructor(
private val repo: DataRepo,
application: Application,
): AndroidViewModel(application) {
fun updateUserNickName(
userId: String,
userKey: String,
userNickName: String,
resultCallBack: (Boolean, String) -> Unit = {_, _ -> },
) {
viewModelScope.launch {
repo.updateUserInfo(
userId = userId,
userKey = userKey,
userNickName = userNickName,
).collect {
//.....
}
}
}
}
}
In the above ViewModel, the DetaRepo component is injected into the ViewModel through construction injection.
The method provided in DataRepo can be easily accessed in the method updateUserNickName.
Below is the code of ViewModel in Fragment:
#AndroidEntryPoint
class NickNameModifyFragment: Fragment(R.layout.nick_name_modify_fragment) {
companion object {
const val nickNickNameResult = "nickNickNameResult"
}
private var _binding: NickNameModifyFragmentBinding? = null
private val viewModel by viewModels<NickNameViewModel>()
}
It should be noted that if Hilt is in Fragment's ViewModel, you still need to add the #AndroidEntryPoint annotation to the attach Activity.
Hope to bring you good luck.
If your using SingletonComponent then with the #Provides annotation you should also required to use #singleton annotation
#Module
#InstallIn(SingletonComponent::class)
object AppModule {
#Singleton #Provides
fun provideServiceGenerator(
context: Context
) = ServiceGenerator(context)
}
similarly For FragmentComponent you need annotate with
#Provides #FragmentScoped
as I read you mentioned UseCase and other stuff u must check out hilt viewModel because it will auto generate a lot boilerplate code for you and handles dependency according to activity or fragment lifecycle
#Module
#InstallIn(ViewModelComponent::class)
object ViewModelModule {
//repo
#Provides
#ViewModelScoped
fun providesTicketsRepo(
checkOutApi: CheckOutApi
)= TicketsRepo(checkOutApi)
}
#HiltViewModel
class TicketsAndCouponViewModel
#Inject constructor(
val ticketsRepo: TicketsRepo
) : ViewModel() { /****/}
inside fragment and activity
val viewModel by viewModels<TicketsAndCouponViewModel>()

Using Hilt, how to inject into a class that does not have a context?

I have a class named NetworkManager. Since it is not one of the Android Components, I am using custom entry point, NetworkManagerEntryPoint with one fun that returns NetworkClient object which is what I want to inject.
Now, to inject an instance of this class using Hilt, I believe I need to use one of the Helper methods in EntryPointAccessors. But all of them requires a reference to android components. So, do I really have to pass an android component like Context to my class to inject an object using Hilt?
class NetworkManager() {
#InstallIn(SingletonComponent::class)
#EntryPoint
interface NetworkManagerEntryPoint {
fun getNetworkClient(): NetworkClient
}
var defaultNetworkClient: NetworkClient = EntryPointAccessors.fromApplication(
context, // Do I have to pass a context to this class to use Hilt?
NetworkManagerEntryPoint::class.java
).getNetworkClient()
fun <R : Any> executeRequest(
request:Request<R>,
networkClient: NetworkClient = defaultNetworkClient
): Response<R> {
// Do some operation
}
}
Hi there maybe you can try this way i have done , i follow the mvvm pattern
My RetrofitApi
interface RetrofitApi {
#GET("endpoint")
suspend fun getApi():Response<RetrofitApiResponse>
}
My NetworkModule
#Module
#InstallIn(SingletonComponent::class)
object NetworkModule{
#Singleton
#Provides
fun provideApi(): RetrofitApi = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(RetrofitApi::class.java)
#Singleton
#Provides
fun provideRepository(retrofitApi:RetrofitApi) : MainRepository =
DefualtMainRepository(retrofitApi)
}
and this module gets injected in my repository
class DefualtMainRepository #Inject constructor(
val retrofitApi: RetrofitApi
):MainRepository {
override suspend fun getQuotes(): Resource<RetrofitApiResponse> {
val response = retrofitApi.getApi()
val result = response.body()
if (response.successful){
}
}
}
If you are interested i have full project in my github and even wrote a medium article explaining it, Hopefully my answer is helpful to you
https://zaidzakir.medium.com/a-simple-android-app-using-mvvm-dagger-hilt-e9f45381f1bc
Okay. #Zaid Zakir's answer showed me that I can inject objects via constructor parameters, if not field injection. So, the solution for me ended up looking like this.
#Singleton
class NetworkManager #Inject constructor(
var defaultNetworkClient: NetworkClient
) {
fun <R : Any> executeRequest(
request:Request<R>,
networkClient: NetworkClient = defaultNetworkClient
): Response<R> {
// Do some operation
}
}
In another class named NetworkClientModule, I have this,
#Module
#InstallIn(SingletonComponent::class)
abstract class NetworkClientModule {
#Binds #Singleton
abstract fun bindDefaultNetworkClient(impl: DefaultNetworkClient): NetworkClient
}

Categories

Resources