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.
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.
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
I using hilt to inject everything I want in viewModel, I find hilt support SavedStateHandle through #ViewModelInject, so any bundle data pass to it can be get back if I want.
class TestViewModel #ViewModelInject constructor(
private val testRepository: TestRepository,
#Assisted private val state: SavedStateHandle
) : ViewModel() {
val testItem = state["test"] ?: "defaultValue"
}
#AndroidEntryPoint
class TestFragment : Fragment() {
private val viewModel: TestViewModel by viewModels() // How to pass bundle to the init viewModel?
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentTestBinding.inflate(inflater)
binding.lifecycleOwner = this
binding.viewModel = viewModel
...
}
}
It seems like the way to use ViewModelFactory to init viewModel with bundle.
interface ViewModelAssistedFactory<T : ViewModel> {
fun create(state: SavedStateHandle): T
}
class TestViewModelFactory #Inject constructor(
private val testRepository: TestRepository
) : ViewModelAssistedFactory<TestViewModel> {
fun create(handle: SavedStateHandle) {
return TestViewModel(handle, testRepository)
}
}
class TestViewModel(
private val state: SavedStateHandle
private val testRepository: TestRepository,
) : ViewModel() {
val id = state["test"] ?: "defaultValue"
}
If I understand your question correctly, that you want in instantiate the ViewModel and pass in a bundle I suspect that Injecting ViewModel with Dagger Hilt article might help.
Scrolling towards the bottom there are 2 example using varying methods to instance a ViewModel and one in particular is passing a Bundle.
This is another good resource: No more Factory Needed for ViewModel with Dependencies.
This is what I did, maybe not elegant, but it works well:
class dataViewModel #ViewModelInject constructor(
val stateData: StateData,
#Assisted private val savedStateHandle: SavedStateHandle
): ViewModel() {
Then, to instance the viewModel you need a module definition:
#Module
#InstallIn(ApplicationComponent::class)
class StateDataModule {
#Singleton
#Provides
fun provideStateData(): StateData {
return StateData(null, null)
}
}
Then, in classes that use it I set the values and the ViewModel retains the values for all instances and across activities (which was my primary reason to do this).
dataViewModel.stateData.[property] = blah
Trying to create ViewModel in a dynamic feature module with private val viewModel: PostDetailViewModel by viewModels()
in fragment
class PostDetailFragment : DynamicNavigationFragment<FragmentPostDetailBinding>() {
private val viewModel: PostDetailViewModel by viewModels()
override fun getLayoutRes(): Int = R.layout.fragment_post_detail
override fun bindViews() {
// Get Post from navigation component arguments
val post = arguments?.get("post") as Post
dataBinding.item = post
viewModel.updatePostStatus(post)
}
override fun onCreate(savedInstanceState: Bundle?) {
initCoreDependentInjection()
super.onCreate(savedInstanceState)
}
private fun initCoreDependentInjection() {
val coreModuleDependencies = EntryPointAccessors.fromApplication(
requireActivity().applicationContext,
DomainModuleDependencies::class.java
)
DaggerPostDetailComponent.factory().create(
coreModuleDependencies,
requireActivity().application
)
.inject(this)
}
}
results error
Caused by: java.lang.InstantiationException: java.lang.Class<com.x.post_detail.PostDetailViewModel> has no zero argument constructor
it works in any fragment in app module but not working in dynamic feature modules. What's the proper way to add ViewModels to dynamic feature modules? Should i create ViewModels in app module with a ViewModelFactory and get them from app module?
Based on this official github posts
There's documentation on Hilt and DFM now at
https://developer.android.com/training/dependency-injection/hilt-multi-module#dfm
In general though, because we're built off of subcomponents and
monolithic components you won't be able to use the standard Hilt
mechanisms like #AndroidEntryPoint with DFM.
Unfortunately, no. #ViewModelInject uses the Hilt
ActivityRetainedComponent which is monolithic, so any #ViewModelInject
class in your DFM won't be recognized.
it seems that injecting to a ViewModel only with #ViewModelInject and by viewModels() in a dynamic feature module is not possible as of now.
Based on plaid app i rebuilt my Dagger module in dynamic feature module as
#InstallIn(FragmentComponent::class)
#Module
class PostDetailModule {
#Provides
fun providePostDetailViewModel(fragment: Fragment, factory: PostDetailViewModelFactory) =
ViewModelProvider(fragment, factory).get(PostDetailViewModel::class.java)
#Provides
fun provideCoroutineScope() = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
}
And ViewModel and ViewModelFactory are
class PostDetailViewModel #ViewModelInject constructor(
private val coroutineScope: CoroutineScope,
private val getPostsUseCase: UseCase
) : ViewModel() {
// Do other things
}
class PostDetailViewModelFactory #Inject constructor(
private val coroutineScope: CoroutineScope,
private val getPostsUseCase: UseCase
) : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass != PostDetailViewModel::class.java) {
throw IllegalArgumentException("Unknown ViewModel class")
}
return PostDetailViewModel(
coroutineScope,
getPostsUseCase
) as T
}
}
And injected to fragment in dynamic feature module
class PostDetailFragment : Fragment() {
#Inject
lateinit var viewModel: PostDetailViewModel
override fun onCreate(savedInstanceState: Bundle?) {
initCoreDependentInjection()
super.onCreate(savedInstanceState)
}
private fun initCoreDependentInjection() {
val coreModuleDependencies = EntryPointAccessors.fromApplication(
requireActivity().applicationContext,
DomainModuleDependencies::class.java
)
DaggerPostDetailComponent.factory().create(
dependentModule = coreModuleDependencies,
fragment = this
)
.inject(this)
}
}