how to bind ViewModel life cycle to compose
Asked Answered
A

3

9

I'm using Jetpack Compose now. I realized I could have ViewModel per my composable and init view model in composable just like this:

val myViewModel:MyViewModel = viewModel()

But there is a problem that these view models will never be destroyed, even when the composable is not shown.

for instance, I have a Main composable screen that loads some other screen based on user interaction, like this:

@Composable
fun MainAuthentication(viewModel: MainViewModel) {
    val state = viewModel.state.value
    val scope = rememberCoroutineScope()
    val scaffoldState = rememberScaffoldState()
    Scaffold(scaffoldState = scaffoldState)
    {

        //--------------------(login and sign up button)--------------------//
        Row(
            modifier = Modifier
                .padding(top = 50.dp)
                .fillMaxSize(),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            if (!state.signUpFormIsVisible && !state.loginFormIsVisible) {
                Button(
                    onClick = {
                        viewModel.onEvent(event = MainEvent.LoginButtonClick)
                    },
                    modifier = Modifier
                        .padding(10.dp)
                        .weight(0.5f)
                ) {
                    Text(text = stringResource(id = R.string.login))
                }

                Button(
                    onClick = { viewModel.onEvent(event = MainEvent.SignUpButtonClick) },
                    modifier = Modifier
                        .padding(10.dp)
                        .weight(0.5f)
                ) {
                    Text(text = stringResource(id = R.string.signup))
                }
            }

        }



        LoginForm(show = state.loginFormIsVisible) { msg ->
            scope.launch {
                scaffoldState.snackbarHostState.showSnackbar(
                    message = msg
                )
            }
        }
        SignUpForm(show = state.signUpFormIsVisible) { msg ->
            scope.launch {
                scaffoldState.snackbarHostState.showSnackbar(
                    message = msg
                )
            }
        }


    }
}

each of the login and sign up screens has its view model and is used like this:

@Composable
fun LoginForm(show: Boolean, snackBarMsg: (String) -> Unit) {

    val viewModel: LoginViewModel = viewModel()
    val state = viewModel.state.value
 ...

AnimatedVisibility(
        visible = show,
        enter = slideInVertically(),
        exit = slideOutVertically()
    ) {
    ...
    ...
    }
}

How can I bind each view model to its composable function if the composable is not visible, the view model gets destroyed?

Is it a good practice to destroy the view models if the respective composable is not visible?

Affricate answered 28/11, 2021 at 6:33 Comment(0)
W
4

In Compose you can use navigation, which is perfect for your needs: each route has its own view model scope, which is destroyed as soon as the route is removed from the navigation back stack.

You can use popBackStack to remove current screen from the stack before navigation to the new screen, the old screen will be destroyed with the corresponding view model. Check out this answer on how you can remove multiple items.

Compose Navigation is based on regular Android navigation, so its documentation is relevant for most of questions, in case the Compose Navigation documentation seems short to you.

Whitmer answered 28/11, 2021 at 7:22 Comment(0)
T
1

When you create ViewModel by using function viewModel<YourViewModel>(), created ViewModel associated with scope of local ViewModelStoreOwner. ViewModelStoreOwner contains class to store ViewModels - ViewModelStore, where you can remove all models stored inside with function clear().

One of the ways to bind ViewModel lifecycle to composable, you can create your own ViewModelStoreOwner, associate created ViewModel with it, and call viewModelStoreOwner.viewModelStore.clear() in composable function DisposableEffect() inside closing composable screen.

Something like:

Create your own simple ViewModelStoreOwner:

val viewModelStoreOwnerLoginForm = object : ViewModelStoreOwner {
    override val viewModelStore: ViewModelStore = ViewModelStore()
}

Pass viewModelStoreOwnerLoginForm in parameters in your composable function LoginForm(). When creating ViewModel by using function viewModel(), pass viewModelStoreOwnerLoginForm in the parameter viewModelStoreOwner. Call viewModelStoreOwnerLoginForm.viewModelStore.clear() inside composable function DisposableEffect():

@Composable
fun LoginForm(
    show: Boolean, 
    snackBarMsg: (String) -> Unit
    viewModelStoreOwnerLoginForm: ViewModelStoreOwner
) {
    val viewModel: LoginViewModel = viewModel<LoginViewModel>(
        viewModelStoreOwner = viewModelStoreOwnerLoginForm,
        key = "LoginViewModel"
    )
    //Your code of screen

    DisposableEffect(key1 = Unit, effect = {
        onDispose {
            //Code inside will work as the last thing after leaving the screen
            viewModelStoreOwnerLoginForm.viewModelStore.clear()
        }
    })
}

When screen LoginForm will be closed, function DispossableEffect() will be called and viewModel will be cleared and destroyed.

Thibodeaux answered 6/12, 2023 at 14:46 Comment(0)
A
0

ViewModels are meant to be independent of the UIs. If the UI gets destroyed due to a configuration change, the viewmodel should remain intact and retain the state of the UI when it gets recomposed. Binding a viewmodel to every composable makes no sense. Normally, you should have only one viewmodel per screen and all the composables on that screen should use that one. But that isn't a hard rule. There are certainly composables that can and should have their own viewmodels. But they need to be managed at a higher level so that they get destroyed when the screen they appear on is no longer in use. When you navigate from the current screen to a previous screen using the Back button, you normally will want to have all the viewmodels for that screen destroyed.

There are better approaches to how composables in your UI hierarchy can access viewmodels and have them destroyed. I developed one solution that manages your viewmodels for your screens and allows composables anywhere within the hierarchy to easily access them. There is also the feature that a viewmodel can stay alive even if the screen is destroyed when you return to the previous screen. A use-case for this is if you are developing an app that hosts video conferences and you want the camera and audio to continue while you navigate back without terminating the meeting. For this, the viewmodel needs to remain alive, even though the screen itself has been destroyed. Jetpack Compose has its own solutions but I was never satisfied with them. The two biggest drawbacks with the native approach is that you cannot pass objects between screens and you need to write code to detect device configuration settings and changes to adapt the screen. I solved these with the framework I developed. Check it out:

https://github.com/JohannBlake/Jetmagic

Assiduity answered 28/11, 2021 at 6:48 Comment(1)
> ViewModels are meant to be independent of the UIs Not really? The ViewModels are absolutely bound to UI. They are destroyed when the corresponding Activity/Fragment is destroyed, with the exception of surviving configuration changes. So their lifecycle, although longer, definitely depends on the view lifecycleBalder

© 2022 - 2024 — McMap. All rights reserved.