Greg Noe
Greg Noe

Reputation: 1156

Drawer navigation that retains or restores state of page

I'm building a Flutter app and I'm trying to wrap my head around Navigation and State. I built a very simple app below that has two pages that both have the increment button on them. They both share a Scaffold definition so that there is a consistent navigation drawer on both pages.

Basically the functionality I want is that FirstPage and SecondPage are singletons. So if you increment the counter on FirstPage a few times, go to the SecondPage and then return to the FirstPage through the drawer (and not the back button), the FirstPage should still have its counter incremented.

Right now if you do this it appears to create a new instance of FirstPage due to the Navigation.push(). Additionally if you're on the FirstPage and use the drawer to click "First Page" again for some reason, you should not lose state.

I saw a Flutter dev on here mention the term "non-linear navigation" and that made me think something like this is possible. Thanks for your help.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(title: "Navigation", home: FirstPage());
  }
}

class MyScaffold extends Scaffold {
  final BuildContext context;
  final Text title;
  final Widget body;

  MyScaffold({
    @required this.context,
    @required this.title,
    @required this.body,
  })  : assert(context != null),
        assert(title != null),
        assert(body != null),
        super(
            appBar: AppBar(title: title),
            drawer: Drawer(
                child: ListView(padding: EdgeInsets.zero, children: <Widget>[
              SizedBox(height: 100.0),
              ListTile(
                  title: Text("First Page"),
                  onTap: () => Navigator.of(context).push(MaterialPageRoute(
                      builder: (BuildContext context) => FirstPage()))),
              ListTile(
                  title: Text("Second Page"),
                  onTap: () => Navigator.of(context).push(MaterialPageRoute(
                      builder: (BuildContext context) => SecondPage()))),
            ])),
            body: body);
}

class FirstPage extends StatefulWidget {
  @override
  _FirstPageState createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage> {
  int _counter = 0;
  void _increment() => setState(() => _counter++);

  @override
  Widget build(BuildContext context) {
    return MyScaffold(
        context: context,
        title: Text("First Page"),
        body: Center(
            child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
              Text('$_counter'),
              RaisedButton(onPressed: _increment, child: Icon(Icons.add))
            ])));
  }
}

class SecondPage extends StatefulWidget {
  @override
  _SecondPageState createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> {
  int _counter = 0;
  void _increment() => setState(() => _counter++);

  @override
  Widget build(BuildContext context) {
    return MyScaffold(
        context: context,
        title: Text("Second Page"),
        body: Center(
            child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
              Text('$_counter'),
              RaisedButton(onPressed: _increment, child: Icon(Icons.add))
            ])));
  }
}

Upvotes: 2

Views: 1697

Answers (1)

rmtmckenzie
rmtmckenzie

Reputation: 40433

Rather than extending Scaffold, I would recommend making a StatelessWidget that builds a scaffold and takes a child widget as a parameter. By doing it the way you're doing it, you don't get a context or anything, and encapsulation is generally recommended over inheritance in Flutter.

In addition to doing that you can separate the logic for forward/back. You can do a push(secondPage) and then pop back to the first page, rather than pushing a new page each time. The navigation in flutter is a stack; each time you push you're adding to the top of it and each time you pop you're removing the top element.

If you want SecondPage to retain its counter, that's going to be a little more difficult. You have a couple of options here - the first would be to pass in an initial value when you build it. It would then increment that value internally, and then when you call Navigator.pop(context, [value]) you would pass that value back. It would be saved into the first screen's state, and then when you push the page again you'd pass the new initial value.

The second option would be to retain the counter's value at a level higher than the second page (using a StatefulWidget and possibly InheritedWidget) - i.e. in whichever widget holds your navigator/materialapp. This is probably a little overkill though.

EDIT: in response to comment from OP, it has been made clear that there are actually several pages and more complex information than just a counter.

There are various ways to go about handling this; one would be to implement the app using Redux as strategy; when used with flutter_persist it can make a quite powerful tool for persistent state and I believe there's also a plugin for integrating with firestore for cloud backup (not sure about that).

I'm not a huge fan of redux myself though; it adds quite a lot of overhead which seems to me to go against flutter's simplicity (although I could see how for a large app or team it could create consistency).

A simpler option is as described before the edit; use InheritedWidget to contain the state at a higher level. I've made a quick example below:

import 'package:flutter/material.dart';

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

class MyApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => new MyAppState();
}

class MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return CounterInfo(
      child: new MaterialApp(
        routes: {
          "/": (context) => new FirstPage(),
          "/second": (context) => new SecondPage(),
        },
      ),
    );
  }
}

class _InheritedCounterInfo extends InheritedWidget {
  final CounterInfoState data;

  _InheritedCounterInfo({
    Key key,
    @required this.data,
    @required Widget child,
  }) : super(key: key, child: child);

  @override
  bool updateShouldNotify(_InheritedCounterInfo old) => true;
}

class CounterInfo extends StatefulWidget {
  final Widget child;

  const CounterInfo({Key key, this.child}) : super(key: key);

  static CounterInfoState of(BuildContext context) {
    return (context.inheritFromWidgetOfExactType(_InheritedCounterInfo) as _InheritedCounterInfo).data;
  }

  @override
  State<StatefulWidget> createState() => new CounterInfoState();
}

class CounterInfoState extends State<CounterInfo> {

  int firstCounter = 0;
  int secondCounter = 0;

  void incrementFirst() {
    setState(() {
      firstCounter++;
    });
  }

  void incrementSecond() {
    setState(() {
      secondCounter++;
    });
  }

  @override
  Widget build(BuildContext context) => new _InheritedCounterInfo(data: this, child: widget.child);
}

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counterInfo = CounterInfo.of(context);

    return new MyScaffold(
      title: "First page",
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text("${counterInfo.firstCounter}"),
            RaisedButton(onPressed: counterInfo.incrementFirst, child: Icon(Icons.add)),
          ],
        ),
      ),
    );
  }
}

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counterInfo = CounterInfo.of(context);

    return new MyScaffold(
      title: "Second page",
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text("${counterInfo.secondCounter}"),
            RaisedButton(onPressed: counterInfo.incrementSecond, child: Icon(Icons.add)),
          ],
        ),
      ),
    );
  }
}

class MyScaffold extends StatelessWidget {
  final String title;
  final Widget child;

  const MyScaffold({Key key, this.title, this.child}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: AppBar(title: new Text(title)),
      drawer: Drawer(
          child: ListView(padding: EdgeInsets.zero, children: <Widget>[
        SizedBox(height: 100.0),
        ListTile(
          title: Text("First Page"),
          onTap: () => Navigator.of(context).pushReplacementNamed("/"),
        ),
        ListTile(title: Text("Second Page"), onTap: () => Navigator.of(context).pushReplacementNamed("/second")),
      ])),
      body: child,
    );
  }
}

Whether you go this way or look into using Redux, this is a useful website with various flutter architecture models including using inherited widget.

Upvotes: 2

Related Questions