Search code examples
flutterdartstatestate-managementstatefulwidget

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?


Solution

  • 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.