Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Instance state not saved when app is killed by OS #6827

Open
LouisCAD opened this Issue Nov 12, 2016 · 112 comments

Comments

@LouisCAD
Copy link

LouisCAD commented Nov 12, 2016

What is instance state, and why it exists

On Android, an Activity can be killed at any time by the system. This happens usually when Android needs memory when your Activity is not in the foreground, or because of a non-handled configuration change, such as a locale change.
To avoid the user having to restart what he did from scratch when Android killed the Activity, the system calls onSaveInstanceState(…) when the Activity is paused, where the app is supposed to save it's data in a Bundle, and passes the saved bundle in both onCreate(…) and onRestoreInstanceState(…) when the task is resumed if the activity has been killed by the system.

The issue about it in flutter

In the flutter apps I tried (Flutter Gallery, and the base project with the FAB tap counter), if I open enough apps to make Android kill the flutter app's Activity, all the state is lost when I come back to the flutter activity (while not having remove the task from recents).

Steps to Reproduce

  1. Install Flutter Gallery
  2. Open the device's settings, and in developer options, switch "Don't keep activities" on. (see the screenshot)
    dev_option
    This will allow to simulate when Android kills Activities because it lacks memory and you're not in the foreground.
  3. Open the Flutter Gallery app and go anywhere other than the main screen.
  4. Go to the launcher by pressing the device's home button.
  5. Press the overview button and return to Flutter Gallery. Here's the bug.

What's expected: The app is in the same state that where we left off, with untouched UI.
What happens: The activity is restarted from scratch, losing all the UI state, even really really long forms.

@eseidelGoogle

This comment has been minimized.

Copy link
Contributor

eseidelGoogle commented Nov 12, 2016

#3427 is also likely related.

@LouisCAD

This comment has been minimized.

Copy link
Author

LouisCAD commented Nov 12, 2016

@eseidelGoogle That's right. In which format could the flutter Activity state be saved, if it can be at all as of the current version?

@Hixie

This comment has been minimized.

Copy link
Contributor

Hixie commented Nov 12, 2016

Right now we don't do anything to save anything.

The framework itself has very little state worth saving -- it's all animation and stuff like that -- so it may be that we always leave this up to the app to do. We should probably expose it at the Dart level though. (Right now it's only exposed at the Java level.)

@LouisCAD

This comment has been minimized.

Copy link
Author

LouisCAD commented Nov 12, 2016

@Hixie On Android, all framework Views have their state automatically saved and restored by the system, which only requires the developer to save manually the non UI part of the instance state. Shouldn't flutter work the same way for all UI widgets to prevent developers from having to write all the boilerplate each time to save where the user was, the scroll position, the enabled state of some button, the state of the previous screen and so on…?

@Hixie

This comment has been minimized.

Copy link
Contributor

Hixie commented Nov 14, 2016

In Flutter, there's very little framework state to save. For example, the enabled state of a button is not state, it's input provided by the application. The current route history is stored in the framework, but not stored in a state that the framework can rebuild (since it's all instances of objects provided by the application code). The scroll position is about the only thing we could actually store (and we do save that in a store currently, just not one that survives the app).

But in any case we should definitely do better than today.

@LouisCAD

This comment has been minimized.

Copy link
Author

LouisCAD commented Nov 25, 2016

As an experienced Android Developer, I'd like to take part to the conversation with iOS developers too when it'll be discussed so flutter can be the perfect framework to build iOS and Android apps.
I think the persistence ability of the framework may be important to save data, preferences, cache data and states in the most developer friendly possible way.

@Hixie Hixie added this to the 4: Make shippers happy milestone Feb 27, 2017

@Takhion

This comment has been minimized.

Copy link

Takhion commented Sep 24, 2017

Whoa I didn't realise this was the case? This is actually critical, and it's worth mentioning that on devices with less memory the process could be killed quite easily!

What about saving the PageStore (or related) as a byte stream through serialization.dart in onSaveInstanceState?

@LouisCAD

This comment has been minimized.

Copy link
Author

LouisCAD commented Sep 27, 2017

@Takhion Could you link "PageStore" you're mentioning? I'm interested in trying to workaround this issue since it seems it won't be fixed any soon (31st december of 2029 is a liiittle far IMHO)

@Takhion

This comment has been minimized.

Copy link

Takhion commented Sep 27, 2017

@LouisCAD sure it's here: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/page_storage.dart

I think you'll need to save more than that though: at least the current route(s) and maybe some state?
What do you think @Hixie?

@Hixie

This comment has been minimized.

Copy link
Contributor

Hixie commented Sep 29, 2017

We don't currently do anything to make this easy. We haven't studied this problem in detail yet. For now I recommend storing the information you want to persist manually, and applying it afresh when the app is restored.

@sethladd

This comment has been minimized.

Copy link
Contributor

sethladd commented Oct 7, 2017

Do we expose the necessary lifecycle events ("you're about to be killed!", "congrats, you're now restored") into Dart code, so developers can handle persisting state? That seems like the sufficient capabilities to unlock devs to explore solutions. Thoughts?

@LouisCAD

This comment has been minimized.

Copy link
Author

LouisCAD commented Oct 7, 2017

@sethladd Just exposing it to developers is not enough IMHO. Flutter needs to save UI state too, without developers having to do it manually repeatedly for each app with dirty and hardly maintainable verbose boilerplate code.

@sethladd

This comment has been minimized.

Copy link
Contributor

sethladd commented Oct 7, 2017

@LouisCAD thanks for the feedback. I'm thinking in steps... what's step one? Do we have the lifecycle events exposed yet?

@Takhion

This comment has been minimized.

Copy link

Takhion commented Oct 7, 2017

@sethladd all I could find is this, which doesn't expose any callback for saving/restoring instance state

@mit-mit

This comment has been minimized.

Copy link
Member

mit-mit commented Oct 9, 2017

@Hixie you mentioned over in #3427 that we do have hooks for lifecycle. Did you mean https://docs.flutter.io/flutter/widgets/WidgetsBindingObserver-class.html ?

@DanielNovak

This comment has been minimized.

Copy link

DanielNovak commented Oct 10, 2017

Is Flutter using multiple Activities by default? (e.g. if you go to a new screen).

Process death is fairly common, the user hits the Home button and launches some other memory/CPU intensive application and your application will be killed in the background (or your application has been in the background for too long). It's an integral part of Android OS - every screen (Activity) should be able to persist and restore its' state and the process can be killed at any time after onStop().

Android will recreate the backstack of Activities if the user returns to a killed app, the top Activity is created first and then activities in the backstack are recreated on-demand if you go back in the backstack history. This can be a bigger issue in case Flutter is using multiple Activities (not sure if it does).

This means that if you don't have state saving implemented then the system will recreate the Activities but everything else is lost (process was killed), thus leading to inconsistency and crashes.

I wrote an article about process death on Android https://medium.com/inloop/android-process-kill-and-the-big-implications-for-your-app-1ecbed4921cb

@DanielNovak

This comment has been minimized.

Copy link

DanielNovak commented Oct 10, 2017

Also there is no (clean) way to prevent Android from killing your application once it's past the onStop() state (after clicking Home or if the app is just not the current foreground app). So somehow you have to deal with it. Default Android widgets are saving their state (e.g. entered text in EditText) automatically into the Bundle instance. Your Activity will notify you that it's necessary to store your state by calling onSaveInstanceState(Bundle). So maybe Flutter should be able to forward this onSaveInstanceState callback from the Activity to your "screens". You will get the Bundle with the saved state back in the onCreate(Bundle savedInstanceState) or onRestoreInstanceState(Bundle savedInstanceState) lifecycle callback in your activity.

So to recap - Flutter could maybe forward the onSaveInstanceState() and onRestoreInstanceState() callbacks to the developer. Ideally you would also wrap the Android Bundle object into something that can be also used in Flutter. The next step would be that all Flutter widgets inside the screen would be also notified about these callbacks and use them to persist their current state.
The Android OS will then take the Bundle and actually persist it on disk so that it's not lost in case the process is killed and can be deserialized again.

Good luck with that :-). Please take care and don't introduce too much of this Android state / lifecycle hell to Flutter.

I am not sure how that works on iOS - but I think there is something similar but it's not "necessary" to use it (?).

@zoechi

This comment has been minimized.

Copy link
Contributor

zoechi commented Oct 10, 2017

A lifecycle callback would be fine for me. I would just store/load the serialized redux state.

@sethladd

This comment has been minimized.

Copy link
Contributor

sethladd commented Oct 10, 2017

Thanks for all the feedback! @Takhion also offered to help here. Perhaps an API design and a library is a good start? If that library works, we can look to integrate more formally. Also, the library will help identify what we need to do at the low-level engine (if anything). Basically: what's the concrete API proposal?

@Takhion

This comment has been minimized.

Copy link

Takhion commented Oct 11, 2017

Is Flutter using multiple Activities by default? (e.g. if you go to a new screen)

@DanielNovak Flutter uses it's own "routing" system and by default it integrates with Android through a single View in a single Activity. You can have a hybrid Android/Flutter app and as such potentially multiple Activities and Flutter Views, but in that case you could easily save/restore instance state through Android directly.

I would just store/load the serialized redux state

@zoechi you probably don't want to do that because the saved instance state Bundle has to go through IPC with a hard limit of 1MB for your entire Android app state. Instance state, by definition, should only be the pieces of data that would allow you to recreate the same conditions of whatever in-progress activity the user is performing, so: text input, scroll position, current page, etc. Anything else should either be persisted on disk or derived from other state.

@raju-bitter

This comment has been minimized.

Copy link
Contributor

raju-bitter commented Oct 18, 2017

Flutter exposes the Android onPause lifecycle event. The Android onDestroy() lifecycle event is not exposed. I guess the right approach would be to hook into the onPause() event for storing instance related state.
To be compatible with Android's onSaveInstanceState() and onRestoreInstanceState(), maybe it makes sense to add similar methods to Flutter widgets.
@sethladd @LouisCAD Did anyone create a design proposal?

@DanielNovak

This comment has been minimized.

Copy link

DanielNovak commented Oct 18, 2017

@raju-bitter onPause() is not optimal, it's better to hook into onSaveInstanceState(). Here is the javadoc from onSaveInstanceState which mentions that onPause() is called more regularly than onSaveInstanceState (you would be triggering state saving more often than necessary or when it's not necessary at all):

Do not confuse this method with activity lifecycle callbacks such as onPause(), which is always called when an activity is being placed in the background or on its way to destruction, or onStop() which is called before destruction. One example of when onPause() and onStop() is called and not this method is when a user navigates back from activity B to activity A: there is no need to call onSaveInstanceState(Bundle) on B because that particular instance will never be restored, so the system avoids calling it. An example when onPause() is called and not onSaveInstanceState(Bundle) is when activity B is launched in front of activity A: the system may avoid calling onSaveInstanceState(Bundle) on activity A if it isn't killed during the lifetime of B since the state of the user interface of A will stay intact.

@zoechi

This comment has been minimized.

Copy link
Contributor

zoechi commented Oct 18, 2017

@Takhion

instance state Bundle has to go through IPC with a hard limit of 1MB for your entire Android app state

That's not my issue.
I just need to know when to persist/restore, persisting myself on disk is fine for me.

@gitspeaks

This comment has been minimized.

Copy link

gitspeaks commented Dec 19, 2018

@aletorrado

The "Solution A" seems perfectly plausible to me when using appropiate locks, and no ANR should be triggered if the amount of work is reasonable.

I agree. when ... Where the Android Model is far more simple and robust since it does not require the developer to deal with locks, it does not persist transient state to disk and it retains state only for those cases where it's relevant something you (flutter) can't do without hooking into the Android lifecycle events and the onSaveInstance facility - to begin with, so what is it good for?

Also, by embracing the platform's notification mechanism it avoids doing unnecessary serialization work all the time, which may be expensive to do for every single keystroke.

There is no requirement to persist every key stroke but rather a snapshot consisting of selected states

@LouisCAD

This comment has been minimized.

Copy link
Author

LouisCAD commented Dec 19, 2018

@Zhuinden

This comment has been minimized.

Copy link

Zhuinden commented Dec 19, 2018

The real trickery is building up this ByteArray you speak of 🙂

@LouisCAD

This comment has been minimized.

Copy link
Author

LouisCAD commented Dec 20, 2018

@Zhuinden

This comment has been minimized.

Copy link

Zhuinden commented Dec 20, 2018

No. Once you have the byte array, it is very easy. Just show a loading indicator while you're loading it (assuming you've told yourself with channels that you need to load it).

@LouisCAD

This comment has been minimized.

Copy link
Author

LouisCAD commented Dec 20, 2018

@mehmetf

This comment has been minimized.

Copy link
Contributor

mehmetf commented Dec 20, 2018

A bunch of us discussed this a bit more here: mehmetf#1. Relevant points to this issue:

  • To correct (#6827 (comment)): Google Ads was tested with "Don't Keep Activities" turned on. The app process does not die in that case so my explanation at (#6827 (comment)) stands. As @gitspeak and @Zhuinden have pointed out, this is not a good way to test for the scenarios we are guarding against as it does not work that way in practice.

  • Whenever the application is backgrounded, View.onSaveInstanceState is called on the UI thread for every View in the hierarchy which has save enabled. So, a good solution from developer's point of view would be for FlutterView to implement this. To prevent race conditions with the UI thread (which you can't/shouldn't block), this method should not reach into the Flutter application as there's no synchronous way to do it.

  • Whatever onSaveInstanceState saves cannot be larger than 1MB so saving the entire isolate state is out of question. Instead, we can possibly come up with a way to split the code, assets and data that is contained in the isolate memory and dump only the data portion serialized into a parcelable.

  • When the view is later restored, FlutterView creates an isolate and unpacks the previously saved data portion if any.

  • When the app is backgrounded, the state should presumably stop changing on the Flutter side. There will always be corner cases so coming up with a list of best practices for Flutter apps would be nice to accompany this solution (considering both lifecycle and memory pressure).

  • I would imagine this would be a far more pressing issue when it comes to emerging markets such as India where Flutter is expected to run on low-end phones. Android Go is also rumored to have a far more aggressive memory management strategy.

@pepegich

This comment has been minimized.

Copy link
Contributor

pepegich commented Dec 27, 2018

  • They create and maintain their main isolate when the application starts (not the activity). Then, they pass this isolate to the Flutter view when it is created.

can you provide an example this code?

@mehmetf

This comment has been minimized.

Copy link
Contributor

mehmetf commented Dec 27, 2018

They create and maintain their main isolate when the application starts (not the activity). Then, they pass this isolate to the Flutter view when it is created.

can you provide an example this code?

I can't provide a copy/paste type of example but here's what's happening:

Application

  • In your Application.onCreate, call initialization functions for FlutterMain. (startInitialization and ensureInitializationComplete). You can find references to these in the flutter/engine repo.
  • Create a new FlutterNativeView object, pass in the Application instance as context.
  • Run your bundle in this native view (Look for examples of runFromBundle).
  • Implement an interface in this Application that lets you return this native view (something like FlutterNativeViewProvider).

Activity

  • Extend FlutterFragmentActivity and override these two methods:
  @Override
  public FlutterNativeView createFlutterNativeView() {
    FlutterNativeViewProvider provider = (FlutterNativeViewProvider) getApplication();
    return provider.getFlutterNativeView();
  }

  @Override
  public boolean retainFlutterNativeView() {
    return true;
  }

Something along these lines should get you going. Note that your Flutter application will start running before an Activity (and thus a window) is available. Therefore you should not be calling runApp() in your main function until you receive a signal from Activity that it is ready. You can do that via PlatformChannels and one place to signal that would be in createFlutterNativeView().

Disclaimer(s)

  1. As I mentioned above, we don't quite condone this pattern since it does not really solve this particular problem and it is quite messy.

  2. Note that many of these APIs are slated to change. Since add2app use cases are still evolving, the Android and iOS embedding APIs are not quite stable yet. We will announce breaking changes on flutter-dev@ as usual.

@jsroest

This comment has been minimized.

Copy link

jsroest commented Dec 27, 2018

@passsy, @gitspeak

Recovery solution A: Your own saved instance state file

To be sure that you have a valid state-file you can write to a .tmp file and then rename when writing is done. If you also keep the previous state file, you always will end up with valid state. Also when the process is killed while saving the state.

@gitspeaks

This comment has been minimized.

Copy link

gitspeaks commented Dec 27, 2018

@jsroest

you always will end up with valid state.

But not necessarily the correct state, which is kind of the whole point..

There is also no requirement for persisting state to non-volatile memory to begin with ..

@jsroest

This comment has been minimized.

Copy link

jsroest commented Dec 28, 2018

@gitspeak

you always will end up with valid state.

But not necessarily the correct state, which is kind of the whole point..

There is also no requirement for persisting state to non-volatile memory to begin with ..

In many situations having a consistent state is more important than having 'most of the latest values'. Relational databases used with transactions are an example of that. Which in my opinion is also a great place to store state.

Persisting to non-volatile memory has some advantages; The process is also killed on 'phone drops', battery changes, hardware failures, os updates, reboots in general etc, so the state does persist when saved to non-volatile memory.

@gitspeaks

This comment has been minimized.

Copy link

gitspeaks commented Dec 28, 2018

@jsroest Your arguments are valid but IMHO not applicable to this particular issue since the "state" being alluded to here is transient UI state (e.g Scroll position, active selection(s), temporary text input, etc...). Keep in mind that restoring this type of data is only required in those situations where the user has not explicitly discarded the "Screen" but user-input can still get lost, like when a user switches to another app. There is no user-expectation to recover transient state upon power-loss much like there is no user expectation that the browser will scroll the last displayed webpage to the exact offset prior to a system power loss. Point being, the nature of the data considered to be "transient state" is significant and the discussion here (Android onSaveInsatnceState facility in particular) is about handling that type of data.

@jsroest

This comment has been minimized.

Copy link

jsroest commented Dec 28, 2018

@gitspeak
I write programs for the enterprise. For example programs that are used in warehouses by order pickers, or other example in the retail business, where the shop owner scans items to order from his supplier. My customers expect the application to start where they left it, no matter what happened. That includes transient state, but also the navigation-stack. Could you explain why this is not a perfect fit for the OnSaveInstanceState facility as discussed here? Maybe the user expectation in the consumer market is different, but the same logic to save and restore state can be used IMHO.

@gitspeaks

This comment has been minimized.

Copy link

gitspeaks commented Dec 28, 2018

@jsroest

My customers expect the application to start where they left it, no matter what happened.

Sorry, I don't know how to build applications for such customers, perhaps someone else can help ...

@mehmetf

This comment has been minimized.

Copy link
Contributor

mehmetf commented Dec 28, 2018

@jsroest That's a great question but is not within the scope of this issue. Flutter is limited by what the platform itself can do and, as @gitspeak mentions, Android's onSaveInstanceState is about saving transient UI state where the data lifecycle is different than what you want. Local storage or remote storage (if you want the state to survive across uninstalls and devices) would give you what you want but it has caveats, which are again outside the scope of this issue.

Further recommended reading:

https://developer.android.com/topic/libraries/architecture/saving-states [Note for instance how this article separates local storage from onSaveInstanceState]
https://developer.android.com/guide/topics/data/data-storage
https://firebase.google.com/docs/firestore/

@Zhuinden

This comment has been minimized.

Copy link

Zhuinden commented Dec 28, 2018

@jsroest if your customers expect the app to return where it was even if it was force-stopped, task-cleared or the OS was restarted, then that is something completely custom, and not what people are talking about here.

onSaveInstanceState was saving the task state for a given task, but it was dropped on task clear or force stop or OS reboot.

@gitspeaks

This comment has been minimized.

Copy link

gitspeaks commented Dec 28, 2018

@mehmetf I think this issue has indeed reached a level of verbosity at which it can be quite challenging to grok. With @LouisCAD blessing, I suggest you close this issue and open a new one which starts with your most recent summary (perhaps somewhat adapted to emphasis the problem statement and scope) along with back references to previous discussions.

@mehmetf

This comment has been minimized.

Copy link
Contributor

mehmetf commented Dec 28, 2018

I concur but I will leave that up to @matthew-carroll; he will likely own this issue.

@jsroest

This comment has been minimized.

Copy link

jsroest commented Dec 29, 2018

Thanks for the feedback.

I don’t want to push things, but I think I was not clear enough describing what I am looking for. I am not up to par with all the terminology used here on this thread. I see a lot of similarity between what I would like to see in Flutter and what @LouisCAD and @passsy are describing.

My programs usually consist of the following parts:

  • Backend for pushing data from the steady state when the user is done with a job.
  • Local sqlite database for the steady state
  • In memory data-structure called ‘variables’ for the transient state that can be serialized to non-volatile memory so that it can outlive the process.

The ‘variables’ data-structure only has simple classes with properties that are serializable. An important part of this ‘variables’ datastructure is the screenstack. (Simply List). The screen that’s on top, is the last screen the user interacted with. The one below is the one that the user navigates to when pressing 'back'.

I can imagine that the OnSaveInstanceState can be used on the Android platform to trigger a serialization of this datastructure. Something similar has to be found on the other platforms (iOS, future: win, mac, web), but as @passy suggests other key points also may trigger this and that may be good enough.

When the program starts, it will check if a serialized datastructure is available and if the current version of the program is the same as the version that serialized it. This can be done just by comparing version numbers. We do not want to load a datastructure that is not compatible.

If this all adds up then the datastructure is deserialized and available in memory. The program loads the screen that is on top of the stack. The screen itself loads its transient state from the datastructure. Now we have a program that survives process deaths. (Or am I missing something? This definitely works on my past projects on windows mobile, Xamarin forms and asp.net. I think it should also work in Flutter).

Sample application, where SXXX stands for a screen with a number. The number is only used to remind the programmer which screens belongs more or less together.

S100 Main menu
  S200 Order pick
    S210 Select order
      S220 Confirm pick location
        S230 Pick nr of pieces
          S240 Report shortage
        S250 Scan dropoff location
    S300 Cycle count
      S210 XXXXXX
        S220 XXXXXX
      S230 XXXXXX
    S300 Reprint labels
      S310 XXXXXX

Sample variables datastructure

Class variables {
	Class AmbientVariables {
		….
	}
	
	Class ScreenStack{
		List<string> ScreenStack;
	}

	Class Orderpick {
		int selected_order;
		string comfirmed_location;
		string nr_picked;
		….
	}

	Class CycleCount {
		….
	}

	Class ReprintLabels {
		….
	}
}

All that I would like to see in flutter is probably already there, except the recreation of the screenstack aka navigationstack. I have no idea how to recreate this object tree in memory. I am also not sure if I should want to recreate it. I can also make a navigation stack of my own and implement this in flutter as a single page application. The same as I have done in previous projects. But then I would loose a lot of built-in goodies from the MaterialApp and Scaffold classes, where the back arrow/button is the most obvious one.

I realize that this does not automatically save all transient state. For example, selected texts, position of lists, position of a specific animation. But as the programmer, you can decide per screen what is needed to save. Although that is exactly what @LouisCAD is trying to prevent because all of the boilerplate code needed.

Should I make a new issue or does this fit into this thread?

Thanks again for the feedback, I really appreciate it.

@mehmetf

This comment has been minimized.

Copy link
Contributor

mehmetf commented Dec 29, 2018

@jsroest Thank you for the detailed explanation. Please post this on StackOverflow with the flutter tag. You are essentially asking for a recommendation on how to structure and build your routes and how to save the last visited route in the persistent storage.

You might think that solving this particular issue solves your problem too but that's not true. You can PM me at my github handle @ gmail.com if you don't understand why.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.