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>()
}
}
}
Related
I have a Screen ( composable function ) that gets It's data from view model ( a list and two function to remove and add data in it ).
#Composable
fun MainScreen(
notes: List<Note>,
onAddNote: (Note) -> Unit,
onRemoveNote: (Note) -> Unit
){}
Now when i call this function inside the composable of my Nav host, I get errors stating that i should fill the parameters.
#Composable
fun NotesNavigation(){
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Navigation.MainScreen.name){
composable(Navigation.MainScreen.name){
MainScreen() // error here
}
}
}
Now I am wondering what is the best practice to sort it out, do i need to provide default values for my parameters like supplying an empty list
or
there is better way to get around it.
You can set default values to the MainScreenFunction, but since you are using navigation, this would become useless. I would suggest to set the viewmodel as a parameter. The viewmodel should still be passed through the navigation though.
I don't know if you use any dependency injection. If so, that would make it a bit easier. Then you can set it up like this:
#Composable
fun MainScreen(
viewModel: MainScreenViewModel = getViewModel() //If using Koin DI
){
...
}
This way, the navigation doesn't have to know about the viewmodel. You can still set it though, if you do need a different viewModel then the one injected for example.
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>()
}
}
}
In the case of using Di, the way it is written on the official Android website is as follows
// import androidx.hilt.navigation.compose.hiltViewModel
#Composable
fun MyApp() {
NavHost(navController, startDestination = startRoute) {
composable("example") { backStackEntry ->
// Creates a ViewModel from the current BackStackEntry
// Available in the androidx.hilt:hilt-navigation-compose artifact
val exampleViewModel = hiltViewModel<ExampleViewModel>()
ExampleScreen(exampleViewModel)
}
/* ... */
}
}
Then if there are a lot of other #Composable functions in the ExampleScreen, like this
ExampleScreen() {
A()
B()
}
A() {
TopBar()
BottomBar()
....
}
B() ...
If both A() and its sub-functions need to use things in vm, don't you have to pass the vm parameters one by one? Because if vm is created in these functions, it is not a singleton(Because navigation compose affects the viewModel, each time you switch the page, these viewModels will be recreated as a new one). When I was puzzled, I saw this design idea on the official website again:
Pass explicit parameters
The general idea is that I should pass the logic code of the child function in the parent function, e.g. in ExampleScreen write:
ExampleScreen() {
val vm = hilt<VM>()
A(onClick = vm.onClick, ...)
B(...)
}
So my question is, if I have a lot of nested functions, don't I need to write a logical parameter in each function? So if I want to create a vm directly in each function, but it is not a singleton, what should I do? Im confused
You've done the right thing by injecting the viewmodel at the top-level. It's now up to you to decide how you want to pass it down. They're just functions in the end.
You can pass the viewmodel down everywhere, pass down only specific members or pass nothing down.
Do what makes sense and iterate if it doesn't work.