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.
Related
I am using Hilt. In my ViewModel, I have EventLogger class and a repository class, which are throwing me an error while being built, so build is unsuccessful. The code is as follows,
#HiltViewModel
class ConversionViewModel #Inject constructor(
private val preConversionUseCase : PreConversionUseCase,
private val conversionUseCase: ConversionUseCase,
private val conversionSmsUseCase: ConversionSmsUseCase,
private val currencyRepository: MyBankRepository,
private val logger: EventLogger
) : ViewModel() { ... }
class EvenLogger #Inject constructor(
#ActivityContext val context: Context
){ ... }
class MyBankRepository #Inject constructor(
#ActivityContext private val module: ApiModule
): ApiRequest() {
val api = module.provideRetrofit()
}
I am getting the following error:
I think you can't inject the ActivityContext into an ApiModule. You need to add an Context-type param to the constructor of your MyBankRepository, I suppose:
class MyBankRepository #Inject constructor (
#ActivityContext
val context: Context,
private val module: ApiModule
) : ApiRequest() { ...
But this does unfortunately not work for ViewModels as described here:
https://github.com/google/dagger/issues/2698#issuecomment-862516150
The Repository and the EventLogger are both children of a ViewModel.
So it looks like you have to chance your architecture a little bit. You shouldn't implement too much logic into an ViewModel.
Trying to create a view model in a fragment by providing the factory with a dagger but app crashes with error. Move injection to another lifecycle doesn't work
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.bigproject.planyourlife, PID: 13934
kotlin.UninitializedPropertyAccessException: lateinit property factory has not been initialized
at com.bigproject.planyourlife.view.SignupFragment.getFactory(SignupFragment.kt:27)
ViewModel
class SignupViewModel(private val service: AuthService) : ViewModel() {
private var email: String = ""
private var password: String = ""
private val _uiState = MutableStateFlow<SignupState>(SignupState.Success(null))
val uiState: StateFlow<SignupState> = _uiState
#Suppress("UNCHECKED_CAST")
class Factory #Inject constructor(
private val service: AuthService
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
require(modelClass == SignupViewModel::class)
return SignupViewModel(service) as T
}
}
Fragment
class SignupFragment : Fragment(R.layout.signup_page) {
private val binding by viewBinding(SignupPageBinding::bind)
private val signupViewModel: SignupViewModel by viewModels {
factory
}
#Inject
lateinit var factory: SignupViewModel.Factory
override fun onAttach(context: Context) {
context.appComponent.inject(this)
super.onAttach(context)
}
}
First, create a module for your ViewModelFactory. This is important for you to provide ViewModelFactory for dagger.
ViewModelModule.kt
#Module
abstract class ViewModelModule {
#Binds
abstract fun provideViewModelFactory(factory: Factory): ViewModelProvider.Factory
// Binds your viewmodel in here.
}
Next, you need to include that module in your component. Specifically here is AppComponent.
AppComponent.kt
#Singleton
#Component(modules = [ViewModelModule::class, YourFragmentModule::class, YourActivityModule::class, ...])
interface AppComponent {
#Component.Factory
abstract class Factory : AndroidInjector.Factory<YourApplication>
}
And now, viewModelFactory is ready for you to inject into your Fragment, Activity, ....
YourFragment.kt
class YourFragment: Fragment() {
#Inject
lateinit var viewModelFactory: Factory
}
I have
BeatPlayer.kt
interface BeatPlayer {
fun getSession(): MediaSessionCompat
fun playSong(extras: Bundle = bundleOf(BY_UI_KEY to true))
fun playSong(id: Long)
fun playSong(song: Song)
}
class BeatPlayerImplementation(
private val context: Application,
private val musicPlayer: AudioPlayer,
private val songsRepository: SongsRepository,
private val queueUtils: QueueUtils,
private val audioFocusHelper: AudioFocusHelper
) : BeatPlayer {
.........
}
MusicService.kt
#AndroidEntryPoint
class MusicService : CoroutineService(Main) {
#Inject
lateinit var beatPlayer: BeatPlayer
}
When I run it says:
[Dagger/MissingBinding] BeatPlayer cannot be provided without an #Provides-annotated method.
So I added this:
#Module
#InstallIn(SingletonComponent::class)
abstract class StorageModule {
#Singleton
#Binds
abstract fun bindBeatPlayer(beatPlayer: BeatPlayer): BeatPlayerImplementation
}
Now, I run, it says:
error: #Binds methods' parameter type must be assignable to the return type hilt
How to do it properly?
I will answer my question based on comment from #HenryTest.
StorageModule.kt
#Module
#InstallIn(SingletonComponent::class)
abstract class StorageModule {
#Binds
abstract fun bindsPreferenceStorage(preferenceStorageImpl:
PreferenceStorageImpl): PreferenceStorage
#Singleton
#Binds
abstract fun bindBeatPlayer(beatPlayer: BeatPlayerImplementation):
BeatPlayer
}
Now Provide BeatPlayerImplementation.
AppModule.kt
#Singleton
#Provides
fun providesBeatPlayerImplementation(#ApplicationContext context:
Context.,.,.,.) =
BeatPlayerImplementation(
context, .., .., ..,
)
OR
In BeatPlayerImplementation
class BeatPlayerImplementation #Inject constructor(
#ApplicationContext private val context: Application,
.....
) : BeatPlayer {
.........
}
My ViewModel:
class LoginViewModel #ViewModelInject constructor(
private val loginUseCase: LoginUseCase
) : ViewModel() {
val currentResult: MutableLiveData<String> by lazy {
MutableLiveData<String>()
}
fun loginUseCase(username: String, password: String) {
viewModelScope.launch {
loginUseCase.invoke(username, password).apiKey.let {
currentResult.value = it
}
}
}
}
Is being used by my MainActivity:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val loginViewModel: LoginViewModel by viewModels()
And I know that the ViewModelProvider is expecting a empty constructor but I need to use the LoginUseCase:
class LoginUseCase #Inject constructor(
private val apiService: ApiServiceImpl
) : UseCase<Unit>() {
suspend operator fun invoke(username: String, password: String) =
apiService.login(username, password)
}
Inside the modelView, but i get the error:
Cannot create an instance of class com.example.myboards.ui.login.LoginViewModel
in runtime, and I dont know how I could manage the LoginUseCase inside the LoginViewModel
Provide a ViewModel by annotating it with #HiltViewModel and using the #Inject annotation in the ViewModel object's constructor.
#HiltViewModel
class LoginViewModel #Inject constructor(
private val loginUseCase: LoginUseCase
) : ViewModel() {
...
}
Hilt needs to know how to provide instances of ApiServiceImpl, too. Read here to know how to inject interface instances with #Binds.
Let me know If you still have a problem.
#Module
#InstallIn(ApplicationComponent::class)
object AppDataModule {
#Provides
#Singleton
fun provideAU(loginPreferences: LoginPreferences, #ApplicationContext context: Context): AccountUtil = AccountUtil (loginPreferences, context)
}
class SomeClass {
#Inject
lateinit var accountUtil: AccountUtil
constructor(context:Context){}
constructor(context:Context, obj:SomeClass1){}
init {
accountUtil.isLoggedIn()
}
}
I got
kotlin.UninitializedPropertyAccessException: lateinit property accountUtil has not been initialized
How can i resolve this one? is i need to use custom component in Hilt?
Make sure you have your application as a #HiltApplication
#HiltAndroidApp class Application: Application()
Then once you've created a module like so
#Module
#InstallIn(ApplicationComponent::class)
object AppDataModule {
#Provides
#Singleton
fun provideAU(loginPreferences: LoginPreferences, #ApplicationContext context: Context): AccountUtil = AccountUtil (loginPreferences, context)
}
Make sure the class you're wanting to inject is injected in the constructor
class SomeClass #Inject constructor(private var accountUtil: AccountUtil) {
init { accountUtil.isLoggedIn() }
}
class SomeClass #Inject constructor(){
#Inject constructor(private var accountUtil: AccountUtil)
#Inject constructor(private var util1: AccountUtil, private var util2: AccountUtil)
init { accountUtil.isLoggedIn() }
}