Home > OS >  ViewModel does not preserve state when leaving and returning (switching between apps)
ViewModel does not preserve state when leaving and returning (switching between apps)

Time:08-01

ViewModel does not preserve state during configuration changes, e.g., leaving and returning to the app when switching between background apps.

@HiltViewModel
class MainViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
) : ViewModel()
{
    var title by mutableStateOf("")
        internal set

    var showMenu by  mutableStateOf(false)
        internal set

    var tosVisible by mutableStateOf(false)
        internal set
}

The menu:

enter image description here


Currently: It survives the rotation configuration change, the menu remains open if opened by clicking on the three ... dots there. But, changing app, i.e. leaving the app and going into another app. Then returning, does not preserve the state as expected. What am I possibly doing wrong here?


In MainActivity:

val mainViewModel by viewModels<MainViewModel>()

Main(mainViewModel) // Passing it here

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Main(viewModel: MainViewModel = viewModel()) {

    val context = LocalContext.current
    val navController = rememberNavController()

EDIT: Modified my ViewModel to this, Makes no difference.

@HiltViewModel
class MainViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
) : ViewModel()
{
    var title by mutableStateOf("")
        internal set

    var showMenu by  mutableStateOf(savedStateHandle["MenuOpenState"] ?: false)
        internal set

    var tosVisible by mutableStateOf(savedStateHandle["AboutDialogState"] ?: false)
        internal set

    fun displayAboutDialog(){
        savedStateHandle["AboutDialogState"] = tosVisible;
    }

    fun openMainMenu(){
        savedStateHandle["MenuOpenState"] = showMenu;
    }
}

Full code of the MainActivity:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Main(viewModel: MainViewModel) {

    val context = LocalContext.current
    val navController = rememberNavController()
    //val scope = rememberCoroutineScope()

    val decayAnimationSpec = rememberSplineBasedDecay<Float>()
    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
        decayAnimationSpec,
        rememberTopAppBarScrollState()
    )

    LaunchedEffect(navController){
        navController.currentBackStackEntryFlow.collect{backStackEntry ->
            Log.d("App", backStackEntry.destination.route.toString())
            viewModel.title = getTitleByRoute(context, backStackEntry.destination.route);
        }
    }

    Scaffold(
        topBar = {
            CenterAlignedTopAppBar(
                title = {
                    Text(
                        viewModel.title,
                        //color = Color(0xFF1877F2),
                        style = MaterialTheme.typography.headlineSmall,
                    )
                },
                colors = TopAppBarDefaults.smallTopAppBarColors(
                    containerColor = MaterialTheme.colorScheme.background
                ),
                actions =  {
                    IconButton(
                        onClick = {
                            viewModel.showMenu = !viewModel.showMenu
                        }) {
                        Icon(imageVector = Icons.Outlined.MoreVert, contentDescription = "")
                        MaterialTheme(shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(20.dp))) {
                            IconButton(
                                onClick = { viewModel.showMenu = !viewModel.showMenu }) {
                                Icon(imageVector = Icons.Outlined.MoreVert, contentDescription = "")
                                DropdownMenu(
                                    expanded = viewModel.showMenu,
                                    onDismissRequest = { viewModel.showMenu = false },
                                    modifier = Modifier
                                        .background(MaterialTheme.colorScheme.background)
                                        .padding(0.dp),
                                    properties = PopupProperties(focusable = true)
                                ) {
                                    DropdownMenuItem(text = { Text("Sign out", fontSize = 16.sp) },  onClick = { viewModel.showMenu = false })
                                    DropdownMenuItem(text = { Text("Settings", fontSize = 16.sp) },  onClick = { viewModel.showMenu = false })
                                    Divider(color = Color.LightGray, thickness = 1.dp)
                                    DropdownMenuItem(text = { Text("About", fontSize = 16.sp) },
                                        onClick = {
                                            viewModel.showMenu = true
                                            viewModel.tosVisible = true
                                        })
                                }
                            }
                        }
                    }
                },
                scrollBehavior = scrollBehavior
            ) },
        bottomBar = { BottomAppBar(navHostController = navController) }
    ) { innerPadding ->
        Box(modifier = Modifier.padding(PaddingValues(0.dp, innerPadding.calculateTopPadding(), 0.dp, innerPadding.calculateBottomPadding()))) {
            BottomNavigationGraph(navController = navController)
        }
    }
}

CodePudding user response:

Maybe you are trying to access another instance of the view model or creating a new instance of the view model upon re-opening the app. The code should work fine if you are accessing the same instance.

Edit: Seems like you were in fact reinitializing the ViewModel. You are creating a new instance of ViewModel by putting the ViewModel inside the constructor because when you re-open the app the composable will be created again, thus creating a new ViewModel instance. What I will suggest you do is initialise the ViewModel inside composable like this maybe:

fun Main(){
val viewModel = ViewModelProvider(this@MainActivity)[MainViewModel::class.java]
}

CodePudding user response:

Solved with this:

@HiltViewModel
class MainViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
) : ViewModel()
{
    companion object {
        const val UI_MENU_STATE = "ui.menu.state"
    }

    init {
        savedStateHandle.get<Boolean>(UI_MENU_STATE)?.let {
                m -> onMenuStateChange(m);
        }
    }

    private var m_title by mutableStateOf("")
    private var displayMenu by mutableStateOf( false)

    var tosVisible by mutableStateOf( false)
        internal set

    fun updateTitleState(title: String){
        m_title = title;
    }

    fun onMenuStateChange(open: Boolean){
        Log.d("App", open.toString());
        displayMenu = open;
        savedStateHandle.set<Boolean>(UI_MENU_STATE, displayMenu);
    }

    fun isMenuOpen(): Boolean {
        return displayMenu;
    }

    fun getTitle(): String { return m_title; }
}
  • Related