Scoping States in Jetpack Compose

Scoping States in Jetpack Compose

This is precisely what navigation graph scoped view models are used for.

This involves two steps:

  1. Finding the NavBackStackEntry associated with the graph you want to scope the ViewModel to

  2. Pass that to viewModel().

For part 1), you have two options. If you know the route of the navigation graph (which, in general, you should), you can use getBackStackEntry directly:

// Note that you must always use remember with getBackStackEntry
// as this ensures that the graph is always available, even while
// your destination is animated out after a popBackStack()
val navigationGraphEntry = remember {
navController.getBackStackEntry("graph_route")
}
val navigationGraphScopedViewModel = viewModel(navigationGraphEntry)

However, if you want something more generic, you can retrieve the back stack entry by using the information in the destination itself - its parent:

fun NavBackStackEntry.rememberParentEntry(): NavBackStackEntry {
// First, get the parent of the current destination
// This always exists since every destination in your graph has a parent
val parentId = navBackStackEntry.destination.parent!!.id

// Now get the NavBackStackEntry associated with the parent
// making sure to remember it
return remember {
navController.getBackStackEntry(parentId)
}
}

Which allows you to write something like:

val parentEntry = it.rememberParentEntry()
val navigationGraphScopedViewModel = viewModel(parentEntry)

While the parent destination will be equal to the root graph for a simple navigation graph, when you use nested navigation, the parent is one of the intermediate layers of your graph:

NavHost(navController, startDestination = startRoute) {
...
navigation(startDestination = nestedStartRoute, route = nestedRoute) {
composable(route) {
// This instance will be the same
val parentViewModel: YourViewModel = viewModel(it.rememberParentEntry())
}
composable(route) {
// As this instance
val parentViewModel: YourViewModel = viewModel(it.rememberParentEntry())
}
}
navigation(startDestination = nestedStartRoute, route = secondNestedRoute) {
composable(route) {
// But this instance is different
val parentViewModel: YourViewModel = viewModel(it.rememberParentEntry())
}
}
composable(route) {
// This is also different (the parent is the root graph)
// but the root graph has the same scope as the whole NavHost
// so this isn't particularly helpful
val parentViewModel: YourViewModel = viewModel(it.rememberParentEntry())
}
...
}

Note that you are not limited to only the direct parent: every parent navigation graph can be used to provide larger scopes.

How to scope a viewModel to a Dialog Composable Function which is not related to NavHost

As per this issue, Compose does not offer any mechanism to scope ViewModels to an individual @Composable - any ViewModels you create outside of a NavHost's destination is scoped to the activity/fragment that contains your ComposeView/where you call setContent and, thus, lives for the entire lifetime of your Compose hierarchy - that's why you always get the same instance back.

Note that Navigation Compose has existing feature requests for supporting dialog destinations and for supporting BottomSheetScaffold, which would bring the same scoping of ViewModels and state to those types of destinations as well. You should star those issues to get updates and indicate your interest (which then helps prioritize that work).

How can I manage a lots of States in jetpack compose

I don't think there's any point in creating a setter for each property, in your example. It can be useful when you have some logic which not just updating the value.

You can move your states into separate classes. As long as you have them as state objects, updating them will trigger recomposition.

Let's say you have a CardView which draw your card with number, name, etc. If you don't have it in a separate view, that's an other good practice to split your composables into small views. In addition to the convenience of splitting the data into chunks, it will help with reconfiguration and is much easier to read and develop.

So group all data that's being used by this view into CardData:

class CardData {
val cardNumber = mutableStateOf("")
val cardHolderName = mutableStateOf("")
val pinNumber = mutableStateOf("")
val cvvNumber = mutableStateOf("")
val issueDate = mutableStateOf("")
val expiryDate = mutableStateOf("")
}

Store it in your view model:

@HiltViewModel
class CardsViewModel @Inject constructor(
private val repository: CardsRoomRepository
): ViewModel() {
...
val cardData = CardData()
}

And pass to your view:

@Composable
fun Screen(viewModel: CardsViewModel() = viewModel()) {
...
CardView(viewModel.cardData)
}

And if you're using these setters to pass to text fields, you can unwrap them like this:

@Composable
fun CardView(cardData: CardData) {
val (cardHolderName, cardHolderNameSetter) = cardData.cardHolderName
TextField(cardHolderName, cardHolderNameSetter)
}

Custom class for holding multiple states in Jetpack Compose

I suppose you want something like this:

class LoginPageState(username: String) {
val username = mutableStateOf(TextFieldValue(username))
}

and then use it like

val state by rememberSaveable(
stateSaver = mapSaver(
save = {
mapOf("username" to it.username.value.text)
},
restore = { map ->
LoginPageState(username = map["username"] as String)
}
)
) {
mutableStateOf(
LoginPageState("")
)
}

TextField(value = state.username.value , onValueChange = {
state.username.value = it
})

View Model not destroyed on back press in jetpack compose

The issue was, I was using using CompositionLocalProvider incorrectly.

This was my initial code.

@Composable
fun MyNavGraph(
activityViewModel: MainActivityViewModel,
viewModelStoreOwner: ViewModelStoreOwner,
) {
val navHostController = rememberNavController()
val myNavActions = remember(navHostController) {
MyNavActions(navHostController)
}

NavHost(
navController = navHostController,
startDestination = Screen.Home.route,
) {
composable(
route = Screen.Home.route,
) { navBackStackEntry ->
CompositionLocalProvider(
LocalViewModelStoreOwner provides viewModelStoreOwner
) {
HomeScreen(
activityViewModel = activityViewModel,
navigateToSettings = {
myNavActions.navigateTo(
navBackStackEntry,
Screen.Settings.route,
)
},
)
}
}
composable(
route = Screen.Settings.route,
) { navBackStackEntry ->
CompositionLocalProvider(
LocalViewModelStoreOwner provides viewModelStoreOwner
) {
SettingsScreen(
activityViewModel = activityViewModel,
)
}
}
}
}

Removing

CompositionLocalProvider(
LocalViewModelStoreOwner provides viewModelStoreOwner
)

fixed the scoping of the viewmodel to composable scope.

Sample repo with initial commit and fixed commit -

https://github.com/Abhimanyu14/compose-navigation-sample



Related Topics



Leave a reply



Submit