skip to Main Content

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

video

2

Answers


  1. Chosen as BEST ANSWER

    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

     val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
            "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
        }
    

    and when navigating to screens I passed my viewmodel from here like this:

     composable(NavigationItem.Home.route) {
                val viewModel = hiltViewModel<ShopListHomeViewModel>(viewModelStoreOwner = viewModelStoreOwner)
                ShopListHomeScreen(viewModel)
            }
            composable(NavigationItem.Settings.route) {
                SettingsHomeScreen()
            }
            composable(NavigationItem.CreateListScreen.route) {
                ShopListCreateScreen(navController)
            }
    

    What do you think about this solution?


  2. I think you’re simply missing a @Singleton tag on top of your ShopListHomeViewModel 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search