androidkotlinandroid-jetpack-composeandroid-jetpack-navigation

ViewModel#onCleared not called anymore in Navigation3


I just updated the dependencies for:

androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }

From:

nav3Core = "1.0.0-alpha03"
lifecycleViewmodelNav3 = "1.0.0-alpha01"

To:

nav3Core = "1.0.0-alpha04"
lifecycleViewmodelNav3 = "1.0.0-alpha02"

As announced here and here, and the onCleared() function inside the ViewModel is not called anymore. With the previous versions, it worked fine. How to solve this issue?

Edit:

I tried to reproduce the issue in the simplest way possible. I only have two screens and I navigate from one screen to another on a button click. So here is my code:

@Composable
fun AppNavDisplay(
    backStack: NavBackStack
) {
    NavDisplay(
        entryDecorators = listOf(
            rememberSceneSetupNavEntryDecorator(),
            rememberSavedStateNavEntryDecorator(),
            rememberViewModelStoreNavEntryDecorator()
        ),
        backStack = backStack,
        onBack = {
            backStack.removeLastOrNull()
        },
        entryProvider = entryProvider {
            entry<Screen.SignIn> {
                SignInScreen(
                    onClearAndNavigate = backStack::onClearAndNavigate
                )
            }
            entry<Screen.Main> {
                MainScreen(
                    onClearAndNavigate = backStack::onClearAndNavigate
                )
            }
        }
    )
}

fun NavBackStack.onClearAndNavigate(screen: Screen) {
    clear()
    add(screen)
}

Here is my SignInViewModel:

class SignInViewModel(
    private val repo: AuthRepository
): ViewModel() {
    val signInState = ResponseStateHandler<Unit>()

    fun signIn() = viewModelScope.launch {
        try {
            signInState.setLoading()
            repo.signIn()
            signInState.setSuccess(Unit)
        } catch (e: Exception) {
            signInState.setFailure(e.message!!)
        }
    }

    override fun onCleared() {
        Log.d(TAG, "signInViewModel.onCleared") //Not called
        signInState.setIdle()
    }
}

And here is how to use the result in the UI:

val signInResponse by viewModel.signInState.collect()

LaunchedEffect(signInResponse) {
    signInResponse.onSuccess {
        onClearAndNavigate(Screen.Main)
    }.onFailure { message ->
        showMessage(context, "$message")
        viewModel.signInState.setIdle()
    }
}

The only thing that makes the onCleared not to be called, is the upgrade of the dependencies.

Edit2:

Here are the screens:

Scaffold(
    modifier = Modifier.fillMaxSize()
) { innerPadding ->
    Box(
        modifier = Modifier.fillMaxSize().padding(innerPadding),
        contentAlignment = Alignment.Center
    ) {
        Button(
            onClick = viewModel::signIn,
            enabled = !isProcessing
        ) {
            Row(
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text(
                    text = "SignIn"
                )
                Spacer(
                    modifier = Modifier.width(4.dp)
                )
                if (isProcessing) {
                    CircularProgressIndicator(
                        modifier = Modifier.size(12.dp),
                        strokeWidth = 2.dp
                    )
                }
            }
        }
    }
}

And:

val isUserSignedOut by viewModel.isSignedOutState.collectAsStateWithLifecycle()

LaunchedEffect(isUserSignedOut) {
    if (isUserSignedOut) {
        onClearAndNavigate(Screen.SignIn)
    }
}

Scaffold(
    modifier = Modifier.fillMaxSize()
) { innerPadding ->
    Box(
        modifier = Modifier.fillMaxSize().padding(innerPadding),
        contentAlignment = Alignment.BottomCenter
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically
        ) {
            Button(
                onClick = viewModel::deleteUser,
                enabled = !isProcessing
            ) {
                Text(
                    text = "SignOut"
                )
                Spacer(
                    modifier = Modifier.width(4.dp)
                )
                if (isProcessing) {
                    CircularProgressIndicator(
                        modifier = Modifier.size(12.dp),
                        strokeWidth = 2.dp
                    )
                }
            }
        }
    }
}

Edit3:

sealed interface Screen: NavKey {
    @Serializable
    data object Main : Screen
    @Serializable
    data object SignIn : Screen
}

Solution

  • Starting from today, 2025-07-03, with the new updates from here and here:

    nav3Core = "1.0.0-alpha05"
    lifecycleViewmodelNav3 = "1.0.0-alpha03"
    

    Everything works fine. The ViewModel#onCleared is called now as expected.