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.
Related
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()
}
I have created a custom component AuthUserComponent using Hilt and need to provide multiple implementation to DataRepository interface.
class Sample #Inject constructor(
#DemoMode private val demoRepository: DataRepository,
#ProductionMode private val productionRepository: DataRepository
) {}
I have created the below #Provides implementations of the interface:
Module
#InstallIn(AuthUserComponent::class)
object DIModule {
#AuthUserScope
#DemoMode
#Provides
fun provideDataRepositoryImplDemo(): DataRepository =
DataRepositoryImplDemo()
#AuthUserScope
#Provides
#ProductionMode
fun provideDataRepositoryImpl(): DataRepository =
DataRepositoryImpl()
}
How do I provide multiple repository implementations via Entrypoint and bridge it with SingletonComponent? I get the below error:
DataRepository is bound multiple times
error
#InstallIn(AuthUserComponent::class)
#EntryPoint
interface AuthUserDataEntryPoint {
#ProductionMode
fun dataRepositoryImpl(): DataRepository
#DemoMode
fun dataRepositoryImplDemo(): DataRepository
}
#Module
#InstallIn(SingletonComponent::class)
internal object AuthUserDataEntryBridge {
#DemoMode
#Provides
internal fun provideDataRepositoryImplDemo(
authUserComponentManager: AuthUserComponentManager
): DataRepository {
return EntryPoints
.get(authUserComponentManager, AuthUserDataEntryPoint::class.java)
.dataRepositoryImplDemo()
}
#ProductionMode
#Provides
internal fun provideDataRepositoryImpl(
authUserComponentManager: AuthUserComponentManager
): DataRepository {
return EntryPoints
.get(authUserComponentManager, AuthUserDataEntryPoint::class.java)
.dataRepositoryImpl()
}
}
The issue to me seems to be that you are defining implementations in both DIModule and AuthUserDataEntryBridge for the same things. To be honest, I'm not really sure what the purpose of AuthUserDataEntryBridge is and I'm unfamiliar with the usage of EntryPoints inside a class annotated with #Module.
Is there are reason you can't just do this install this in the SingletonComponent directly:
Module
#InstallIn(SingletonComponent::class)
object DIModule {
#DemoMode
#Provides
fun provideDataRepositoryImplDemo(): DataRepository =
DataRepositoryImplDemo()
#Provides
#ProductionMode
fun provideDataRepositoryImpl(): DataRepository =
DataRepositoryImpl()
}
That seems to be the core of the issue here. I'm guessing its because of the custom component / scope. In that case, I think you just need to use EntryPoints where you are actually injecting the repositories:
class Sample {
private lateinit var demoRepository: DataRepository
private lateinit var productionRepository: DataRepository
fun inject(authUserComponent: AuthUserComponent) {
val entryPoint = EntryPoints.get(authUserComponent, AuthUserDataEntryPoint:class.java)
demoRepository = entryPoint.demoRepositoryImpl()
productionRepository = entryPoint.productionRepositoryImpl()
}
}
But I don't think you're going to get away with trying to hide the EntryPoints usage inside a module definition.
I'm migrating from Dagger2 to Hilt however I'm unable to solve this part:
#Module
class HistoryPurchaseModule(private val historyPurchaseFragment: HistoryPurchaseFragment) {
#Provides
fun provideHistoryPurchaseFragment(): HistoryPurchaseFragment = historyPurchaseFragment
#Provides
fun provideUseCase() = HistoryPurchaseUseCase()
#Provides
fun provideSession() = HistoryPurchaseSession(
isHistoryLoading = MutableLiveData(true),
isThisTheFirstTime = true,
isHistoryEmpty = MutableLiveData(false)
)
#Provides
fun provideViewModel(session: HistoryPurchaseSession): HistoryPurchaseViewModel {
return historyPurchaseFragment.createViewModel {
HistoryPurchaseViewModel(session)
}
}
#Provides
fun provideLogic(
useCases: HistoryPurchaseUseCase,
viewModel: HistoryPurchaseViewModel
) = HistoryPurchaseLogic(viewModel.viewModelScope.coroutineContext, useCases, historyPurchaseFragment, viewModel)
}
My logic class takes two interfaces in the constructor that the viewModel(HistoryPurchaseContract.ViewModel) and fragment(HistoryPurchaseContract.View) implement.
Fragment:
class HistoryPurchaseFragment : BaseFragment<HistoryPurchaseEvent>(), HistoryPurchaseContract.View
ViewModel:
class HistoryPurchaseViewModel #Inject constructor(override var session: HistoryPurchaseSession) : BaseViewModel<HistoryPurchaseEvent>(),
HistoryPurchaseContract.ViewModel
I managed to pass the viewModel however I'm unable to pass the fragment.
Is there a way I can do it?
I am new to Dagger 2 in android. I am having trouble understanding how to inject ViewModel with dynamic value. So Far I have successfully injected ViewModel using dagger multi binding with pre-defined repository dependency. Here's my code.
ApplicationComponent
#Singleton
#Component(modules = [AppModule::class, SubComponentsModule::class, ViewModelFactoryModule::class])
interface ApplicationComponent {
#Component.Factory
interface Factory {
fun create(#BindsInstance applicationContext: Context): ApplicationComponent
}
fun activityComponent(): ActivitySubComponent.Factory
fun fragmentComponent(): FragmentSubComponent.Factory
}
FragmentModule
#Module
abstract class FragmentModule {
#Binds
#IntoMap
#ViewModelKey(WeatherViewModel::class)
abstract fun bindWeatherView(weatherViewModel: WeatherViewModel) : ViewModel
}
ViewModelFactoryModule
#Module
class ViewModelFactoryModule {
#Provides
#Singleton
fun viewModelFactory(providerMap: Map<Class<out ViewModel>, Provider<ViewModel>>): ViewModelProvider.Factory {
return ViewModelFactory(providerMap)
}
}
Application class
class ThisApplication: Application(),InjectorProvider {
override fun onCreate() {
super.onCreate()
Stetho.initializeWithDefaults(this)
}
override val component by lazy {
DaggerApplicationComponent.factory().create(applicationContext)
}
}
I'm using InjectorProvider interface to get dagger to fragments and activity without having to cast every time.
InjectorProvider
interface InjectorProvider {
val component: ApplicationComponent
}
val Activity.injector get() = (application as InjectorProvider).component
val Fragment.injector get() = (requireActivity().application as InjectorProvider).component
This is the simple ViewModel I used for testing ViewModel injection.
WeatherViewModel
class WeatherViewModel #Inject constructor(val repository: WeatherRepository): ViewModel() {
fun printMessage(){
Log.d("WeatherViewModel","ViewModel binding is working")
repository.printMessage()
}
}
Finally, I Injected this view model into a fragment like below.
WeatherFragment
class WeatherFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelFactory
override fun onAttach(context: Context) {
injector.fragmentComponent().create().injectWeatherFragment(this)
super.onAttach(context)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val mainActivityViewModel =
ViewModelProvider(this,viewModelFactory)[WeatherViewModel::class.java]
mainActivityViewModel.printMessage()
}
}
This part is working fine. The Log message inside printMessage() getting printed. I saw in the dagger issue discussion that using AssistedInject is the best approach to handle this kind of scenario. I changed my ViewModle by adding a simple int value as a parameter.
Edited WeatherViewModel
class WeatherViewModel #AssistedInject constructor(val repository: WeatherRepository,
#Assisted val id: Int): ViewModel() {
#AssistedInject.Factory
interface Factory{ fun create(id: Int) : WeatherViewModel }
fun printMessage(){
Log.d("WeatherViewModel","ViewModel binding is working")
repository.printMessage()
}
}
Edited ApplicationComponent
#Singleton
#Component(modules = [AppModule::class, SubComponentsModule::class, ViewModelFactoryModule::class, AssistedInjectModule::class])
interface ApplicationComponent {
#Component.Factory
interface Factory {
fun create(#BindsInstance applicationContext: Context): ApplicationComponent
}
fun activityComponent(): ActivitySubComponent.Factory
fun fragmentComponent(): FragmentSubComponent.Factory
}
#AssistedModule
#Module(includes = [AssistedInject_AssistedInjectModule::class])
interface AssistedInjectModule
From this point onwards I don't understand how to inject ViewModel into fragment with repository plus dynamic "id" value. If I inject WeatherViewModel.Factory into the fragment by calling the create method (val mainActivityViewModel = factory.create(5)) it won't fulfill the repository dependency in ViewModel. How to combine these two solutions to have pre-defined repository dependency with dynamic value? OR is there any other better way of approaching this?
Not quite sure why your setup wont fulfill repository dependency by using create() method of factory. The repository dependency will be provided by Dagger's Acyclic Dependency Graph.
For example, below I'm saying to Dagger that I am responsible for providing SavedStateHandle and the NavigationDispatcher so don't even bother looking these up in your acyclic dependency graph.
class ProfileViewModel #AssistedInject constructor(
#Assisted val handle: SavedStateHandle,
#Assisted val navigationDispatcher: NavigationDispatcher,
private val eventTracker: EventTracker,
private val getUserUseCase: GetUserUseCase,
private val logOutUseCase: LogOutUseCase
) : ViewModel(), ProfileHandler {
#AssistedInject.Factory
interface Factory {
fun create(
handle: SavedStateHandle,
navigationDispatcher: NavigationDispatcher
): ProfileViewModel
}
In Fragment side, all I have to provide in the create method will be the dependencies i marked with #Assisted to fulfil my side of promise.
class ProfileFragment : Fragment() {
private val navigationDispatcher by getActivityViewModel {
getBaseComponent().navigationDispatcher
}
private val eventTracker by lazy {
getProfileComponent().eventTracker
}
private val viewModel by getViewModel { savedStateHandle ->
getProfileComponent().profileViewModelFactory.create(savedStateHandle, navigationDispatcher)
}
getViewModel is simply an extension function as follows:
inline fun <reified T : ViewModel> Fragment.getViewModel(crossinline provider: (handle: SavedStateHandle) -> T) =
viewModels<T> {
object : AbstractSavedStateViewModelFactory(this, arguments) {
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
) = provider(handle) as T
}
}
I was wondering how can I pass application dependency to ViewModel using Hilt?
I was trying with AndroidViewModel, but I couldn't make it. Can someone help me? Some short sample could will mean a lot to me.
This is my ViewModel:
class MainViewModel #ViewModelInject constructor(
private val application: Application,
private val repository: Repository,
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
This is my hilt module
#Module
#InstallIn(ApplicationComponent::class)
object DatabaseModule {
#Singleton
#Provides
fun provideDatabase(
#ApplicationContext context: Context
) = Room.databaseBuilder(
context,
MyDatabase::class.java,
"my_database"
).build()
#Singleton
#Provides
fun provideDao(database: MyDatabase) = database.myDao()
#Singleton
#Provides
fun provideRepository(myDao: MyDao) = Repository(myDao)
#Singleton
#Provides
fun provideApplicationContext() = MyApplication()
}
Everything else is fine, and I got the error message:
Caused by: java.lang.RuntimeException: Cannot create an instance of
class com.example.example.viewmodel.MainViewModel
Caused by: java.lang.InstantiationException:
java.lang.Class<com.example.example.viewmodel.MainViewModel> has
no zero argument constructor
You can see full source https://github.com/Kotlin-Android-Open-Source/MVI-Coroutines-Flow/tree/dagger_hilt
Repository:
#Singleton
class UserRepositoryImpl #Inject constructor(
private val userApiService: UserApiService,
private val dispatchers: CoroutineDispatchers,
...
) : UserRepository { ... }
Usecases:
class AddUserUseCase #Inject constructor(private val userRepository: UserRepository) {
suspend operator fun invoke(user: User) = userRepository.add(user)
}
class RemoveUserUseCase #Inject constructor(private val userRepository: UserRepository) {
suspend operator fun invoke(user: User) = userRepository.remove(user)
}
class RefreshGetUsersUseCase #Inject constructor(private val userRepository: UserRepository) {
suspend operator fun invoke() = userRepository.refresh()
}
...
ViewModel:
class MainVM #ViewModelInject constructor(
private val getUsersUseCase: GetUsersUseCase,
private val refreshGetUsers: RefreshGetUsersUseCase,
private val removeUser: RemoveUserUseCase,
) : ViewModel() { ... }
Activity:
#AndroidEntryPoint
class MainActivity : AppCompatActivity(), View {
private val mainVM by viewModels<MainVM>()
...
}
Edited:
To inject application context:
First, remove this definition, because Hilt already provides application context:
#Singleton
#Provides
fun provideApplicationContext() = MyApplication()
Second, Use #ApplicationContext annotation on your context parameter.
class MainViewModel #ViewModelInject constructor(
#ApplicationContext private val context: Context,
private val repository: Repository,
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
Use #ApplicationContext Context context as a parameter in the constructor.