I’m developing an Android app where I have a bottom bar. I am using Jetpack Compose, hilt and ViewModel. I have a ViewModel (ShopListHomeViewModel) that manages the state of a screen (ShopListHomeScreen). However, when I switch tabs and return to the screen, the ViewModel’s state resets, causing me to lose data and screen goes to loading again.
Can you guys help me what is missing and wrong? I want to keep the data even tho user switch tabs because I don’t want to go for loading everytime, I will do that when I only refresh by pull and creating new list. I am adding code and video. Thanks
Codes:
data class ShopListHomeState(
val isLoading: Boolean = true,
val name: String = "Test",
val surname: String = "User"
)
@HiltViewModel
internal class ShopListHomeViewModel @Inject constructor() : ViewModel() {
private val _state = MutableStateFlow(ShopListHomeState())
val state: StateFlow<ShopListHomeState> = _state.asStateFlow()
var counter: Int = 0
init {
viewModelScope.launch {
delay(3000)
_state.update { it.copy(isLoading = false) }
}
}
fun handleEvent(event: ShopListHomeEvents) {
viewModelScope.launch {
when (event) {
is ShopListHomeEvents.CreateNewListClicked -> {
_state.update { it.copy(name = counter++.toString()) }
}
}
}
}
}
and screen itself:
@Composable
internal fun ShopListHomeScreen() {
val viewModel: ShopListHomeViewModel = hiltViewModel()
val state = viewModel.state.collectAsState()
if (state.value.isLoading) {
ScLoadingScreen()
} else {
ShopListHomeScreenContent(state.value, viewModel)
}
}
@Composable
internal fun ShopListHomeScreenContent(
state: ShopListHomeState,
viewModel: ShopListHomeViewModel
) {
Column(modifier = Modifier.padding(16.dp)) {
ScCardItem(
modifier = Modifier.fillMaxWidth(),
onClick = {}
) {
Box(
modifier = Modifier
.padding(32.dp)
.fillMaxWidth(),
contentAlignment = Alignment.TopStart,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement
.spacedBy(
space = 16.dp,
alignment = Alignment.Start
),
) {
Text(text = "Family List", style = ScTheme.typography.titleMedium)
Text(text = "Created by ${state.name}", style = ScTheme.typography.bodySmall)
}
}
}
Spacer(modifier = Modifier.weight(1f))
ScPrimaryButton(text = "Create New List", onClick = {
viewModel.handleEvent(ShopListHomeEvents.CreateNewListClicked)
})
}
}
Other Codes about navigation and settings:
@Composable
fun AppNavHost(
modifier: Modifier = Modifier,
navController: NavHostController,
startDestination: String = NavigationItem.Home.route,
) {
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination,
enterTransition = {
EnterTransition.None
},
exitTransition = {
ExitTransition.None
}
) {
composable(NavigationItem.Home.route) {
ShopListHomeScreen()
}
composable(NavigationItem.Settings.route) {
SettingsHomeScreen()
}
composable(NavigationItem.CreateListScreen.route) {
ShopListCreateScreen(navController)
}
}
}
@Composable
internal fun SettingsHomeScreen() {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = "Setting List", style = ScTheme.typography.titleMedium)
Spacer(modifier = Modifier.weight(1f))
}
}
enum class Screen {
Home,
Settings,
CreateListScreen
}
sealed class NavigationItem(val route: String) {
object Home : NavigationItem(Screen.Home.name)
object Settings : NavigationItem(Screen.Settings.name)
object CreateListScreen : NavigationItem(Screen.CreateListScreen.name)
}
@Composable
fun TabView(tabBarItems: List<TabBarItem>, navController: NavController) {
var selectedTabIndex by rememberSaveable {
mutableStateOf(0)
}
NavigationBar {
tabBarItems.forEachIndexed { index, tabBarItem ->
NavigationBarItem(
colors = NavigationBarItemColors(
selectedIconColor= ScTheme.colors.neutrals.black200TextPrimary,
selectedTextColor = ScTheme.colors.neutrals.black200TextPrimary,
selectedIndicatorColor = Color.Transparent,
unselectedIconColor = ScTheme.colors.neutrals.grey200TitleSecondary,
unselectedTextColor = ScTheme.colors.neutrals.grey200TitleSecondary,
disabledIconColor = ScTheme.colors.neutrals.grey200TitleSecondary,
disabledTextColor = ScTheme.colors.neutrals.grey200TitleSecondary,
),
selected = selectedTabIndex == index,
onClick = {
selectedTabIndex = index
navController.navigate(tabBarItem.title)
},
icon = {
TabBarIconView(
isSelected = selectedTabIndex == index,
selectedIcon = tabBarItem.selectedIcon,
unselectedIcon = tabBarItem.unselectedIcon,
title = tabBarItem.title,
)
},
label = { Text(tabBarItem.title) })
}
}
}
@Composable
fun TabBarIconView(
isSelected: Boolean,
selectedIcon: ImageVector,
unselectedIcon: ImageVector,
title: String,
badgeAmount: Int? = null
) {
BadgedBox(badge = { TabBarBadgeView(badgeAmount) }) {
Icon(
imageVector = if (isSelected) {
selectedIcon
} else {
unselectedIcon
},
contentDescription = title
)
}
}
2
Answers
So after researching for hours I found a way I don't know if it is the perfect solution but it works. Here it is:
At my navhost, I created a variable
and when navigating to screens I passed my viewmodel from here like this:
What do you think about this solution?
I think you’re simply missing a
@Singleton
tag on top of yourShopListHomeViewModel
View Model, as to make sure you always get a single instance of that class.Alternatively, if you have a
@Provider
Module class or something of the sort to manage the View Models, you could make sure you manually handle a single instance in there, again, so that you always get the same object from the DI.