Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Preserve widget state when temporarily removed from tree in Flutter

I'm trying to preserve the state of a widget, so that if I temporarily remove the stateful widget from the widget tree, and then re-add it later on, the widget will have the same state as it did before I removed it. Here's a simplified example I have:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool showCounterWidget = true;
  @override
  Widget build(BuildContext context) {

    return Material(
      child: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            showCounterWidget ? CounterButton(): Text("Other widget"),
            SizedBox(height: 16,),
            FlatButton(
              child: Text("Toggle Widget"),
              onPressed: (){
                setState(() {
                showCounterWidget = !showCounterWidget;
              });
                },
            )
          ],
        ),
      ),
    );
  }
}

class CounterButton extends StatefulWidget {
  @override
  _CounterButtonState createState() => _CounterButtonState();
}

class _CounterButtonState extends State<CounterButton> {
  int counter = 0;

  @override
  Widget build(BuildContext context) {
    return MaterialButton(
      color: Colors.orangeAccent,
      child: Text(counter.toString()),
      onPressed: () {
        setState(() {
          counter++;
        });
      },
    );
  }
}

Ideally, I would not want the state to reset, therefor the counter would not reset to 0, how would I preserve the state of my counter widget?

like image 959
Arthur Facredyn Avatar asked Dec 29 '19 02:12

Arthur Facredyn


3 Answers

The reason why the widget loose its state when removed from the tree temporarily is, as Joshua stated, because it loose its Element/State.

Now you may ask:

Can't I cache the Element/State so that next time the widget is inserted, it reuse the previous one instead of creating them anew?

This is a valid idea, but no. You can't. Flutter judges that as anti-pattern and will throw an exception in that situation.

What you should instead do is to keep the widget inside the widget tree, in a disabled state.

To achieve such thing, you can use widgets like:

  • IndexedStack
  • Visibility/Offstage

These widgets will allow you to keep a widget inside the widget tree (so that it keeps its state), but disable its rendering/animations/semantics.

As such, instead of:

Widget build(context) {
  if (condition)
    return Foo();
  else
    return Bar();
}

which would make Foo/Bar loose their state when switching between them

do:

IndexedStack(
  index: condition ? 0 : 1, // switch between Foo and Bar based on condition
  children: [
    Foo(),
    Bar(),
  ],
)

Using this code, then Foo/Bar will not loose their state when doing a back and forth between them.

like image 71
Rémi Rousselet Avatar answered Sep 28 '22 11:09

Rémi Rousselet


Widgets are meant to store transient data of their own within their scope and lifetime.

Based on what you have provided, you are trying to re-create CounterButton child widget, by removing and adding it back to the widget tree.

In this case, the counter value that is under the CounterButton was not saved or not saving in the MyHomePage screen, the parent widget, without any reference to a view model or any state management within or at the top level.

A more technical overview how Flutter renders your widgets

Ever wonder what is the key if you try to create a constructor for a widget?

class CounterButton extends StatefulWidget {
  const CounterButton({Key key}) : super(key: key);

  @override
  _CounterButtonState createState() => _CounterButtonState();
}

keys (key) are identifiers that are automatically being handled and used by the Flutter framework to differentiate the instances of widgets in the widget tree. Removing and adding the widget (CounterButton) in the widget tree resets the key assigned to it, therefore the data it holds, its state are also removed.

NOTE: No need to create constructors for the a Widget if it will only contain key as its parameter.

From the documentation:

Generally, a widget that is the only child of another widget does not need an explicit key.

Why does Flutter changes the key assigned to the CounterButton?

You are switching between CounterButton which is a StatefulWidget, and Text which is a StatelessWidget, reason why Flutter identifies the two objects completely different from each other.

You can always use Dart Devtools to inspect changes and toggle the behavior of your Flutter App.

Keep an eye on #3a4d2 at the end of the _CounterButtonState. image-1

This is the widget tree structure after you have toggled the widgets. From CounterButton to the Text widget. image-2

You can now see that the CounterButton ending with #31a53, different from the previous identifier because the two widgets are completely different. image-2

What can you do?

I suggest that you save the data changed during runtime in the _MyHomePageState, and create a constructor in CounterButton with a callback function to update the values in the calling widget.

counter_button.dart

class CounterButton extends StatefulWidget {
  final counterValue;
  final VoidCallback onCountButtonPressed;

  const CounterButton({Key key, this.counterValue, this.onCountButtonPressed})
      : super(key: key);

  @override
  _CounterButtonState createState() => _CounterButtonState();
}

class _CounterButtonState extends State<CounterButton> {
  @override
  Widget build(BuildContext context) {
    return MaterialButton(
      color: Colors.orangeAccent,
      child: Text(widget.counterValue.toString()),
      onPressed: () => widget.onCountButtonPressed(),
    );
  }
}

Assuming you named your variable _counterValue in the _MyHomePageState, you can use it like this:

home_page.dart

_showCounterWidget
    ? CounterButton(
        counterValue: _counterValue,
        onCountButtonPressed: () {
          setState(() {
            _counterValue++;
          });
        })
    : Text("Other widget"),

In addition, this solution will help you re-use CounterButton or other similar widgets in other parts of your app.

I've added the complete example in dartpad.dev.

Andrew and Matt gave a great talk how Flutter renders widgets under the hood:

  • https://www.youtube.com/watch?v=996ZgFRENMs

Further reading

  • https://medium.com/flutter-community/flutter-what-are-widgets-renderobjects-and-elements-630a57d05208
  • https://api.flutter.dev/flutter/widgets/Widget/key.html
like image 41
Joshua de Guzman Avatar answered Sep 28 '22 11:09

Joshua de Guzman


The real solution to this problem is state management. There are several good solutions for this available as concepts and flutter packages. Personally I use the BLoC pattern regularly.

The reason for this is that widget state is meant to be used for UI state, not application state. UI state is mostly animations, text entry, or other state that does not persist.

The example in the question is application state as it is intended to persist longer than the live time of the widget.

There is a little Tutorial on creating a BLoC based counter which could be a good starting point.

like image 28
Ber Avatar answered Sep 28 '22 13:09

Ber