fragment how to pass bundle to viewModel by ViewModelInject - android

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

Related

How to instantiate same viewModelFactory across different feature modules in single activity multi modular app with dagger?

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.

How to access a function from a viewModel in another viewModel

I have 2 viewModels -
MainViewModel**
StorageViewModel
StorageViewModel.kt
class StorageViewModel #ViewModelInject constructor(private val preferenceStorage:
PreferenceStorage, #ApplicationContext context: Context) : ViewModel() {
........
//save last played song
fun saveLastPlayedSong(song: Songs){
viewModelScope.launch {
protoDataStoreManager.saveLastPlayedSong(song)
}
}
}
Now, I want to call the saveLastPlayedSong function in MainViewModel
MainViewModel.kt
class MainViewModel #ViewModelInject constructor(
private val musicServiceConnection: MusicServiceConnection,
private val storageViewModel: StorageViewModel
) : ViewModel(){
.........
fun playOrToggleSong(
mediaItem: Songs, toggle: Boolean = false
)
{
//here, I want to call the function from StorageViewModel e.g
storageViewModel.saveLastPlayedSong(mediaItem)
}
}
How do I instantiate the "StorageViewModel" inside MainViewModel and whats the best way (Good Practice).
I'm using MVVM and Hilt.
This is usually a symptom of bad architecture.
If StorageViewModel is acting like a Repository it should not extend ViewModel. If it doesn't have connections to UI you can convert it to a repository class and that would solve your problem because it would just become an injectable singleton.
If StorageViewModel is connected to a Fragment (for example) you should take a reference to both viewmodels and pass data between them from the UI layer.
Something like:
class StorageFragment : Fragment {
private val storageViewModel: StorageViewModel by viewModels()
private val mainActivityViewModel: MainViewModel by activityViewModels()
//....
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
//you can do this if the song saving is a UI related thing
//just have playOrToggleSong accept a function as parameter
//as success callback
button.setOnClickListener {
mainActivityViewModel.playOrToggleSong(...) {
storageViewModel.saveLastPlayedSong(param)
}
}
}
}

How to use viewmodel + room with fragments?

I'm looking through the tutorial for Android room with a view, and trying to extend their model for using ViewModels to multiple fragments, but not really sure how.
MyApplication
class myApplication : Application() {
companion object {
var database: myDatabase? = null
var repository: myRepository? = null
}
override fun onCreate() {
super.onCreate()
database = MyDatabase.getInstance(this)
repository = MyRepository(database!!.myDatabaseDao)
}
}
MyViewModel
class MyViewModel(private val repository: MyRepository) : ViewModel() {
val allWords: LiveData<List<Words>> = repository.allWords.asLiveData()
fun insert(word: Word) = viewModelScope.launch {
repository.insert(word)
}
}
class MyViewModelFactory(private val repository: MyRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return MyViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
HomeFragment
class HomeFragment : Fragment() {
private val myViewModel: MyViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
var rootView = inflater.inflate(R.layout.fragment_home, container, false)
return rootView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
myViewModel.allWords.observe(viewLifecycleOwner) { words ->
// Update the cached copy of the words in the adapter.
words.let { Log.d("fragment", it.toString()) }
}
}
}
I have a couple of other fragments that will hopefully share the same ViewModel as HomeFragment. I've tried many different approaches, such as using
myViewModel = ViewModelProviders.of(activity!!).get(MyViewModel::class.java)
but all of them give me Caused by: java.lang.InstantiationException: java.lang.Class<com.example.tabtester.ViewModels.MyViewModel> has no zero argument constructor. I can't find any SO posts or documentation that shows me how to provide a constructor in Kotlin.
Also conceptually I can't find any description for what exactly is happening and how the viewmodel is being constructed (and by what). In the Room with a View tutorial, the example given is in MainActivity:
private val wordViewModel: WordViewModel by viewModels {
WordViewModelFactory((application as WordsApplication).repository)
}
This makes sense, to me; you're using the Factory to instantiate a ViewModel to use in the MainActivity. But for any description of how to use ViewModels in Fragments, I don't see where the ViewModel is being constructed. If you have multiple fragments who is constructing the ViewModel? If I use Fragments then does that mean I also need an activity to construct the ViewModel, then somehow share between the Fragments?
Would appreciate any help, or documentation that explains this more clearly.
The underlying APIs of by viewModels(), by activityViewModels() and the (now deprecated) ViewModelProviders.of() all feed into one method: the ViewModelProvider constructor:
ViewModelProvider(viewModelStore: ViewModelStore, factory: ViewModelProvider.Factory)
This constructor takes two parameters:
The ViewModelStore controls the storage and scoping of the ViewModel you create. For example, when you use by viewModels() in a Fragment, it is the Fragment which is used as the ViewModelStore. Similarly, by activityViewModels() uses the Activity as the ViewModelStore.
The ViewModelProvider.Factory controls the construction of the ViewModel if one has not already been created for that particular ViewModelStore.
Therefore if you need a custom Factory, you must always pass that Factory into all places that could create that ViewModel (remember, due to process death and recreation, there's no guarantee that your HomeFragment will be the first fragment to create your ViewModel).
private val myViewModel: MyViewModel by activityViewModels() {
MyViewModelFactory(MyApplication.repository!!)
}
As long as you're using activityViewModels(), the storage of your ViewModel will always be at the activity level, no matter what Factory you are using.

How to inject SavedStateHandle to ViewModel in dynamic feature module?

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.

How to use hilt instead of using factory to init the argument in viewModel

Now I using the viewModelFactory to init the argument for viewModel in fragment.
class MyFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val binding = FragmentMyBinding.inflate(inflater)
binding.lifecycleOwner = this
val argument = MyFragmentArgs.fromBundle(requireArgument()).myArgument
val viewModelFactory = MyViewModelFactory(myArgument, application)
binding.viewModel = ViewModelProvider(
this, viewModelFactory).get(MyViewModel::class.java)
return binding.root
}
}
class MyViewModelFactory(
private val myArgument: MyArgument,
private val application: Application) : ViewModelProvider.Factory {
#Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
return MyViewModel(myArgument, application) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Compare to the hilt dependency inject way, is there a way to pass argument to viewModel directly?
Yes. With Hilt, you can completely ditch the factory pattern.
To inject a param, you can use #ViewModelInject annotation, and to inject the viewModel in the activity you can use the by viewModels() method from androidx.activity package.
Here's an example.
ProductsViewModel.kt
import androidx.hilt.lifecycle.ViewModelInject
class ProductsViewModel #ViewModelInject constructor(
private val foo: Foo
private val bar: Bar
) : ViewModel()
ProductsActivity.kt
#AndroidEntryPoint
class ProductsActivity : AppCompatActivity(){
val viewModel: ProductsViewModel by viewModels()
}
To see this in action, you can checkout this repo.
You can do it using Hilt library. You should first define your view model by #HiltViewModel and use constructor injection(#Inject for constructor of your view model):
#HiltViewModel
class MainViewModel #Inject constructor(val foo: Foo) : ViewModel()
My Foo class is:
class Foo #Inject constructor(val someDependency: Dependency)
If you have any other class which you need to inject to your ViewModel, you can add it to your hilt module.
Finally, you should instantiate your ViewModel in your activity/fragment:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
val mainViewModel: MainViewModel by viewModels()
}
Please note that we are talking only about build time dependencies. For runtime dependencies with Hilt, you still have to stick to a classic ViewModelProvider.Factory since #AssistedInject is not yet supported by it.

Categories

Resources