Reputation: 1156
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
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