Is State<T> only designed for #Composable in Android? - android

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.

Related

What to put in parameters of a Screen when putting it in a composable of nav host in jetpack navigation

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.

Problems with the ViewModel in Jetpack Compose [duplicate]

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>()
}
}
}

Other state management options that I may use with Jetpack Compose, beyond State hoisting?

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.

Initiating a ViewModel with Input parameters in Jetpack Compose -> Composable function

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.

Observe livedata and navigate in jetpack compose

I just started learning jetpack compose. I have a very basic question. My ViewModel has a SingleLiveEvent that I use to navigate to another screen.
private val _navigateToDetails: SingleLiveEvent<Movie> = SingleLiveEvent()
val navigateToDetails: MutableLiveData<Movie> = _navigateToDetails
I know that I can use Livedata as state to emit UI but how to use it to trigger some action within composable.
Previously I had used viewLifecycleOwner to observer the state as anyone would do like this.
viewModel.navigateToDetails.observe(viewLifecycleOwner) {
// navigate to details
}
How do I achieve the same thing in compose. I don't know if that's possible or not. Maybe I am not thinking this in compose way. Any help would be appreciated.
I would do something like to make sure I'm only doing it once:
#Composable
fun LoginScreen(viewModel: LoginViewModel) {
val loginState by viewModel.loginState.observeAsState(null)
val hasHandledNavigation = remember { mutableStateOf(false)}
if (loginState != null && !hasHandledNavigation.value ) {
navigateToWelcomeScreen()
else {
// rest of the Compose UI
}
}
UPDATE:
Option two, you can also just pass the action of going to next screen to viewmodel and fire it up there.
Actually, in compose we use mutableStateOf() over LiveData. In your viewmodel, you can change the type of the data holder from LiveData to mutableStateOf(...) which will allow you to directly use it in your Composables without explicitly calling observe()
Let's say you wish to store an integer of any kind in your viewmodel and update the state of your Composable based on that.
In your viewmodel,
var mInteger by mutableStateOf (0) //'by' helps treat state as data
fun updateMInteger(newValue: Int){
mInteger = newValue
}
In your Composable, directly call viewmodel.mInteger and Compose being built like that, automatically updates the UI, given that mInteger is being read from the Composable
Like
Text(viewModel.mInteger)

Categories

Resources