This is my viewModel
class ViewModel(private val savedStateHandle: SavedStateHandle, private val dataSource: DataSource) :ViewModel()
This is my viewModelProviderFactory
class ViewModelProviderFactory(
private val savedStateHandle: SavedStateHandle,
private val dataSource: DataSource
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ViewModel(savedStateHandle, dataSource) as T
}
}
In MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: ViewModel by viewModels()
...
I do not know how to get a savedStateHandle to pass to the factory so that I can create a viewModel.
You can use other create method of ViewModelProvider.Factory:
class ViewModelProviderFactory(
private val dataSource: DataSource
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
return ViewModel(extras.createSavedStateHandle(), dataSource) as T
}
}
And when initializing ViewModel by by viewModels() syntax, don't forget to pass the factory parameter:
private val viewModel: ViewModel by viewModels(factory = ViewModelProviderFactory(dataSource))
There is also another way to avoid the headache of creating ViewModelProvider.Factory that using hilt, you can refer this link to try hilt: https://dagger.dev/hilt/view-model
Related
I'm trying to send three values to my UserViewModel but even if sending the savedStateHandle
In my Activity I have
private val viewModel: UserViewModel by viewModels()
Then my UserViewModel is :
#HiltViewModel
internal class UserViewModel #Inject constructor(
private val myRepo: MyRepo,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
But then this savedStateHandle is empty, what I'm missing?
If you are using MVVM based on the Android Architecture guidelines you can send an event to the viewmodel from your Activity/Fragment once your view is created.
You must add savedStateHandle in AppModule. You want inject savedStateHandle.
I've been using #AssistedInject to do so as follows :
internal class UserViewModel #AssistedInject constructor(
...
#Assisted val name: String,
) : ViewModel() {
...
}
Then I had to create a Factory
#AssistedFactory
interface UserViewModelAssistedFactory {
fun create(name: String): UserViewModel
}
class Factory(
private val assistedFactory: UserViewModelAssistedFactory,
private val name: String, <-- value you want to pass
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return assistedFactory.create(name) as T
}
}
Then in the Activity/Fragment you have to inject the AssistedFactory as follows
#Inject internal lateinit var assistedFactory: UserViewModel.UserViewModelAssistedFactory
private val userViewModel: UserViewModel by viewModels {
UserViewModel.Factory(assistedFactory, intent.getStringExtra(USER_NAME_ARG).orEmpty())
}
Doing this it should work, but also your solution should work make sure you are sending the intent args correctly because it says is null looks like what you are passing is not correct, savedInstace.keys() should return everything you passed from your previous Activity/Fragment.
Please help. I have single activity multi modular app, how can i instantiate same view model with factory and dagger 2 in feature modules? This is my viewmodel assisted factory:
class HomeViewModelFactory #AssistedInject constructor(
private val homeRepository: HomeRepositoryImpl,
private val cartRepository: CartRepositoryImpl,
#Assisted owner: SavedStateRegistryOwner
) : AbstractSavedStateViewModelFactory(owner, null) {
override fun <T : ViewModel> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T = HomeViewModel(homeRepository, cartRepository, handle) as T
}
#AssistedFactory
interface HomeViewModelAssistedFactory {
fun create(owner: SavedStateRegistryOwner): HomeViewModelFactory
}
And i instantiate it in a fragment like this
#Inject
lateinit var homeAssistedFactory: HomeViewModelAssistedFactory
private lateinit var homeViewModel: HomeViewModel
private fun provideViewModel() {
val viewModelFactory = homeAssistedFactory.create(requireActivity())
homeViewModel =
ViewModelProvider(requireActivity(), viewModelFactory)[HomeViewModel::class.java]
}
How can i create same viewModelFactory across different fragments?
I found no information regarding this issue or i simply don't recognize the right answer.
Our app follows clean architecture so in the domain layer we have an Interface and a use-case like this:
interface MyRepository {
suspend fun doSomething(): Result<String>
}
class MyUseCase #Inject constructor(private val repository: MyRepository) {
// implementation
}
In the data layer, we have an implementation of the MyRepository for example called MyRepositoryImpl. And we bind the repository like this:
#Binds
abstract fun binMyRepostory(repository: MyRepositoryImpl): MyRepository
And we use #AssistedInject in our view model like this:
class MyViewModel #AssistedInject constructor(
private val myUseCase: MyUseCase,
#Assisted private var id: Int
) : ViewModel() {
#AssistedFactory
interface Factory {
fun create(id: Int): MyViewModel
}
companion object {
fun provideFactory(
assistedFactory: Factory,
id: Int
): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return assistedFactory.create(id) as T
}
}
}
}
Running the app shows this error but in a view model without AssistedInject it works properly.
It error: [Dagger/MissingBinding] package.MyRepository cannot be provided without an #Provides-annotated method.
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
}
Using #Assisted annotation with savedStateHandle and by viewModels() it's possible to inject SavedStateHandle object to ViewModel in modules that are not dynamic feature modules with dagger hilt as
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: MainActivityViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
class MainActivityViewModel #ViewModelInject constructor(
#Assisted savedStateHandle: SavedStateHandle
) : ViewModel() {
val stringData = savedStateHandle.getLiveData<String>("hello_world")
}
but it's not possible for dynamic feature modules to do like this. How is it done with dynamic feature module ViewModels?
My ViewModel is
class DashboardViewModel #ViewModelInject constructor(
#Assisted private val savedStateHandle: SavedStateHandle,
private val coroutineScope: CoroutineScope,
private val dashboardStatsUseCase: GetDashboardStatsUseCase,
private val setPropertyStatsUseCase: SetPropertyStatsUseCase
) : ViewModel() {
}
Creating generic FragmentFactory for SavedStateHandle with
interface ViewModelFactory<out V : ViewModel> {
fun create(handle: SavedStateHandle): V
}
class GenericSavedStateViewModelFactory<out V : ViewModel>(
private val viewModelFactory: ViewModelFactory<V>,
owner: SavedStateRegistryOwner,
defaultArgs: Bundle? = null
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T {
return viewModelFactory.create(handle) as T
}
}
/**
* Convenience function to use with `by viewModels` that creates an instance of
* [AbstractSavedStateViewModelFactory] that enables us to pass [SavedStateHandle]
* to the [ViewModel]'s constructor.
*
* #param factory instance of [ViewModelFactory] that will be used to construct the [ViewModel]
* #param owner instance of Fragment or Activity that owns the [ViewModel]
* #param defaultArgs Bundle with default values to populate the [SavedStateHandle]
*
* #see ViewModelFactory
*/
#MainThread
inline fun <reified VM : ViewModel> SavedStateRegistryOwner.withFactory(
factory: ViewModelFactory<VM>,
defaultArgs: Bundle? = null
) = GenericSavedStateViewModelFactory(factory, this, defaultArgs)
ViewModel factory for ViewModel
class DashboardViewModelFactory #Inject constructor(
private val coroutineScope: CoroutineScope,
private val dashboardStatsUseCase: GetDashboardStatsUseCase,
private val setPropertyStatsUseCase: SetPropertyStatsUseCase
) : ViewModelFactory<DashboardViewModel> {
override fun create(handle: SavedStateHandle): DashboardViewModel {
return DashboardViewModel(
handle,
coroutineScope,
dashboardStatsUseCase,
setPropertyStatsUseCase
)
}
}
And creating ViewModel using the DashBoardViewModelFactory in Fragment as
#Inject
lateinit var dashboardViewModelFactory: DashboardViewModelFactory
private val viewModel: DashboardViewModel
by viewModels { withFactory(dashboardViewModelFactory) }
Here you can see the full implementation in action. I wasn't able to find the source i used to implement this solution, if you can comment the link, i would like to give credit to author.