4

From my understanding of navigation graph scoped view models, their onCleared method should get called immediately when the associated navigation graph is popped from the navigation stack. What I'm seeing is that it takes a couple more navigate calls, until onCleared is actually getting called.

Inside MainActivity.kt:

val swipeDismissableNavController = rememberSwipeDismissableNavController()

swipeDismissableNavController.addOnDestinationChangedListener(NavController.OnDestinationChangedListener { controller, destination, arguments ->
    try {
        controller.getBackStackEntry("chat")
    } catch(e: IllegalArgumentException) {
        Log.d(TAG, "Not on stack")
    }
    Log.d(TAG, "Route: " + destination.route)
})

SwipeDismissableNavHost(
    navController = swipeDismissableNavController,
    startDestination = "home"
) {
    navigation(
        startDestination = "messages",
        route = "chat"
    ) {
        composable(
            route = "messages"
        ) { 
            val backStackEntry = remember { swipeDismissableNavController.getBackStackEntry("chat") }
            val chatViewModel: ChatViewModel = viewModel(backStackEntry)
            
            ...
        }
        
        composable(
            route = "image"
        ) { 
            val backStackEntry = remember { swipeDismissableNavController.getBackStackEntry("chat") }
            val chatViewModel: ChatViewModel = viewModel(backStackEntry)
            
            ...
        }
    }
    
    ...
}

Inside ChatViewModel.kt

class ChatViewModel(): ViewModel() {
    ...
    
    init {
        Log.d(TAG, "init")
    }

    override fun onCleared() {
        super.onCleared()
        Log.d(TAG, "onCleared")
    }
}

What I am observing is the following behavior:
When I navigate to chat, the view model is correctly created printing init. By navigating inside the chat graph, the view model instance is correctly reused. However, when I'm navigating out of the chat graph, I'm seeing that Not on stack is getting printed, but onCleared is not. Only after some additional navigations, onCleared is getting printed eventually.

Does anyone have an idea why onCleared is getting called delayed?

The print stack looks something like:

Not on Stack
Route: home

Route: messages
init

Not on Stack
Route: home
** (THIS IS WHERE I WOULD EXPECT TO SEE onCleared) **

Not on Stack
Route: settings

Not on Stack
Route: home
onCleared

However, if I'm doing more navigations inside the chat graph, onCleared is getting called correctly:

Not on Stack
Route: home

Route: messages
init

Route: image

Route: messages

Route: image

Route: messages

Route: image

Route: messages

Not on Stack
Route: home
onCleared
Yannick
  • 61
  • 1
  • 7

1 Answers1

1

I had a similar issue.

Compose hooks up viewModels by default to the nearest Fragment or Activity in view hierarchy which controls lifetime of those viewModels in same way as it would be non - compose world.

I have solved the issue in my case by using custom ViewModelOwner + DisposableEffect:

if (selectedParkingAreaId != null) {
    val viewModelStore = remember(selectedParkingAreaId) { ViewModelStore() }
    DisposableEffect(selectedParkingAreaId) {
        onDispose { viewModelStore.clear() }
    }

    val viewModel: WheelScreenViewModel =
        viewModel(viewModelStoreOwner = { viewModelStore })
    
    ...
}

Then when selectedParkingAreaId is null the if section is being excluded from composition which fires DisposableEffect.onDispose{...} which clears ViewModelStore which calls WheelScreenViewModel.onCleared.

Not super elegant, but functional.

Alexander Skvortsov
  • 2,696
  • 2
  • 17
  • 31