I am trying to get a view model in two places, one in the MainActivity using:
val viewModel:MyViewModel by viewModels()
The Other place is inside a compose function using:
val viewModel:MyViewModel = hiltViewModel()
When I debug, it seems that those are two different objects. Is there anyway where I can get the same object in two places ?
Even though you solved your issue without needing the view model, the question remained unanswered so I am posting this in case someone else finds it helpful.
This answer explains how view model scopes are shared and how you can override it.
In case you are using Navigation component, this should help. However, if you don't want to pass down view models or override the provided ViewModelStoreOwners, you can access the parent activity's view model in any child composable like below.
val composeView = LocalView.current
val activityViewModel = composeView.findViewTreeViewModelStoreOwner()?.let {
hiltViewModel<MyViewModel>(it)
}
Related
I have a single activity app using only composables for the ui (one activity, no fragments). I use one viewmodel to keep data for the ui in two different screens (composables). I create the viewmodel in both screens as described in state documentation
#Composable
fun HelloScreen(helloViewModel: HelloViewModel = viewModel())
Now I noticed that the data that was loaded or set in the first screen is reset in the second.
I also noticed that init{} is called every time viewModel() is called. Is this really the expected behavior?
According to the method's documentation it should return either an existing ViewModel or create a new one.
I also see that the view models are different objects. So viewModel() always creates a new one. But why?
Any ideas what I could be doing wrong? Or do I misunderstand the usage of the method?
Usually view model is shared for the whole composables scope, and init shouldn't be called more than once.
But if you're using compose navigation, it creates a new model store owner for each destination. If you need to share models between destination, you can do it like in two ways:
By passing it directly to viewModel call. In this case only the passed view model will be bind to parent store owner, and all other view models created inside will be bind(and so destroyed when route is removed from the stack) to current route.
By proving value for LocalViewModelStoreOwner, so all composables inside will be bind to the parent view model store owner, and so are not gonna be freed when route is removed from the stack.
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "first") {
composable("first") {
val model = viewModel<Model>(viewModelStoreOwner = viewModelStoreOwner)
}
composable("second") {
CompositionLocalProvider(
LocalViewModelStoreOwner provides viewModelStoreOwner
) {
val model = viewModel<Model>()
}
}
}
I have read the article,
It seems that State<T> is designed for #Composable.
Is it better to use State<T> in other classes such as ViewModel?
Yes, being part of the androidx.compose.runtime package State<T> was indeed intended as a value holder for composables.
If you want to publish/emit and consume "states" within ViewModels or Composables you might want to take a look at StateFlow and SharedFlow
You can either collect those as you would with any kotlin Flow<T> and use collectAsState within compose functions.
#Composable
fun YourComposable() {
val myState by viewModel.stats.collectAsState()
}
States trigger recomposable, for each screen I've always used custom data class (if it's necessary) and wrap it inside mutableStateOf(YourDataClass()) and place it in ViewModel just like we always use LiveData. And in your screen (composable) you can just val yourState = viewModel.yourState.value.
For a complete example
// ViewModel
private val _yourState: MutableState<AnimeTopState> = mutableStateOf(YourState())
val yourState: State<YourState> = _yourState
// ViewModel
// Composable
val yourState = viewModel.yourState.value
// Composable
So, state is like the way to trigger view changes on #Composable function, we cant just trigger view change with LiveData or normal value like the way we used to with XML view.
I am learning Jetpack compose, and I have been seen so far that lifting the state up to a composable's caller to make a composable stateless is the way to go. I`ve been using this pattern in my Compose apps.
For an app state that I need to modify from many different places of the tree, I will have to pass around a lot of callbacks, This can become difficult to manage.
I have some previous experience with Flutter. The way Flutter deals with providing a state to its descendants in the tree to overcome the above, is to use other mechanisms to manage state, namely Provider + ChangeNotifier.
Basically, with Provider, a Provider Widget is placed in the tree and all the children of the provider will have access to the values exposed by it.
Are there any other mechanisms to manage state in Jetpack Compose apps, besides State hoisting? And, what would you recommend?
If you need to share some data between views you can use view models.
#Composable
fun TestScreen() {
val viewModel = viewModel<SomeViewModel>()
Column {
Text("TestScreen text: ${viewModel.state}")
OtherView()
}
}
#Composable
fun OtherView() {
val viewModel = viewModel<SomeViewModel>()
Text("OtherScreen text: ${viewModel.state}")
}
class SomeViewModel: ViewModel() {
var state by mutableStateOf(UUID.randomUUID().toString())
}
The hierarchy topmost viewModel call creates a view model - in my case inside TestScreen. All children that call viewModel of the same class will get the same object. The exception to this is different destinations of Compose Navigation, see how to handle this case in this answer.
You can update a mutable state property of view model, and it will be reflected on all views using that model. Check out more about state in Compose.
The view model lifecycle is bound to the compose navigation route (if there is one) or to Activity / Fragment otherwise, depending on where setContent was called from.
I have a single activity app using only composables for the ui (one activity, no fragments). I use one viewmodel to keep data for the ui in two different screens (composables). I create the viewmodel in both screens as described in state documentation
#Composable
fun HelloScreen(helloViewModel: HelloViewModel = viewModel())
Now I noticed that the data that was loaded or set in the first screen is reset in the second.
I also noticed that init{} is called every time viewModel() is called. Is this really the expected behavior?
According to the method's documentation it should return either an existing ViewModel or create a new one.
I also see that the view models are different objects. So viewModel() always creates a new one. But why?
Any ideas what I could be doing wrong? Or do I misunderstand the usage of the method?
Usually view model is shared for the whole composables scope, and init shouldn't be called more than once.
But if you're using compose navigation, it creates a new model store owner for each destination. If you need to share models between destination, you can do it like in two ways:
By passing it directly to viewModel call. In this case only the passed view model will be bind to parent store owner, and all other view models created inside will be bind(and so destroyed when route is removed from the stack) to current route.
By proving value for LocalViewModelStoreOwner, so all composables inside will be bind to the parent view model store owner, and so are not gonna be freed when route is removed from the stack.
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "first") {
composable("first") {
val model = viewModel<Model>(viewModelStoreOwner = viewModelStoreOwner)
}
composable("second") {
CompositionLocalProvider(
LocalViewModelStoreOwner provides viewModelStoreOwner
) {
val model = viewModel<Model>()
}
}
}
I have a ViewModel that takes a string as an argument
class ComplimentIdeasViewModel(ideaCategory : String) : ViewModel() {
//some code here
}
What is the best way to initiate this ViewModel inside a composable fun without using a ViewModel factory and Hilt? A simple statement seems to achieve this inside a composable fun
#Composable
fun SampleComposableFun() {
val compIdeasViewModel = remember { ComplimentIdeasViewModel("someCategory") }
}
There is no warning in Android studio when I try to do this, but this seems too easy to be true, I am able to do this without Dependency Injection and with a ViewModelFactory class. Am I missing something here?
I've tried how you have written yours out and I had issues with screen rotation resetting the view model. I suspect you may too.
I was able to fix it by utilizing the the factory parameter on viewModel() for this, which worked well for me. See this answer on a similar question with example on how to use it: jetpack compose pass parameter to viewModel
This will not provide you the correct instance if viewmodel. See if you store some state in the viewmodel, then using the factory to initialise it is necessary to ensure that you get the same and latest copy of the viewmodel currently present. There is no error since the syntactic implementation is correct. I do not know of any way to do this because most of the times, you don't need to. Why don't you initialise it in the top-level container, like the activity? Then pass it down wherever necessary.
Create a CompositionLocal for your ViewModel.
val YourViewModel = compositionLocalOf { YourViewModel(...) }
Then initialise it (You'd likely use the ViewModelProvider.Factory here). And then provide that to your app.
CompositionLocalProvider(
YourViewModel provides yourInitialisedViewModel,
) {
YourApp()
}
Then reference it in the composable.
#Composable
fun SampleComposableFun(
compIdeasViewModel = YourViewModel.current
) {
...
}
Note, the docs say that ViewModels are not a good fit for CompositionLocals because they will make your composable harder to test, make your composables tied to this app and make it harder to use #Preview.
Some get pretty angry about this. However, if you manage to mock out the ViewModel, so you can test the app and use #Preview and your composables are tied to the app and not generic, then I see no problem.
You can mock a ViewModel fairly simply, providing its dependencies are included as parameters (which is good practice anyway).
open class MockedViewModel : MyViewModel(
app = Application(),
someOtherDependeny = MockedDependecy(),
)
The more dependencies your ViewModel has the more mocking you'll need to do. But I've not found it prohibitive and including the ViewModel as a default parameter has massively sped up development.