(* one of the meanings of the word flutter - flutter)
We understand, is there a life state saving in Flutter application. What happens if the OS decides to restart it. Where does user input and navigation go, and how to handle it.
Disclaimers:
Flutter , a framework for cross-platform development of mobile applications, was announced on Google I / O 17.
Flutter is made in C and C ++, it implements its 2D rendering engine (WebView is not used). Something similar to React, development is conducted in the language of Dart . The code is written once, and at assembly it is compiled in native for each platform.
Flutter (hereinafter referred to as flutter) beckons with a list of advantages:
In order to avoid the Crusades, the pros and cons: Of course, you cannot choose one tool that will be suitable for solving absolutely all problems, each tool is good for its context. It seems to me that it would be convenient to use flutter for small projects with conditionally “typical” interfaces and use cases. In this case, he would seriously shorten the process and get rid of the routine work (writing the same code for two platforms + trying to make it “on android like on ios”).
I’m worried about a few moments. They need to be clarified before rushing to do something for production:
Let's start in order. In this article we will deal with life cycle issues. Perhaps I have a typical paranoia android developer. The first thing that comes to my mind when I find out about a new solution is “Does it ensure the preservation of the state? What happens if the activation dies? And if the application process dies? ”
Well, check it out! I create a small demo project from the Kodlab . This is an application that lists randomly generated words in the list. As you scroll, new words are added, so the list is conditionally endless. You can also add these words to your favorites and view your favorites on a separate screen.
I put the flag “do not keep activities”, launch, minimize, deploy, and ... bingo! Well, that is quite the opposite, but the pessimist in me says “I warned you” and rubs his hands.
What's happening:
On the first two points (the list of words is created anew, the “favorites” is reset) everything is in order - this is about business logic, no one expects such things from the framework. If I want the words to be saved until the user restarts the application, then save them somewhere, at least just in shared preferences.
On the third point (the position of the scroll is not saved) - it seems to be normal. You never know what happens to the data when you restart, you can save here and not necessary. And RecyclerView would not save the scroll position automatically either.
For the sake of interest, I checked what is happening with TextField (analogous to EditText). It turned out that the user input from it in such a situation disappears, which seems to be quite bad. Then I went to look at other widgets that allow you to do input: Slider, Switcher, CheckBox, etc.
It turned out that here, in principle, the logic of input organization is slightly different (different from Androids). In general, widgets do not store user input. That is, if you poke into the checkbox, there will not be a tick. For it to appear, you need to have a separate field for this, transfer it to the widget, and change the field to a click event. The field changes -> the drawing changes.
If we build the chain further, the loss of values ​​of these fields with the state will mean resetting user input.
And the bad news: if you store these fields somewhere in the in-memory cache, they will be lost if the process is restarted. Moreover, if you store them somewhere in the Dart (in the field of any class, even if it is singleton), they will be lost even if the activation is restarted.
But when you change the orientation, everything will be fine. Because inside the flutter - one view in one activit, on which the whole drawing takes place. And when you change the orientation of this activity is not recreated.
On the fourth point (navigation is not saved) - very disappointing. Android saves navigation, but no flutter.
I'm going to google and find out that:
a) I was not the only one who asked this question, people are actively discussing it on a githaba;
b) the authors of flutter do not yet do anything to save the navigation / state. Provide developers to handle this on their own.
To quote their answers:
We don't currently do anything to make this easy. We haven't studied this problem in detail yet. For now you need to save the app.
Total of troubles:
See if there is an inexpensive way to solve this. I need a proof-of-concept. Hello bikes!
As an example, I will continue to torment that demo application from the Kodlab. Task: save navigation and user input. In this case, let the user input be a scroll position, so as not to complicate. Plus, I will save the generated words and “favorites” in shared preferences, but this does not apply to the topic - I will not describe it.
I changed the demo a little to make it easier to deal with it: I took the widget with random words into a separate file, replaced WordPair with lines, and even little things.
+ Everything that is well described in the codebook, I will not repeat here. About the basic principles, the structure of the application, the tree of widgets, the logic of forming a list of words, see there.
I want to save user input and navigation in a bundle (remember that there is only one activity). Obviously, you will need communication between Dart and Android. Let's figure out how to fix it ( documentation ). On the flutter side, you need to create a MethodChannel:
save(String key) async { const platform = const MethodChannel('app.channel.shared.data'); platform.invokeMethod("save", /* */); }
And on the side of the android create MethodChannel with the same name:
MethodChannel(getFlutterView(), "app.channel.shared.data") .setMethodCallHandler { call, result -> if (call.method.contentEquals("save")) { // } }
In what format to save / transfer data? On the Dart side, this can be anything, and any type can be passed through the MethodChannel. But on the android side, I want to deal with something uniform that I will put in the Bundle. To begin with I will try the data (whatever it may be) in json, json in strings, strings in a bundle.
First, let's deal with user input. Let's get an abstract class that will store state data in itself:
abstract class Restorable { save(String key); Future<Restorable> restore(String key); }
The key argument is required to associate a specific Restorable with a specific widget. That is, when creating widgets you will need to give them unique keys.
The implementation to save the position of the scroll will look like this:
class RandomWordsInput implements Restorable { double scrollPosition = -1.0; RandomWordsInput(); save(String key) async { String json = JSON.encode(this); const platform = const MethodChannel('app.channel.shared.data'); platform.invokeMethod("saveInput", {"key": key, "value": json}); } Future<RandomWordsInput> restore(String key) async { const platform = const MethodChannel('app.channel.shared.data'); String s = await platform.invokeMethod("readInput", {"key" : key}); if (s != null) { var restoredModel = new RandomWordsInput.fromJson(JSON.decode(s)); scrollPosition = restoredModel.scrollPosition; } else { _empty(); } return this; } _empty() { scrollPosition = 0.0; } }
In order not to write serialization with my hands, I use the json_annotation library. How to use is described on the flutter website .
On the side of the android in the activit we will create a field for data storage:
var savedFromFlutter: MutableMap<String, String> = mutableMapOf()
In onCreate, we forward the methods:
MethodChannel(getFlutterView(), "app.channel.shared.data").setMethodCallHandler { call, result -> if (call.method.contentEquals("save")) { savedModels.put(call.argument<String>("key"), call.argument<String>("value")) } else if (call.method.contentEquals("read")) { result.success(savedModels.get(call.argument<String>("key"))) } }
And do the save / restore:
override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putParcelable("savedFromFlutter", toBundle(savedModels)); } override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) savedModels = fromBundle(savedInstanceState.getParcelable<Bundle>("savedFromFlutter")) }
Now you need to teach the widget to be saved and restored using Restorable. In our example, there is a RandomWords widget:
class RandomWords extends StatefulWidget { @override createState() => new RandomWordsState(); }
And his condition looks like this:
class RandomWordsState extends State<RandomWords> { final _suggestions = <WordPair>[]; Widget _buildSuggestions() { return new ListView.builder( padding: const EdgeInsets.all(16.0), itemBuilder: (context, i) { if (i.isOdd) return new Divider(); final index = i ~/ 2; // If you've reached the end of the available word pairings... if (index >= _suggestions.length) { // ...then generate 10 more and add them to the suggestions list. _suggestions.addAll(generateWordPairs().take(10)); } return _buildRow(_suggestions[index]); } ); } }
When creating a widget, we will pass on the key to it:
class RandomWords extends StatefulWidget { final String stateKey; RandomWords(this.stateKey); @override createState() => new RandomWordsState(); }
In the RandomWordsState, we set the field to the state:
class RandomWordsState extends State<RandomWords> { RandomWordsInput input = new RandomWordsInput(); RandomWordsState() { _init(); } // … }
To control the scrolling position, you need a ScrollController:
final ScrollController scrollController = new ScrollController();
The _init () function will read the saved state and move the scroll to the position:
_init() async { RandomWordsInput newInput = await model.read(widget.stateKey); setState(() { input = newInput; scrollController.jumpTo(input.scrollPosition); }); }
The function for building the widget changes as follows:
Widget _buildSuggestions() { return new NotificationListener( onNotification: _onNotification, child: new ListView.builder( padding: const EdgeInsets.all(16.0), controller: scrollController, itemBuilder: (context, i) { // … } ),); }
The _onNotification function updates the scroll position:
_onNotification(Notification n) { input.scrollPosition = scrollController.position.pixels; input.save(widget.modelKey); }
This widget is now created with the key:
class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text(widget.title), ), body: new RandomWords("list"), ); } }
Now the position of the scroll is saved between restarts of activation, cheers.
To begin with, let's rewrite the transition to another screen in our example, we will use named routes (described in the documentation ).
We list the routes (we have only one) when creating the application:
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', theme: new ThemeData( primarySwatch: Colors.blue, ), home: new MyHomePage(title: 'Startup Name Generator'), routes: <String, WidgetBuilder>{ '/saved': (BuildContext context) => new SavedPage(title: 'Saved Suggestions'), }, ); } }
Let's start a class that will save the history of transitions:
class Routes { static Queue<String> routes = new Queue<String>(); static var _firstTime = true; }
As in Restorable, let's create methods for saving and restoring:
static save() async { const platform = const MethodChannel('app.channel.shared.data'); platform.invokeMethod( "saveInput", {"key": "routes", "value": JSON.encode(routes.toList())}); } static restore(BuildContext context) async { if (!_firstTime) { return; } const platform = const MethodChannel('app.channel.shared.data'); String s = await platform.invokeMethod("readInput", {"key": "routes"}); if (s != null) { routes = new Queue<String>(); routes.addAll(JSON.decode(s)); } _firstTime = false; for (String route in routes) { Navigator.of(context).pushNamed(route); } }
That is, when restoring, we simply take all the saved routes and restore the chain of screens.
It remains when you go to the screen with your favorites to save the route, and when you go back to remove it. With the transition, everything is simple, edit the function that does it:
void _pushSaved() async { Routes.routes.addLast('/saved'); await Routes.save(); Navigator.of(context).pushNamed('/saved'); }
Welcome back a little trickier. To catch the moment when the user clicks "Back", you need to wrap the widget on the screen with the "favorites" in WillPopScope. And also start a function (here _onWillPop), which will handle pressing the “Back” button:
class _SavedPageState extends State<SavedPage> { @override Widget build(BuildContext context) { // … return new Scaffold( appBar: new AppBar( title: new Text('Saved Suggestions'), ), body: new WillPopScope( onWillPop: _onWillPop, child: new ListView(children: divided),), ); } Future<bool> _onWillPop() async { Routes.routes.removeLast(); await Routes.save(); return true; } }
And you need to restore the conversion history. Do this on the main screen:
class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { Routes.restore(context); // … } // … }
It's all! Now both navigation and scroll position are saved.
It seems to me that the lack of saving navigation out of the box is very strange. With saving user input, you can still argue. Suddenly, someone believes that he is quite satisfied with the preservation of the device during the coup and the loss of the activation during the destruction. It does not suit me.
Whether the flutter developers will decide anything on this is not yet clear, but on their githubs there are very active verbal battles.
At the moment, it is quite possible to make the preservation of the state independently. Although, of course, it looks like too much boilerplate.
So far I have a desire to study the flutter further, and to see if my remaining doubts will dissolve. And then decide about its applicability.
Source: https://habr.com/ru/post/352354/
All Articles