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))
])));
}
}
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.
Related
As I'm learning Flutter I've come to navigation. I want to pass data between screens similarly to passing data between Activities in Android and passing data between View Controllers in iOS. How do I do it in Flutter?
Related questions:
The best way to passing data between widgets in Flutter
Flutter pass data between widgets?
Flutter/ How to pass and get data between Statefulwidget
This answer will cover both passing data forward and passing data back. Unlike Android Activities and iOS ViewControllers, different screens in Flutter are just widgets. Navigating between them involves creating something called a route and using the Navigator to push and pop the routes on and off the stack.
Passing data forward to the next screen
To send data to the next screen you do the following things:
Make the SecondScreen constructor take a parameter for the type of data that you want to send to it. In this particular example, the data is defined to be a String value and is set here with this.text.
class SecondScreen extends StatelessWidget {
final String text;
SecondScreen({Key key, #required this.text}) : super(key: key);
...
Then use the Navigator in the FirstScreen widget to push a route to the SecondScreen widget. You put the data that you want to send as a parameter in its constructor.
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondScreen(text: 'Hello',),
));
The full code for main.dart is here:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
title: 'Flutter',
home: FirstScreen(),
));
}
class FirstScreen extends StatefulWidget {
#override
_FirstScreenState createState() {
return _FirstScreenState();
}
}
class _FirstScreenState extends State<FirstScreen> {
// this allows us to access the TextField text
TextEditingController textFieldController = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('First screen')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(32.0),
child: TextField(
controller: textFieldController,
style: TextStyle(
fontSize: 24,
color: Colors.black,
),
),
),
RaisedButton(
child: Text(
'Go to second screen',
style: TextStyle(fontSize: 24),
),
onPressed: () {
_sendDataToSecondScreen(context);
},
)
],
),
);
}
// get the text in the TextField and start the Second Screen
void _sendDataToSecondScreen(BuildContext context) {
String textToSend = textFieldController.text;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondScreen(text: textToSend,),
));
}
}
class SecondScreen extends StatelessWidget {
final String text;
// receive data from the FirstScreen as a parameter
SecondScreen({Key key, #required this.text}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second screen')),
body: Center(
child: Text(
text,
style: TextStyle(fontSize: 24),
),
),
);
}
}
Passing data back to the previous screen
When passing data back you need to do the following things:
In the FirstScreen, use the Navigator to push (start) the SecondScreen in an async method and wait for the result that it will return when it finishes.
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondScreen(),
));
In the SecondScreen, include the data that you want to pass back as a parameter when you pop the Navigator.
Navigator.pop(context, 'Hello');
Then in the FirstScreen the await will finish and you can use the result.
setState(() {
text = result;
});
Here is the complete code for main.dart for your reference.
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
title: 'Flutter',
home: FirstScreen(),
));
}
class FirstScreen extends StatefulWidget {
#override
_FirstScreenState createState() {
return _FirstScreenState();
}
}
class _FirstScreenState extends State<FirstScreen> {
String text = 'Text';
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('First screen')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(32.0),
child: Text(
text,
style: TextStyle(fontSize: 24),
),
),
RaisedButton(
child: Text(
'Go to second screen',
style: TextStyle(fontSize: 24),
),
onPressed: () {
_awaitReturnValueFromSecondScreen(context);
},
)
],
),
),
);
}
void _awaitReturnValueFromSecondScreen(BuildContext context) async {
// start the SecondScreen and wait for it to finish with a result
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondScreen(),
));
// after the SecondScreen result comes back update the Text widget with it
setState(() {
text = result;
});
}
}
class SecondScreen extends StatefulWidget {
#override
_SecondScreenState createState() {
return _SecondScreenState();
}
}
class _SecondScreenState extends State<SecondScreen> {
// this allows us to access the TextField text
TextEditingController textFieldController = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second screen')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(32.0),
child: TextField(
controller: textFieldController,
style: TextStyle(
fontSize: 24,
color: Colors.black,
),
),
),
RaisedButton(
child: Text(
'Send text back',
style: TextStyle(fontSize: 24),
),
onPressed: () {
_sendDataBack(context);
},
)
],
),
);
}
// get the text in the TextField and send it back to the FirstScreen
void _sendDataBack(BuildContext context) {
String textToSendBack = textFieldController.text;
Navigator.pop(context, textToSendBack);
}
}
This solution is very easy by passing variables in constructor:
first page:
Navigator.of(context).push(MaterialPageRoute(builder:(context)=>SecondPage('something')));
second page:
class SecondPage extends StatefulWidget {
String something;
SecondPage(this.something);
#override
State<StatefulWidget> createState() {
return SecondPageState(this.something);
}
}
class SecondPageState extends State<SecondPage> {
String something;
SecondPageState(this.something);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
//now you have passing variable
title: Text(something),
),
...
}
Get Perfect Solution :
From 1st Screen navigate to others as:
Navigator.pushNamed(context, "second",arguments: {"name" :
"Bijendra", "rollNo": 65210});
},
On Second Screen in build method get as :
#override
Widget build(BuildContext context) {
final Map<String, Object>rcvdData = ModalRoute.of(context).settings.arguments;
print("rcvd fdata ${rcvdData['name']}");
print("rcvd fdata ${rcvdData}");
return Scaffold(appBar: AppBar(title: Text("Second")),
body: Container(child: Column(children: <Widget>[
Text("Second"),
],),),);
}
Easiest way
FirstPage.dart
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PasswordRoute(usernameController)));
//usernameController is String value,If you want to pass multiple values add all
SecondPage.dart
class PasswordRoute extends StatefulWidget {
final String usernameController;//if you have multiple values add here
PasswordRoute(this.usernameController, {Key key}): super(key: key);//add also..example this.abc,this...
#override
State<StatefulWidget> createState() => _PasswordPageState();
}
class _PasswordPageState extends State<PasswordRoute> {
#override
Widget build(BuildContext context) {
...child: Text(widget.usernameController);
}
}
Answers above are useful for a small app, but if you want to remove the headache of continuously worrying about a widgets state, Google presented the Provider package.
https://pub.dev/packages/provider
Have a look into that one, or watch these videos from Andrea Bizzotto:
https://www.youtube.com/watch?v=MkFjtCov62g // Provider: The Essential Guide
https://www.youtube.com/watch?v=O71rYKcxUgA&t=258s // Provider: Introduction
Learn how to use the Provider package, and you are set for life :)
First Screen :
//send data to second screen
Navigator.push(context, MaterialPageRoute(builder: (context) {
return WelcomeUser(usernameController.text);
}));
Second Screen :
//fetch data from first screen
final String username;
WelcomeUser(this.username);
//use data to display
body: Container(
child: Center(
child: Text("Welcome "+widget.username,
textAlign: TextAlign.center,
),
),
),
Navigators in Flutter are similar to the Intent in Android.
There are two classes we are dealing with FirstScreen and SecondScreen.
In order to pass the data between the first screen to second do the following:
First of all add parameter in the SecondScreen class constructor
Now in the FirstScreen class provide the parameter
Navigator.push(context, MaterialPageRoute(builder: (context)=>SecondScreen(key_name:"Desired Data"));
So in the above line the "key_name" is the name of the parameter given in the SecondScreen class.
The "Desired Data" is data should be passed through the key to the SecondScreen class.
That's it you are done!!!
Passing Data to back screen flutter
Home Page
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/container.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:sqflite_offline/View/Add_data.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
#override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
List<Method> items = []; // => List of items that come form next page.
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Hello"),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context)
.push<Method>(MaterialPageRoute(builder: (_) => AddData()))
// fetching data form next page.
.then((value) => setState(() {
if (value?.title_Ctr != "" && value?.desc_Ctr != "") {
items.add(Method(
title_Ctr: value!.title_Ctr,
desc_Ctr: value.desc_Ctr));
}
}));
},
child: Icon(Icons.add),
),
body: items.isNotEmpty
? Column(children: [
Expanded(
child: ListView.builder(
itemCount: items.length,
itemBuilder: ((context, index) {
return Container(
margin:
EdgeInsets.only(top: 10, left: 10, right: 10),
padding: EdgeInsets.only(left: 10, right: 10),
height: 80,
decoration: BoxDecoration(
color: Colors.pinkAccent,
borderRadius: BorderRadius.circular(10)),
child: Center(
child: ListTile(
title: Text(items[index].title_Ctr),
subtitle: Text(items[index].desc_Ctr),
leading: Icon(Icons.emoji_people),
),
),
);
})))
])
: Center(
child: Text("No Record Found"),
));
}
}
Add List Page
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/container.dart';
import 'package:flutter/src/widgets/framework.dart';
class AddData extends StatefulWidget {
const AddData({super.key});
#override
State<AddData> createState() => _AddDataState();
}
// Creating a Class and constructor.
class Method {
late String title_Ctr;
late String desc_Ctr;
Method({required this.title_Ctr, required this.desc_Ctr});
}
class _AddDataState extends State<AddData> {
// Creating a TextEditingController for two Fiends,
//one is for title TextField and second is for Description TextField.
TextEditingController titleCtr = TextEditingController();
TextEditingController descCtr = TextEditingController();
// Creating a Method for Passing a data to back page.
OnPressed(BuildContext context) {
var data = Method(title_Ctr: titleCtr.text, desc_Ctr: descCtr.text);
Navigator.pop(context, data);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Add Data")),
body: Form(child: Builder(builder: (context) {
return Column(children: [
TextFormField(
controller: titleCtr,
decoration: InputDecoration(hintText: "title"),
validator: (value) {
var newValue = value ?? "";
if (newValue.isEmpty) {
return 'title is Required';
}
return null;
},
),
TextFormField(
controller: descCtr,
decoration: InputDecoration(hintText: "Description"),
validator: (value) {
var newValue = value ?? "";
if (newValue.isEmpty) {
return 'Discription is Required';
}
return null;
},
),
MaterialButton(
color: Colors.red,
onPressed: () {
if (Form.of(context)?.validate() ?? false) {
OnPressed(context);
}
},
child: Text("Save"),
)
]);
})));
}
}
screenshot
1) From where you want to push :
onPressed: () async {
await Navigator.pushNamed(context, '/edit',
arguments: userData);
setState(() {
userData = userData;
});}
2) From Where you want to pop :
void updateData() async{
WorldTime instance = locations;
await instance.getData();
Navigator.pop(context, userData);
}
If you use get package then try this . passing data with get package
check get package package link
Here's another approach.
Nothing wrong with the other answers. I've tried all of the methods mentioned using global wide widgets like provider, third-party solutions, Navigator arguments, etc. This approach differs by allowing one to chain calls and pass precise data of any type required to the widget using it. We can also gain access to a completion handler event and can use this technique without being constrained to Navigator objects.
Here's the tldr:
tldr; We have to turn our thinking on its head a bit. Data can be
passed to the called widget when you navigate to it by using final
arguments with default values in the destination widget. Using an
optional function you can get data back from the 'child' (destination)
widget.
The complete explanation can be found using this SO answer., (Gist)
I just want to be here to help that 1% who might go through what I did Lol
Don't forget to put an "await" infront of "Navigator.push" in the first page,
otherwise no data will be returned to the first page when you pop from the second page...
Passing Data to back screen flutter
First Screen
final result = await Navigator.of(context).push(MaterialPageRoute(builder: (context)=>const PaymentScreen()));
Second Screen
String selected = "Credit/Debit";
Navigator.pop(context,selected);
I am learning flutter and currently testing different aspects of it for app development. My eventual plan is to have two tabs, each pulling a list from an API and displaying it in a list view. My concern is that it seems every time you switch tabs, the tabs get fully redrawn as new. How do I save the current state of a tab and all the children so they don't reset when I visit the tab again? Am i thinking about i wrong? Am I supposed to manually save everything to variables and re-populate it every time the tab is drawn?
Below is a quick example I have been testing. It is a simple two tab app with form fields on each page. If I run the app, then type something into the form field, when I switch to tab two and back, the contents of the form field have been removed.
import 'package:flutter/material.dart';
void main() {
print("start");
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Tab Test',
home: new MyTabbedPage(),
);
}
}
class MyTabbedPage extends StatefulWidget {
const MyTabbedPage({Key key}) : super(key: key);
#override
_MyTabbedPageState createState() => new _MyTabbedPageState();
}
class _MyTabbedPageState extends State<MyTabbedPage>
with SingleTickerProviderStateMixin {
final List<Tab> myTabs = <Tab>[
new Tab(text: 'LEFT'),
new Tab(text: 'RIGHT'),
];
TabController _tabController;
var index = 0;
#override
void initState() {
super.initState();
_tabController = new TabController(
vsync: this, length: myTabs.length, initialIndex: index);
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
bottom: new TabBar(
controller: _tabController,
tabs: myTabs,
),
),
body: new TabBarView(
controller: _tabController,
children: myTabs.map((Tab tab) {
return new Center(
child: new TextFormField(
decoration: new InputDecoration(labelText: 'Type Something:'),
));
}).toList(),
),
);
}
}
So, i think I found the answer in setState. I reworked my sample above to have a button that increments a counter on each tab respectively. Using setState to update the vote count allows me to save the state of the field and persist it when I changes tabs. I am adding my sample code below for reference.
import 'package:flutter/material.dart';
void main() {
print("start");
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Tab Test',
home: new MyTabbedPage(),
);
}
}
class MyTabbedPage extends StatefulWidget {
const MyTabbedPage({Key key}) : super(key: key);
#override
_MyTabbedPageState createState() => new _MyTabbedPageState();
}
class _MyTabbedPageState extends State<MyTabbedPage>
with SingleTickerProviderStateMixin {
final List<Tab> myTabs = <Tab>[
new Tab(text: 'LEFT'),
new Tab(text: 'RIGHT'),
];
TabController _tabController;
var index = 0;
#override
void initState() {
super.initState();
_tabController = new TabController(
vsync: this, length: myTabs.length, initialIndex: index);
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
int votesA = 0;
int votesB = 0;
void voteUpA() {
setState(() => votesA++);
}
void voteUpB() {
setState(() => votesB++);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
bottom: new TabBar(
controller: _tabController,
tabs: myTabs,
),
),
body: new TabBarView(
controller: _tabController,
children: <Widget>[
new Column(
children: <Widget>[
Text("Hello Flutter - $votesA"),
new RaisedButton(onPressed: voteUpA, child: new Text("click here"))
],
),
new Column(
children: <Widget>[
Text("Hello Flutter - $votesB"),
new RaisedButton(onPressed: voteUpB, child: new Text("click here"))
],
),
],
),
);
}
}
I am learning flutter and I am working with tabBars and I am having an issue with saving the state. I have put a small working example of my issue below. Basically, there is a button and a stateful counter. When I click the button, I see the text field update correctly. But, when I switch to a different tab and come back, the text field is back to zero.
I have found if i move the following line outside of _CounterState so its defined at the top level of the file, then, it works correctly. When I switch tabs, the counter stays at the correct count when I switch back
int _counter = 0;
I don't feel like this is the appropriate way to do this and all of the examples I have seen have the variable inside of the class. Can anyone give me any insights? Why would it reset if it is inside the class? Am I supposed to keep it outside the class? Below is the simplified full example.
import 'package:flutter/material.dart';
void main() {
runApp(new TabBarDemo());
}
class TabBarDemo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new DefaultTabController(
length: 3,
child: new Scaffold(
appBar: new AppBar(
bottom: new TabBar(
tabs: [
new Tab(icon: new Icon(Icons.directions_car)),
new Tab(icon: new Icon(Icons.directions_transit)),
new Tab(icon: new Icon(Icons.directions_bike)),
],
),
title: new Text('Tabs Demo'),
),
body: new TabBarView(
children: [
new Counter(),
new Icon(Icons.directions_transit),
new Icon(Icons.directions_bike),
],
),
),
),
);
}
}
class Counter extends StatefulWidget {
#override
_CounterState createState() => new _CounterState();
}
class _CounterState extends State<Counter> {
int _counter = 0;
void _increment() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return new Row(
children: <Widget>[
new RaisedButton(
onPressed: _increment,
child: new Text('Increment'),
),
new Text('Count: $_counter'),
],
);
}
}
Below is the example with the counter moved outside of the class
import 'package:flutter/material.dart';
void main() {
runApp(new TabBarDemo());
}
class TabBarDemo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new DefaultTabController(
length: 3,
child: new Scaffold(
appBar: new AppBar(
bottom: new TabBar(
tabs: [
new Tab(icon: new Icon(Icons.directions_car)),
new Tab(icon: new Icon(Icons.directions_transit)),
new Tab(icon: new Icon(Icons.directions_bike)),
],
),
title: new Text('Tabs Demo'),
),
body: new TabBarView(
children: [
new Counter(),
new Icon(Icons.directions_transit),
new Icon(Icons.directions_bike),
],
),
),
),
);
}
}
class Counter extends StatefulWidget {
#override
_CounterState createState() => new _CounterState();
}
int _counter = 0; //<-- MOVED OUTSIDE THE _CounterState CLASS
class _CounterState extends State<Counter> {
void _increment() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return new Row(
children: <Widget>[
new RaisedButton(
onPressed: _increment,
child: new Text('Increment'),
),
new Text('Count: $_counter'),
],
);
}
}
As _CounterState widget is built everytime you go to the given TabView you'll need to put _counter variable in the state configuration class (Counter).
class Counter extends StatefulWidget {
int _counter = 0;
#override
_CounterState createState() => new _CounterState();
}
class _CounterState extends State<Counter> {
void _increment() {
setState(() {
widget._counter++;
});
}
#override
Widget build(BuildContext context) {
return new Row(
children: <Widget>[
new RaisedButton(
onPressed: _increment,
child: new Text('Increment'),
),
new Text('Count: ${widget._counter}'),
],
);
}
}
As I used one solution AutomaticKeepAliveClientMixin
You need to use this mixin with your state class of StateFullWidget.
you need to pass true to wantKeepAlive getter method.
class SampleWidget extends StatefulWidget {
#override
_SampleWidgetState createState() => _SampleWidgetState();
}
class _SampleWidgetState extends State<SampleWidget> with AutomaticKeepAliveClientMixin{
#override
Widget build(BuildContext context) {
super.build(context);
return Container();
}
#override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
}
This will save your state and stop your widget to recreate again. I have used it with Tabbar and PageView and it's working fine.
put the variable in that statefulwidget and then call it every time as "widget.variable_name"
I've the below custom widget that make a Switch and reads its status (true/false)
Then I add this one to my main app widget (parent), how can I make the parent knows the value of the switch!
import 'package:flutter/material.dart';
class Switchy extends StatefulWidget{
Switchy({Key key}) : super(key: key);
#override
State<StatefulWidget> createState() => new _SwitchyState();
}
class _SwitchyState extends State<Switchy> {
var myvalue = true;
void onchange(bool value) {
setState(() {
this.myvalue = value; // I need the parent to receive this one!
print('value is: $value');
});
}
#override
Widget build(BuildContext context) {
return
new Card(
child: new Container(
child: new Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
new Text("Enable/Disable the app in the background",
textAlign: TextAlign.left,
textDirection: TextDirection.ltr,),
new Switch(value: myvalue, onChanged: (bool value) => onchange(value)),
],
),
),
);
}
}
In the main.dart (parent) file, I started with this:
import 'widgets.dart';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.deepOrange,
),
home: new MyHomePage(title: 'My App settup'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Widget e = new Switchy();
//...
}
The first possibility is to pass a callback into your child, and the second is to use the of pattern for your stateful widget. See below.
import 'package:flutter/material.dart';
class MyStatefulWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() => new MyStatefulWidgetState();
// note: updated as context.ancestorStateOfType is now deprecated
static MyStatefulWidgetState of(BuildContext context) =>
context.findAncestorStateOfType<MyStatefulWidgetState>();
}
class MyStatefulWidgetState extends State<MyStatefulWidget> {
String _string = "Not set yet";
set string(String value) => setState(() => _string = value);
#override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new Text(_string),
new MyChildClass(callback: (val) => setState(() => _string = val))
],
);
}
}
typedef void StringCallback(String val);
class MyChildClass extends StatelessWidget {
final StringCallback callback;
MyChildClass({this.callback});
#override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new FlatButton(
onPressed: () {
callback("String from method 1");
},
child: new Text("Method 1"),
),
new FlatButton(
onPressed: () {
MyStatefulWidget.of(context).string = "String from method 2";
},
child: new Text("Method 2"),
)
],
);
}
}
void main() => runApp(
new MaterialApp(
builder: (context, child) => new SafeArea(child: new Material(color: Colors.white, child: child)),
home: new MyStatefulWidget(),
),
);
There is also the alternative of using an InheritedWidget instead of a StatefulWidget; this is particularly useful if you want your child widgets to rebuild if the parent widget's data changes and the parent isn't a direct parent. See the inherited widget documentation
In 2020, the function in the highest voted answer is marked deprecated. So here is the modified solution based on that answer.
import 'package:flutter/material.dart';
class MyStatefulWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() => new MyStatefulWidgetState();
// --> NOTE this! <--
static MyStatefulWidgetState of(BuildContext context) =>
context.findAncestorStateOfType<MyStatefulWidgetState>();
}
class MyStatefulWidgetState extends State<MyStatefulWidget> {
String _string = "Not set yet";
set string(String value) => setState(() => _string = value);
#override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new Text(_string),
new MyChildClass(callback: (val) => setState(() => _string = val))
],
);
}
}
typedef void StringCallback(String val);
class MyChildClass extends StatelessWidget {
final StringCallback callback;
MyChildClass({this.callback});
#override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new FlatButton(
onPressed: () {
callback("String from method 1");
},
child: new Text("Method 1"),
),
new FlatButton(
onPressed: () {
MyStatefulWidget.of(context).string = "String from method 2";
},
child: new Text("Method 2"),
)
],
);
}
}
void main() => runApp(
new MaterialApp(
builder: (context, child) => new SafeArea(child: new Material(color: Colors.white, child: child)),
home: new MyStatefulWidget(),
),
);
However, the methods mentioned in the answers of this question has a drawback. From doc:
In general, though, consider using a callback that triggers a stateful change in the ancestor rather than using the imperative style implied by this method. This will usually lead to more maintainable and reusable code since it decouples widgets from each other.
Calling this method is relatively expensive (O(N) in the depth of the tree). Only call this method if the distance from this widget to the desired ancestor is known to be small and bounded.
I think notifications are quite a civilized solution and they allow for a very clean communication without variable juggling and they bubble up if you need them to:
Define a notification:
class SwitchChanged extends Notification {
final bool val
SwitchChanged(this.val);
}
Raise notification in your child's event handler:
onPressed: () {
SwitchChanged(true).dispatch(context);
}
Finally, wrap your parent with notification listener:
NotificationListener<SwitchChanged>(
child: YourParent(...),
onNotification: (n) {
setState(() {
// Trigger action on parent via setState or do whatever you like.
});
return true;
}
)
You can pass a callback defined in the parent widget to the child widget and as soon as an action is performed in the child widget, the callback gets invoked.
class ParentWidget extends StatelessWidget {
// This gets called when the button is pressed in the ChildWidget.
void _onData(String data) {
print(data); // Hello World
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ChildWidget(onData: _onData),
);
}
}
class ChildWidget extends StatelessWidget {
final void Function(String) onData;
ChildWidget({
super.key,
required this.onData,
});
#override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// Pass 'Hello World' to parent widget.
onData('Hello World');
},
child: Text('Button'),
);
}
}
Use InheritedWidget - https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html
This lets you access data of the parent in all the children
I found a way to do this which was fairly simple, I'm a flutter noob so maybe it isn't the best way. If someone sees something wrong with it, feel free to leave a comment. Basically state is set in parent widget, child widget updates the state of the parent, and any child widgets of the parents which use the state values are redrawn when the value is updated.
Parent widget:
class MyWidget extends StatefulWidget {
const MyWidget({Key? key}) : super(key: key);
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
String _stringToChange = ""; // the string you want to update in child
// function to update state with changes to term
_updateStringToChange(String stringToChange) {
setState(() {
_stringToChange = stringToChange;
// Other logic you might want to do as string value changes
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'title',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Scaffold(
appBar: AppBar(
title: const Center(
child: Text("app bar title"),
),
),
body: Column(children: <Widget>[
ChildWhichMakesChanges(
updateStringToChange: _updateStringToChange,
),
Expanded(
child: Container(
padding: const EdgeInsets.fromLTRB(20, 10, 0, 10),
child: ChildWhichUsesChanges(
stringToChange: _stringToChange,
)))
]),
));
}
}
ChildWhichMakesChanges (this example uses a text box to enter input):
class ChildWhichMakesChanges extends StatefulWidget {
final ValueChanged<String> updateStringToChange;
const ChildWhichMakesChanges({Key? key, required this.updateStringToChange}) : super(key: key);
#override
_TextInputState createState() => _TextInputState();
}
class _TextInputState extends State<ChildWhichMakesChanges> {
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 25),
child: TextField(
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Enter text',
),
onChanged: (String stringToChange) {
widget.updateStringToChange(stringToChange);
})),
]);
}
}
Using the changed string value in ChildWhichUsesChanges:
class ChildWhichUsesChanges extends StatelessWidget {
final String stringToChange;
const ChildWhichUsesChanges(
{Key? key,
required this.stringToChange})
: super(key: key);
#override
Widget build(BuildContext context) {
return Text(stringToChange)
}
}
2022 Solution:
A simple one.
Make it work like interface.
You can make your own custom CallBack Function just by defining typedef. It will just work as an interface between child to parent widget.
This is an IMP function:
typedef void GetColor(Color? color, String? string);
Following is Parent Widget:
import 'package:flutter/material.dart';
typedef void GetColor(Color? color, String? string);
class NavigationDialog extends StatefulWidget {
const NavigationDialog({Key? key}) : super(key: key);
#override
_NavigationDialogState createState() => _NavigationDialogState();
}
class _NavigationDialogState extends State<NavigationDialog> {
Color? color = Colors.blue[700];
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: color,
appBar: AppBar(
title: const Text('Navigation Dialog Screen'),
),
body: Center(
child: ElevatedButton(
child: const Text('Change Color'),
onPressed: () {
_showColorDialog(context, (value, string) {
setState(() {
color = value;
print(string);
});
});
}),
),
);
}
And Following is a child Widget Code:
_showColorDialog(BuildContext context, Function getColor) async {
color = null;
await showDialog(
barrierDismissible: false,
context: context,
builder: (_) {
return AlertDialog(
title: const Text('Very important question'),
content: const Text('Please choose a color'),
actions: <Widget>[
TextButton(
child: const Text('Red'),
onPressed: () {
color = Colors.red[700];
getColor(color, 'Red');// This line of action wil send your data back to parent
Navigator.pop(context, color);
}),
TextButton(
child: const Text('Green'),
onPressed: () {
color = Colors.green[700];
getColor(color, 'Green');// This line of action wil send your data back to parent
Navigator.pop(context, color);
}),
TextButton(
child: const Text('Blue'),
onPressed: () {
color = Colors.blue[700];
getColor(color, 'Blue');// This line of action wil send your data back to parent
Navigator.pop(context, color);
}),
],
);
},
);
}
}
In this example, We are selecting a color from Child Alert Dialog widget and pass to Parent widget.
Store the value in that child widget in shared preference, then access that shared preference value in the parent widget.
How could I make the name() function run whenever the Page1 page appeared?
In the code below before going to Page2 I execute the dispose()
Already inside Page2 if I click the back button or the physical button of Android the function name() is not executed, but if I click the 'go to Page1' button the function name() is executed.
Could you help me to always execute the name() function when Page1 appears?
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new MyHomePage(),
routes: <String, WidgetBuilder> {
'/page2': (BuildContext context) => new Page2(),
},
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String nameScreen;
String name() {
return 'foo1';
}
#override
void initState() {
super.initState();
this.nameScreen = name();
}
#override
void dispose() {
this.nameScreen = '';
super.dispose();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Page 1'),
backgroundColor: new Color(0xFF26C6DA),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new RaisedButton(
child: const Text('go to Page2'),
onPressed: () async {
dispose();
bool isLoggedIn = await Navigator.of(context).pushNamed('/page2');
if (isLoggedIn) {
setState((){
this.nameScreen = name();
});
}
},
),
new Text(
'$nameScreen',
),
],
),
),
);
}
}
class Page2 extends StatelessWidget{
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Page 2'),
backgroundColor: new Color(0xFFE57373)
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new RaisedButton(
child: const Text('go back to Page1'),
onPressed: () {
Navigator.pop(context, true);
}
),
],
),
),
);
}
}
There is no need to call dispose at all when you are willing to pop and change State later, since dispose will remove the current object from the tree, which does not translate to the logic you are trying to develop.
You can indeed override the BackButton and pass the same call of Navigator.pop(context, result) to it. Check the following example I have tweaked your code a little bit to show you the difference between each State of your nameScreen field. I hope this helps you.
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String nameScreen = "";
String name() {
return 'foo1';
}
#override
void initState() {
super.initState();
this.nameScreen = "From initState";
}
#override
void dipose(){
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Page 1'),
backgroundColor: Color(0xFF26C6DA),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
child: const Text('go to Page2'),
onPressed: () async {
//dispose(); ///No need for dispose
String result = await Navigator.of(context).pushNamed('/page2');
setState((){
this.nameScreen = result;
});
},
),
Text(
'$nameScreen',
),
],
),
),
);
}
}
class Page2 extends StatelessWidget{
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: ()async{
Navigator.pop(context,"From BackButton");
}),
title: const Text('Page 2'),
backgroundColor: Color(0xFFE57373)
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
child: const Text('go back to Page1'),
onPressed: () {
Navigator.pop(context, "From RaisedButton");
}
),
],
),
),
);
}
One way of doing this is to use the .whenComplete() method on the Navigator widget.
Suppose you are going to the second page from the first page. Here you have to pass the functionThatSetsTheState as a pointer to the navigation part of your code.
The function looks like this and should be in a Stateful Widget.
void functionThatSetsTheState(){
setState(() {});
}
Your navigation code for OnPressed, OnTap, OnLongPress, etc.
Navigator.of(context)
.push(
MaterialPageRoute(builder: (BuildContext context) => SecondPage()))
.whenComplete(() => {functionThatSetsTheState()});
You can override the back button on the second screen. And instead of system closing, do
WillPopScope(
onWillPop: () {
print('back pressed');
Navigator.pop(context, "From BackButton");
return true;
},
child: Scaffold(...)
You can use RouteObserves if you want to execute some function whenever your page appears, you will have to implement RouteAware on the page where you want to run execute the function whenever the screens appears, you're gonna have to do something like this on ur Page1
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>(); // add this on your main class
void main() {
runApp(MaterialApp(
home: Container(),
navigatorObservers: [routeObserver], // add observer here;
));
}
// your page where func should run whenever this page appears
class MyHomePage extends StatefulWidget with RouteAware {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String nameScreen = "";
String name() {
return 'foo1';
}
#override
void initState() {
super.initState();
this.nameScreen = "From initState";
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context));
}
#override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
// implementing RouteAware method
void didPush() {
// Route was pushed onto navigator and is now topmost route.
name(); // your func goes here
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Page 1'),
backgroundColor: Color(0xFF26C6DA),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
child: const Text('go to Page2'),
onPressed: () async {
//dispose(); ///No need for dispose
String result = await Navigator.of(context).pushNamed('/page2');
setState((){
this.nameScreen = result;
});
},
),
Text(
'$nameScreen',
),
],
),
),
);
}
}
you can head over to this link for more explanation
https://api.flutter.dev/flutter/widgets/RouteObserver-class.html
Say you want to navigate from page 1 to page 2 and immediately after page 2 loads execute a function in page 2 (useful for showing a dialog immediately when page 2 loads) :
You can do this by adding in initState or didChangeDependencies of page 2 :
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
// Function to execute
});
If you want to add some logic to put a condition before executing the function, simply push an argument in your page 1 :
Navigator.of(context).pushNamed("/page-2", arguments : true)
Finnaly the code in page 2 becomes:
_functionToExecute(){
print("done");
}
#override
void didChangeDependencies() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if(ModalRoute.of(context).settings.arguments)
_functionToExecute()
});
}