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 :)
Related
I'm having a problem using compose navigation and bottom navigation bar. Using these navOptions, the state is not always restored when going back to a previously selected tab in the bottom navigation.
val topLevelNavOptions = navOptions {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
When clicking through the screens sporadically, I lose the state and get new instances of the viewmodels injected using hilt.
Similar behaviour be observed in the nowinandroid app.
Clone the repository and add the following to InterestsViewModel.kt:
override fun onCleared() {
super.onCleared()
Log.d("asd", "onCleared")
}
Then proceed to sporadically click through the screens using the bottom navigation. The log statement above will be printed... sometimes
Does anyone know why the state is lost/reset and how to prevent that from happening?
EDIT
I believe this is a bug in navigation compose. Reported here https://issuetracker.google.com/issues/265838050
When working with Compose Navigation and calling NavController.popBackStack() multiple times on the first shown Composable (startDestination) the backnavigation does not work anymore. For example when navigating to another Composable from this point on and then calling popBackStack does not have an effect.
For some Reason the size of the NavController.backQueue is at least 2 even though it's supposed to only show one Composable. If popping the backstack lower than that, the navigation does not seem to work anymore. (I don't know why)
Therefore I wrote the following simple extension function which prevents popping the BackQueue lower than 2:
fun NavController.navigateBack(onIsLastComposable: () -> Unit = {}) {
if (backQueue.size > 2) {
popBackStack()
} else {
onIsLastComposable()
}
}
You can use it like this:
val navController = rememberNavController()
...
navController.navigateBack {
//do smth when Composable was last one on BackStack
}
A note before I show any code - I know that SingleLiveEvent is no longer recommended. But I have code here that uses an observed navigation event to open fragments and dialog fragments.
It works fine for fragments. But if I double tap any button that opens a dialog fragment, the SingleLiveEvent fires twice. This is counter intuitive to what I thought getContentIfNotHandled was supposed to do. That code seems to return a value every time, and hasBeenHandled inside the Event class is false both times.
This means that my open dialog function is called twice. The first time is fine, but then because I'm trying to open a dialog, while the dialog is open, I get a 'no such destination' error.
Here is my code
private fun setupEventObserver()
{
viewModel.navigateToSettingEvent.observe(viewLifecycleOwner) { event ->
event.getContentIfNotHandled()?.let {
handleEvent(it)
}
}
}
private fun handleEvent(appletEvent: AppletEvent)
{
when (appletEvent) { /* See different actions below */
is AppletEvent.NestedMenuEvent -> showNextSettingsFragment(appletEvent.itemNavigatingTo)
is AppletEvent.SchedulerFlowEvent -> showDatePickerDialogFragment(appletEvent.itemToSchedule))
}
}
private fun showDatePickerDialogFragment(setting: SettingData)
{
val action =
SettingsFragmentDirections.actionHomeItemSettingsFragmentToDatePickerDialogFragment(
setting.id,
setting.title
)
findNavController().navigate(action)
}
So I'm just struggling to figure this one out. I thought this was what SingleLiveEvent was trying to address.
I've tried setting a bool for when the dialog is open, but I don't know where to set that back to false when the dialog is closed.
I've also experimented with .value and postValue() for the nav event, but both give the same problem.
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.
I am implementing Android Architecture Components. Imagine a case like the following where your Fragment is observing a LiveData to change its UI. User minimizes the app and the state is changed (in my case from the repository). So the Observer from the Fragment is not triggered with a change because the Fragment is not visible. But then, when the user comes back to the app it doesn't trigger the new state. If the state is changed again (while the Fragment is visible), the Observer receives the change. Do you know any way to force an update when fragment is visible again?
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
vm = ViewModelProviders.of(this, viewModelFactory).get(MyViewModel::class.java)
vm.getStatus()?.observe(this, Observer<MyRepository.Status> { status ->
if (status != null) {
when (status) {
NONE -> setNoneUI()
LOADING -> setLoadingUI()
CONTENT -> setContentUI()
ERROR -> setErrorUI()
}
}
})
}
Actually your activity should receive the latest state from LiveData when it is visible again, as explained in this video and described here:
Always up to date data: If a lifecycle becomes inactive, it receives the latest data upon becoming active again. For example, an activity that was in the
background receives the latest data right after it returns to the
foreground.
I just tested it in my own application, works as described. So there must be another error in your application: Do you set the LiveData correctly from your repository class? Maybe a different thread / postValue problem?