DrifterAtSea / navigation-with-animated-transitions-using-jetpack-compose

DEPRECATED - Demonstates how to create animated transitions to and from screens using Jetpack Compose.
41 stars 4 forks source link

Navigation state is lost across process death #1

Closed Zhuinden closed 3 years ago

Zhuinden commented 3 years ago

Theoretically, if you put an application in background on any screen, and Android terminates your process, then it is the OS-level contract that navigation state and the minimal state representation is preserved for when Android recreates the app (see intent extras, fragment arguments, and onSaveInstanceState).

In this case, the [current Activity] is recreated along with the ActivityRecord list that is used to synthesize previous Activities (also starting "for the first time") in the new process on back presses, while also providing them the intent argument they were created with originally.

However, this sample seems to lose all navigation state after process death, which is not correct behavior in an Android app.

DrifterAtSea commented 3 years ago

In Android, the word "process" in fact refers to the entire app and not an activity. If you terminate the app's main process, you terminate the entire app and restarting the app starts it from scratch. You can have multiple processes running but this is normally not usual for most cases. When Android terminates an activity but the main process remains alive, the activity that got killed will get restarted.

Because the Navigation Manager does keep track of the navigation stack and it remains alive throughout the lifecycle of the app, even if the main activity gets terminated, once the activity restarts, the screen factory will reconstruct the activity to the same screen state as it was before the activity got terminated. The screen factory iterates over all the screens, constructing them up to the most recent screen.

To test this out, I added an extra button on the Dummy screen to let you terminate the activity. Download the latest version. Then click on a cat and then the Adopt button to take you to the dummy screen. Then click on the button to terminate the activity. Then restart the activity. You will be automatically returned to the same screen (the dummy screen) where you were last. If you use the Back button, you'll notice that you can navigate back to all the previous screens.

Zhuinden commented 3 years ago

If you terminate the app's main process, you terminate the entire app and restarting the app starts it from scratch.

But this is not entirely true if the app is backgrounded before the app is terminated (and then restarted from launcher).

To get a sufficiently small GIF, I had to reduce the quality and size to abysmal, but maybe it's still visible:

Compose-Process-Death-Optimized-3

In a fragment-based app, terminating the app after backgrounded and then relaunching from launcher will return the app to the fragment that was previously in front, restoring the fragment arguments and the onSaveInstanceState bundle (which is why they need to be Parcelable/Serializable/ByteArray/whatever)..

You can also see this behavior in this compose-based app.

However, this currently does not seem to be happening here.

DrifterAtSea commented 3 years ago

Not sure if I follow. If you force kill your app, or Android does it due to low resources, it will in fact terminate everything that was spawned by the app including services, work manager jobs, pending intents, etc. Sure, you can have it so that it saves the state of the activity / fragment as you pointed out and it will restore this state when the app relaunches.

If your app is really terminated and you've saved its state and are restoring it when the app relaunches, you run a risk of starting your app off in a state that could in effect be very bad. Maybe Android killed your app because it became a memory hog or maybe it's accessing some badly behaved API. What you don't want is to restore the app back to the same state and in the end just end up being a bad loop with Android constantly killing it. For this reason, your app should not be restarted to the previous state unless you can ensure that it was shut down in a clean way and without exceptions. The way the Navigation Manager code is designed along with the screen factory, you can safely use Android's task manager to kill the activity. And even if Android does kill the activity for whatever reason, as long as it's not killing the app, your screen state will be restored when you launch the activity again.

The solution I designed is meant for apps that use only a single activity and no fragments. The Compose Navigation framework is probably designed the way it is to support multiple activities and fragments and maintain the state you mentioned. I don't see this applicable for apps that are single activity with no fragments and are built entirely with Compose.

Zhuinden commented 3 years ago

If you force kill your app, or Android does it due to low resources, it will in fact terminate everything that was spawned by the app including services, work manager jobs, pending intents, etc.

This is true, yes.

Sure, you can have it so that it saves the state of the activity / fragment as you pointed out and it will restore this state when the app relaunches.

This is the default behavior when you work on Android, and it is the default behavior because this is the OS-level contract for Activity behavior.

If your app is really terminated and you've saved its state and are restoring it when the app relaunches, you run a risk of starting your app off in a state that could in effect be very bad.

That's where correctly saving and restoring state via onSaveInstanceState/onCreate has always been the recommendation for correct application behavior. (see CR-1)

Every Activity/Fragment-based app has always potentially restored itself to any screen within the application as the "first screen", restoring them from the savedInstanceState. That's why they had intents, fragment arguments, savedInstanceState, etc.

What you don't want is to restore the app back to the same state and in the end just end up being a bad loop with Android constantly killing it.

I must admit I've never seen that happen, because if Android actually terminates your app, then it'll discard the Activity/Fragment records. 🤔

And even if Android does kill the activity for whatever reason, as long as it's not killing the app, your screen state will be restored when you launch the activity again.

Yes, but the Application (not Activity) being killed, with savedInstanceState + intent extras + fragment args being retained, is completely standard Android lifecycle behavior.

The Compose Navigation framework is probably designed the way it is to support multiple activities and fragments and maintain the state you mentioned.

It is designed to correctly restore the current last composable even within a single activity after process death / low memory condition.

DrifterAtSea commented 3 years ago

Android leaves it up to the developer to use onSaveInstanceState if they see it useful. I don't have any issues about saving the state of an activity or fragment if the app is relaunched from scratch after being killed off. Compose does maintain state as you pointed out, but only if you use the state management particulars like "remember". The Navigation Manager I wrote isn't concerned about saving this state because it really is up to the screen to do this as it sees fit. The Navigation Manager is really only concerned about maintaining the stack history while at the same time conveniently keeping the viewmodel state, if you decide to use that feature.

Yes, but the Application (not Activity) being killed, with savedInstanceState + intent extras + fragment args being retained, is completely standard Android lifecycle behavior.

That is the standard lifecycle for an Activity/Fragment with intents. This no longer applies to apps written entirely in Compose. There is only one activity and no fragments. Maybe Google will come up with a different lifecycle framework for composables, but I doubt it.

What I have issues with is for an app to automatically navigate to some screen after startup just to restore the entire state of the app and bring the user back to the last screen they were on. I have rarely seen apps do that. Just open up Instagram and navigate to any depthh and then kill off the app or just terminate the activity and then restart the app. You will not end up at the last screen you were at. You will always end up at the start screen. There is nothing preventing you from writing code to navigate to the last screen if that is something you want to accomplish. But as I pointed out, that's a bad idea and I have rarely seen it happen.

So as it stands, the Navigation Manager does a good job of maintaining the navigation stack while the app is alive. The more I use it and upgrade it on a daily basis, the more I realize just how much better it is than using the Compose Navigation framework.

Zhuinden commented 3 years ago

What I have issues with is for an app to automatically navigate to some screen after startup just to restore the entire state of the app and bring the user back to the last screen they were on. I have rarely seen apps do that. Just open up Instagram and navigate to any depthh and then kill off the app or just terminate the activity and then restart the app. You will not end up at the last screen you were at. You will always end up at the start screen.

In Discord, it not only reloads with the same scroll position as where I used to be, it even re-opens the bottom sheet dialog if it was previously open (maybe they use a BottomSheetDialogFragment, in which case it is not surprising).

It depends on how you kill the app. Task clear and force stop WILL fully terminate the app. If it's killed by low memory like when using the app "Fill RAM Memory", then it will actually restore itself correctly, I think even Instagram and YouTube too.

But as I pointed out, that's a bad idea and I have rarely seen it happen.

It's actually standard behavior, this is how all apps that use Activities and/or Fragments via default means have always worked, people just often don't know about it.

the more I realize just how much better it is than using the Compose Navigation framework.

It is theoretically promising, but you don't seem to interop with SaveableStateHolder or rememberSaveable yet.


I personally find singleton navigation managers shady, because then they are shared between tasks. My nav manager is scoped to Activity retained scope instead, that way onSaveInstanceState can also be reasonably intercepted.

The old (and maybe even the new) version of Magellan had the same problem, they simplified navigation to "no longer handle process death" but thus opt for functionality loss.

Imagine trying to enter a verification code from Gmail during the registration process, and your app reloads on the splash screen instead due to low memory condition. This is something we need to consider while we are working on Android apps.

Zhuinden commented 3 years ago

Please note that this bug is not fixed. I can provide better quality video if needed.