I am using hilt for dependency injection, I can't find a way to get a viewmodel inside composable directly as we can't use #AndroidEntryPoint annotation with compose, it works fine when I passed it from an activity..
You can inject your viewModel like this
#Composable fun MainScreen(viewModel: HomeScreenViewModel = hiltViewModel())
You can see my sample project in this link for more information github
Something like this..
#HiltViewModel
class MyVm #Inject constructor(
private val someDep: SomeDep,
) : ViewModel() {}
My view - considering its fragment in this case
#AndroidEntryPoint
class MYFragment : Fragment() {
private val viewModel: MyVm by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setContent {
MYTheme {
MyTopLevelComposableScreen(
viewModel = viewModel
)
}
}
}
}
}
And my top level composable
#Composable
fun MyTopLevelComposableScreen(vm:MyVm){
}
or directly in the top level composable
#Composable
fun MyTopLevelComposableScreen(vm: MyVm = viewModel()){
}
https://developer.android.com/jetpack/compose/libraries#hilt
First, implementation this module:
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07")
And, you can use it like this:
#Composable
private fun MyLayout() {
val vm: MyViewModel = viewModel() // #HiltViewModel
...
[Android developer guideline]
Related
I have the following implementation on my Fragment -
class HeroesDetailsFragment : Fragment() {
private val navArgs: HeroesDetailsFragmentArgs by navArgs()
private val heroesDetailsViewModel: HeroesDetailsViewModel by stateViewModel(state = { navArgs.toBundle() })
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentHeroDetailsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initListeners()
observeUiState()
observeUiAction()
}
}
My HeroesDetailsViewModel looks like this -
class HeroesDetailsViewModel(
private val savedStateHandle: SavedStateHandle,
private val heroesDetailsRepository: HeroesDetailsRepository
) : ViewModel() {
private fun getArgsModel() = HeroesDetailsFragmentArgs.fromSavedStateHandle(savedStateHandle)
init {
val navArgs = getArgsModel()
getAdditionalHeroDetails(navArgs.heroModel.id)
observeUiEvents()
}
}
And in my ViewModelModule I declare the following
val viewModelModule = module {
// ...
viewModel { params ->
HeroesDetailsViewModel(params.get(), get())
}
}
As you can see, I utilized the stateViewModel extension for Fragments that allows me to create a StateViewModel. The issue is that when trying to use the same functionality in Compose:
#Destination
#Composable
fun HeroDetailsScreen(
model: HeroesListModel,
viewModel: HeroesDetailsViewModel = getStateViewModel() //provides deprecation error
) {
}
I get the following deprecation message -
getStateViewModel will be merged to sharedViewModel - no need anymore of state parameter
I did not find any good references on this topic, and it seems weird for me because the Fragment extension stateViewModel is completely fine and not deprecated so I am missing information on what should I do to replace it.
My goal is to inject a ViewModel with state parameters that will initialize the SavedStateHandle object. Currently I am using Koin DI, will switch in the future to Dagger-Hilt so it would be also a nice bonus to see the solution both in Koin and in Dagger-Hilt.
So I finally found a way to inject the ViewModel with dynamic information coming from the Fragment. I was looking at the old Fragment / Activity way which includes handling bundles, but in Compose it's much easier as we don't need to use the SavedStateHandle object because we can handle process death by the rememberSaveable { } block, which decouples the need to inject a ViewModel with dynamic information and the need to save information for process death.
This leaves the ViewModel to only ask for the relevant model and not bother handling process death. Just pure information.
class HeroesDetailsViewModel(
heroListModel : HeroesListModel,
private val heroesDetailsRepositoryImpl: HeroesDetailsRepositoryImpl
) : ViewModel() {
init {
getAdditionalHeroDetails(heroListModel.id)
observeUiEvents()
}
}
So I added a model that will be injected when needed via the parameters field -
#Destination
#Composable
fun HeroDetailsScreen(
model: HeroesListModel,
viewModel: HeroesDetailsViewModel = koinViewModel(parameters = { ParametersHolder(mutableListOf(model)) })
) {
}
And in my DI module the implementation actually is left the same -
val viewModelModule = module {
viewModelOf(::HeroesViewModel)
viewModelOf(::HeroesListItemViewModel)
viewModel { params ->
HeroesDetailsViewModel(params.get(), get())
}
}
Hopefully this saves some time for other people in the future 💪🙂
I am studying ViewModel to apply it to MVVM design pattern.
There was a method using by viemodels() and a method using ViewModelProvider.Factory in view model creation.
by viewModels() creates a ViewModel object.
ViewModelProvider.Factory also creates Viewmodel objects.
What is the difference between these two?
In addition, in some sample code, I saw the code in comment 3, which uses by viewModels() and factory together. What does this mean?
class WritingRoutineFragment : Fragment() {
private val viewModel: WriteRoutineViewModel by viewModels() // 1
private lateinit var viewModelFactory: WriteRoutineViewModelFactory
// private val viewModel: WriteRoutineViewModel by viewModels(
// factoryProducer = { viewModelFactory } // 3.What does this code mean?
// )
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
_binding = FragmentWritingRoutineBinding.inflate(inflater, container, false)
viewModelFactory = WriteRoutineViewModelFactory()
// viewModel = ViewModelProvider(this, viewModelFactory).get(WriteRoutineViewModel::class.java) // 2
return binding.root
}
If your ViewModel has a zero-argument constructor, or if it has a constructor where its only argument is of type Application and it's a subclass of AndroidViewModel, then you do not need a factory. (Or if your constructor is either of the above plus SavedStateHandle.) A view model factory is a class that is able to instantiate your ViewModel that has a more complicated constructor.
When instantiating your ViewModel without using a delegate, you have to use a lateinit var for the property because you can't instantiate it until onCreateView.
If your ViewModel had no need for a factory, the process of doing it without a delegate would look like this:
class WritingRoutineFragment : Fragment() {
private lateinit var viewModel: WriteRoutineViewModel
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
//...
viewModel = ViewModelProvider(this, viewModelFactory).get(WriteRoutineViewModel::class.java)
//...
}
}
and if it did need a factory, it would look like this, where you have to instantiate a factory and pass it to the ViewModelProvider constructor:
class WritingRoutineFragment : Fragment() {
private lateinit var viewModel: WriteRoutineViewModel
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
//...
viewModel = ViewModelProvider(this, WriteRoutineViewModelFactory()).get(WriteRoutineViewModel::class.java)
//...
}
}
The delegate allows you to do this more concisely in a val right at the declaration site so you don't have to do any setup of your view model property in onCreateView. It will lazily create the ViewModel the first time the property is used. The advantage is more concise and clearer code (lateinit var splits the property from its declaration and makes it mutable even though it will never change).
So the above code when no factory is needed looks like:
class WritingRoutineFragment : Fragment() {
private val viewModel: WriteRoutineViewModel by viewModels()
}
and if you do need a factory it will look like this. You pass it a function that instantiates the factory, which is easily done with a lambda:
class WritingRoutineFragment : Fragment() {
private val viewModel: WriteRoutineViewModel by viewModels { WriteRoutineViewModelFactory() }
}
The code in your example has an extra property just to hold the factory, which is an unnecessary complication since you'll never need to access it directly. It's also quite odd that the factory in your example has an empty constructor, because if the factory doesn't have any state, then it has no data to pass to the ViewModel constructor.
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.
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
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.