I am trying to create a Switch widget add it to a List of widgets inside the initState and then add this list to the children property of a Column in the build method. The app runs successfully and the Switch widget does show but clicking it does not change it as if it is not working. I have tried making the same widget inside the build method and the Switch works as expected.
I have added some comments in the _onClicked which I have assigned to the onChanged property of the Switch widget that show the flow of the value property.
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
home: App(),
));
}
class App extends StatefulWidget {
#override
AppState createState() => new AppState();
}
class AppState extends State<App> {
List<Widget> widgetList = new List<Widget>();
bool _value = false;
void _onClicked(bool value) {
print(_value); // prints false the first time and true for the rest
setState(() {
_value = value;
});
print(_value); // Always prints true
}
#override
void initState() {
Switch myWidget = new Switch(value: _value, onChanged: _onClicked);
widgetList.add(myWidget);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('My AppBar'),
),
body: new Container(
padding: new EdgeInsets.all(32.0),
child: new Center(
child: new Column(children: widgetList),
),
),
);
}
}
initState is to initialize the state, not widgets. build is to create widgets.
There reason it fails is because the widgets needs to be rebuilt when the value changes (when you call setState), but it isn't because when build() is called, the previously (in initState) created widget is reused.
#override
Widget build(BuildContext context) {
List<Widget> widgetList = [];
Switch myWidget = new Switch(value: _value, onChanged: _onClicked);
widgetList.add(myWidget);
return new Scaffold(
appBar: new AppBar(
title: new Text('My AppBar'),
),
body: new Container(
padding: new EdgeInsets.all(32.0),
child: new Center(
child: new Column(children: widgetList),
),
),
);
}
Related
I am new to Flutter and just started to make a tiny little app which takes a list of Top Movies from a server using an async request. and when I tap on top of each one of list items, then it navigates me to another screen to show some details about the movie.
But there is a problem, when I tap on any item to see it's details, inside the details page, when I press back, in the first page, it just loads data again which is not a good user experience. also uses more battery and bandwidth for each request.
I don't know if this is a natural behavior of Flutter to lose data of a Stateful widget after navigating to another screen or there is something wrong with my code.
Can anybody help me with this
This is my code:
import "package:flutter/material.dart";
import "dart:async";
import "dart:convert";
import "package:http/http.dart" as http;
void main() {
runApp(MovieApp());
}
class MovieApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'test',
home: Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
title: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Text("Top Movies List",
textDirection: TextDirection.rtl,
style: TextStyle(color: Colors.black87))
]
)
),
body: MoviesList()
)
);
}
}
class MoviesList extends StatefulWidget {
#override
MoviesListState createState() => new MoviesListState();
}
class MoviesListState extends State<MoviesList> {
List moviesList = [];
Future<Map> getData() async {
http.Response response = await http.get(
'http://api.themoviedb.org/3/discover/movie?api_key={api_key}'
);
setState(() {
moviesList = json.decode(response.body)['results'];
});
// return json.decode(response.body);
}
#override
Widget build(BuildContext context) {
getData();
if(moviesList == null) {
return Scaffold(
body: Text('Getting data from server')
);
} else {
return ListView.builder(
itemCount: moviesList.length,
itemBuilder: (context, index){
return Container(
child: ListTile(
title: Text(moviesList[index]['title']),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => MovieDetails()),
);
}
)
);
}
);
}
}
}
class MovieDetails extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Details')
),
body: Container(
child: Center(
child: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back!'),
),
)
),
);
}
}
Move your getData() method inside the initState() in your State class.
(Remove it from build method)
#override
void initState() {
getData();
super.initState();
}
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.
I have the following screen:
import 'package:flutter/material.dart';
import '../models/patient.dart';
import '../components/patient_card.dart';
import '../services.dart';
class Home extends StatefulWidget {
var patients = <Patient>[];
#override
_HomeState createState() => new _HomeState();
}
class _HomeState extends State<Home> {
#override
initState() {
super.initState();
Services.fetchPatients().then((p) => setState(() => widget.patients = p));
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Home'),
),
body: new Container(
child: new ListView(
children: widget.patients.map(
(patient) => new PatientCard(patient),
).toList()
)
)
);
}
}
As you can see I do the endpoint call when I overwrite initState() in _HomeState. But it only runs once initially when the app starts. I can't just type r in my terminal and let the app hot reload and call the endpoint again.. I have to use Shift + r to do a full restart first.
So the question is, am I calling the web service in the recommended spot? And if it not... where does it go? Also, shouldn't ListView have a function / property that gets called on "pull to refresh" or something?
As mentioned by #aziza you can use a Stream Builder or if you want to call a function every time widget gets built then you should call it in build function itself. Like in your case.
#override
Widget build(BuildContext context) {
Services.fetchPatients().then((p) => setState(() => widget.patients = p));
return new Scaffold(
appBar: new AppBar(
title: new Text('Home'),
),
body: new Container(
child: new ListView(
children: widget.patients.map(
(patient) => new PatientCard(patient),
).toList()
)
)
);
}
If you want to add pull-to-refresh functionality then wrap your widget in refresh indicator widget. Add your call in onRefresh property.
return new RefreshIndicator(child: //Your Widget Tree,
onRefresh: handleRefresh);
Note that this widget only works with vertical scroll view.
Hope it helps.
Have a look on StreamBuilder. This widget will allow you to deal with async data that are frequently updated and will update the UI accordingly by listening onValue at the end of your stream.
Flutter have FutureBuilder class, you can also create your widget as shown below
Widget build(BuildContext context) {
var futureBuilder = new FutureBuilder(
future: Services.fetchPatients().then((p) => setState(() => widget.patients = p)),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
if (snapshot.data != null) {
return new Container(
child: new ListView(
children: snapshot.data.map(
(patient) => new PatientCard(patient),
).toList()
)
);
}
} else {
return new Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(16.0),
child: new CircularProgressIndicator());
}
});
return new Container(child: futureBuilder);
}
Example project : Flutter - Using the future builder with list view.
I want to focus a textField after the user navigated back with the back button in the navigation bar. How do you do that? I tried the autofocus property of the TextField widget. But this only works when navigating forward when the widget gets created for the first time. On iOS there is the viewDidAppear method, is there something similar in Flutter?
Thanks!
You will need to provide your TextField a FocusNode, and then you can await the user to go back and set FocusScope.of(context).requestFocus(myNode) when the navigation happens.
Simple demonstration:
class FirstPage extends StatefulWidget {
#override
_FirstPageState createState() => new _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
FocusNode n = new FocusNode();
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title:new Text("First Page")),
body: new Center(
child: new Column(
children: <Widget>[
new TextField(
focusNode: n,
),
new RaisedButton(onPressed: ()async{
bool focus = await Navigator.of(context).push(new MaterialPageRoute(builder: (_)=>new SecondPage()));
if (focus == true|| focus==null){
FocusScope.of(context).requestFocus(n);
}
},
child: new Text("NAVIGATE"),),
],
),
),
);
}
}
class SecondPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title:new Text("Second Page")),
body: new Center(
child: new RaisedButton(onPressed: (){Navigator.pop(context,true);},child: new Text("BACK"),),
),
);
}
}