How to destroy a ViewModel when user leave a screen - android

In my project I have a splash screen, when it is displayed, my app loading some startup data from server, after loading the data shows another screen.
For splash screen I create a ViewModel, but it stays in memory all the time. How to destroy it correctly?
Thank you for help!
#HiltViewModel
class SplashViewModel #Inject constructor (private val repository: Repository) {
....
}
#Composable
fun SplashScreen(vm: SplashViewModel) {
...
}
#Composable
fun Navigate() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "splash") {
composable("splash") {
SplashScreen(vm = hiltViewModel())
}
composable("main") {
MainScreen(...) // When shows MainScreen, SplashViewModel object still is in memory
}
}
}

Your viewmodel stays in memory because your splash screen is your root destination, and as such it stays always on the stack as the bottom entry.
If you want your splash viewmodel to be automatically destroyed when you leave your splash screen you should pop it from the backstack when you navigate to your main screen, using popUpTo.
Another option you could consider is to make your main screen the root destination and then navigate from that screen to splash if you are starting the app fresh.
Using hiltViewModel and scoping the viewmodel to the nav graph destination as you do will ensure the viewmodel is destroyed when the user leaves that screen, provided it's not in the backstack.

It is not explicitly supported in Android as far as I know. However, you could create a method named onViewModelCleared() inside the viewmodel itself and pass null to all the nullable objects, and something lightweight to non-null objects.

Related

How to navigate to deeply nested screen composable from the Activity's onCreate in Jetpack Compose

Suppose I have a notification, that when clicked, launches my app's activity. It's a notification about a message, in a conversation, and so it launches the activity passing the conversationId as an argument. When the activity is launched by that intent from the notification, it should open MessagesScreen, which is a deeply nested screen in the app, passing to it conversationId.
What is the best way to do this in Compose? In the good old Fragments or Activities you just navigated straight to it, but with Compose is a little trickier. The path to the MessagesScreen is as follows:
SplashScreen (checks for authentication) -> HomeScreen (if authenticated) -> ConversationScreen -> MessagesScreen
I can't just navigate straight to MessagesScreen by having the compose's NavController be stored in the Activity, since I need to go through SplashScreen to check for authentication. Also, I don't know the Compose's implication of navigating to a deeply nested component from the Activity's onCreate().
What I currently do is have a field in my global ViewModel called notificationConversationId, that is set on my Activity's onCreate if it was passed by the notification's intent:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val conversationId = intent.getStringExtra("conversation_id") ?: ""
globalViewModel.notificationConversationId = conversationId
// ...
Then, in a LaunchedEffect in my HomeScreen I observe this field, and if it is not empty, I navigate to the MessagesScreen, and set it to an empty string, so the LaunchedEffect is not executed again.
val conversationId = globalViewModel.notificationConversationid
LaunchedEffect(conversationId) {
if (conversationId.isNotEmpty()) {
val path = getMessagespath(conversationId = conversationId)
globalViewModel.notificationConversationId = ""
navController.navigate(path)
}
}
It works, but it is horrendous. Is there a better way to accomplish this in Compose? Thanks in advance.

Share composable in nav host

I have a NavHost which hosts multiple composables from the main screen to the login screen as show below:
#Composable
fun Navigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Main.route) {
composable(route = Screen.Main.route) {
StatusBar()
Main()
}
composable(route = Screen.Login.route) {
StatusBar()
Login()
}
}
}
You see how the Status bar is in both Main and Login composables I was just wondering if it would be possible to define it in one place so it can be used across all composables?
To keep your code clean I would recommend using the StatusBar in Main and Login. Because at the end the StatusBar and its state belongs to the screen and not to the NavHost.
The problem is that your screen is the component which owns all the information that the StatusBar needs (even though there are no parameters; this is theoretical representation). So it makes sense that the Screen owns its sate.
The composable of the NavHost on the otherside only knows the curren screen, but none of its state unless you state hoist it in a very ugly way. So it makes sense that it only holds that state it is best in managing.
Because of the fact that you already have put the StatusBar into an composable you also don't have a problem with code duplication.

Navigating between Fragments in Espresso UI test

I am trying to write a UI test with Espresso that runs across multiple Fragments. They share a viewModel injected via by activityViewModels().
I am using Android Navigation component, Dagger Hilt injection and a mocked NavController like this:
val navController = Mockito.mock(NavController::class.java)
launchFragmentInHiltContainer<AddRecipeFragment1> {
Navigation.setViewNavController(requireView(), navController)
}
// Here I need to initialize some data in viewModel...
After this I want to navigate to another Fragment that I actually want to test, but neither pressing a button that calls the navigation nor calling navController.navigate(AddRecipeFragment1Directions.toAddRecipeFragment2()) works for me.
An approach using launchFragmentInHiltContainer<AddRecipeFragment>... will restart the whole application and drop the viewModel.
Any ideas?
Meanwhile I found a workaround doing this:
private lateinit var viewModel: AddRecipeViewModel
#Before
fun beforeEach() {
...
navController = Mockito.mock(NavController::class.java)
launchFragmentInHiltContainer<AddRecipeFragment2> {
val viewModelTemp: AddRecipeViewModel by activityViewModels()
viewModel = viewModelTemp
Navigation.setViewNavController(requireView(), navController)
}
}
This way I can at least access the delegated ViewModel directly in my tests.

Android - Observe doesn't work after navigation

I'm developing an app right now and I'm facing one problem. I have simple login screen. I use Kotlin Flow to emit different states as Loading, Success, Failure. When state is Loading I want to navigate user to the loading screen. After that when state is Success I want to navigate user to the home screen. But other state than Loading is never called. It works when I remove navigation from Loading state. I suppose that after navigation to loading screen is viewModel cleared, I tried to log it but it doesn't write me message to the console.
private val viewModel: SignInViewModel by viewModels()
private fun observeSignIn() {
viewModel.signIn.observe(viewLifecycleOwner, {
when (it) {
is Status.Loading -> findNavController().navigate(R.id.loadingFragment)
is Status.Failure -> {
findNavController().navigateUp()
showErrorSnackBar(sv_sign_in, it.message)
}
is Status.Success -> {
findNavController().navigateUp()
findNavController().navigate(R.id.homeFragment)
}
}
})
}
Maybe possible solution would be to use viewModel initialized by navGraphViewModels but it doesn't make sense to me because I use this loading screen for another screens...
Thanks for help :)

Basic testing NavigationController in Android

I'm currently new to testing, so I decided to start off with some basic stuff.
I handle all my navigations from a DrawerLayout that is connected to an Activity.
So for my testing I launch an ActivityScenarioRule, create a testNavController object and then I set this testNavController to the current view that handles the navigation (The container fragment).
So the test consists on opening the drawer, clicking on menu item(Will navigate to a fragment) and therefore check if navigated to the fragment.
Then I check if that happened, but the testNavController stays on the same destination which is weird because it performs the click, so I decided to check the navController (The real one inside the activity), and it shows me that navigated to the correct fragment.
Here's the needed code:
#LargeTest
#RunWith(AndroidJUnit4::class)
class MapsActivityTest {
#get:Rule
var activityScenarioRule = ActivityScenarioRule(MapsActivity::class.java)
#Test
fun clickOnDrawerMaps_NavigateToAboutAppFragment() {
//Create TestNavHostController
val testNavController = TestNavHostController(ApplicationProvider.getApplicationContext())
UiThreadStatement.runOnUiThread { // This needed because it throws a exception that method addObserver must be called in main thread
testNavController.setGraph(R.navigation.nav_graph)
}
val scenario = activityScenarioRule.scenario
var navcontroller : NavController? = null
scenario.onActivity {mapsActivity ->
navcontroller = mapsActivity.navController //Get the real navController just to debug
mapsActivity.navController = testNavController //Set the test navController
Navigation.setViewNavController(mapsActivity.binding.containerFragment, testNavController)
}
onView(withId(R.id.drawerLayout)).perform(DrawerActions.open()).check(matches(isOpen()))
onView(withId(R.id.aboutAppFragment)).perform(click())
assertThat(testNavController.currentDestination?.id).isEqualTo(R.id.aboutAppFragment)
}
}
In the example they use a Fragment, which they set the fragment.requireView() on the launch of the fragment, but I think it's exactly the same.
What am I doing wrong here?
When you use ActivityScenario (or ActivityScenarioRule), your activity is brought all the way up to the resumed state before any onActivity calls are made. This means that your real NavController has already been created and used when you call setupWithNavController. This is why your call to setViewNavController() has no effect.
For these types of integration tests (where you have a real NavController), you should not use TestNavHostController.
As per the Test Navigation guide, TestNavHostController is designed for unit tests where you do not have any real NavController at all, such as when testing one fragment in isolation.

Categories

Resources