Related
I'm trying to learn Dart/Flutter and am working on an example where there's a button on the app that says "Get Data", and when I touch it I want to retrieve JSON data from a restful service.
I see the web service being called in fetchPost, but the builder property of the FutureBuilder isn't called.
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'ResultsList.dart';
import 'dart:convert';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Restul Test',
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> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: (){
FutureBuilder<ResultsList>(
future: fetchPost(),
builder: (context, snapshot){
print('In Builder');
}
);
},
child: Text('Get data'),
)
],
),
)
);
}
}
Future<ResultsList> fetchPost() async {
final response = await http.get('http://mywebserviceurl');
if (response.statusCode == 200){
print('Received data');
return ResultsList.fromJson(json.decode(response.body));
}
else {
throw Exception('Failed to load data');
}
}
Interestingly though, if I move the FutureBuilder out of the onPressed of the button to the child of Center, I do see the builder property getting called.
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'ResultsList.dart';
import 'dart:convert';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Restul Test',
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> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: FutureBuilder<ResultsList>(
future: fetchPost(),
builder: (context, snapshot){
print ('In Builder');
return Container();
}
)
)
);
}
}
Future<ResultsList> fetchPost() async {
final response = await http.get('http://mywebserviceurl');
if (response.statusCode == 200){
print('Received data');
return ResultsList.fromJson(json.decode(response.body));
}
else {
throw Exception('Failed to load data');
}
}
Obviously I'm missing something, but any idea what I'm doing wrong?
If you want to get some data from request - you don't need FutureBuilder. You can do:
RaisedButton(
onPressed: (){
fetchPost().then((result) {
print('In Builder');
})
},
child: Text('Get data'),
)
or
RaisedButton(
onPressed: () async {
var result = await fetchPost()
print('In Builder');
},
child: Text('Get data'),
)
The onPressed method in this RaisedButton is actually not doing anything. It just creates a new FutureBuilder which does nothing but existing^^ It's like you would just call 1+1;, which just creates a value, but that value is not used to do anything.
RaisedButton(
onPressed: (){
FutureBuilder<ResultsList>(
future: fetchPost(),
builder: (context, snapshot){
print('In Builder');
}
);
},
child: Text('Get data'),
)
You could have body be assigned to a Widget(which could just be called body or whatever you want^^), which you then change in a setState((){body = FutureBuilder(/*...*/}); call.
For me FutureBuilder not working in onPresses...
I used this way :
I defined a variable in state:
bool visiblity = false;
and I used this code in build:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () {
visiblity=true;
fetchPost();
},
child: Text('Get data'),
),
FutureBuilder<ResultsList>(
future: ("Your View Model that return from call back"),
builder: (context, snapshot) {
if (visiblity) {
print('In Builder');
visiblity=false;
} else
return Container();
}
),
],
),
)
);
}
I didn't put FutureBuilder in onPressed. I put that in body and changed visibility after return result.
I'm using the Flutter UserAccountsDrawerHeader widget to display the user's data but I could not figure out how to implement the onDetailsPressed() function to call the user details. Here is my code:
import 'package:flutter/material.dart';
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
#override
Widget build(BuildContext context) {
return new Scaffold(
drawer: _buildDrawer(context),
appBar: _buildAppBar(),
);
}
}
Widget _buildAppBar() {
return new AppBar();
}
Widget _buildDrawer(BuildContext context) {
return new Drawer(
child: new ListView(
children: <Widget>[
new UserAccountsDrawerHeader(
accountName: new Text("Cleudice Santos"),
accountEmail: new Text("cleudice.ms#gmail.com"),
onDetailsPressed: () {},
),
new ListTile(
title: new Text("Visão geral"),
leading: new Icon(Icons.dashboard),
onTap: () {
print("Visão geral");
},
),
],
),
);
}
I want to click the arrow and show the account details as shown below. That is, overlapping the content of the drawer. As the Gmail app does.
Basically, what you should be doing is replacing the rest of the content with user details rather than the current list. The simplest way to do this is to make your drawer into a stateful widget and have a boolean that keeps track of whether user details or the normal list should be shown.
I've added that to your code (and added a bit to make it self-contained so you can paste it to a new file to test out):
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: UserDetailDrawer(),
appBar: _buildAppBar(),
);
}
}
Widget _buildAppBar() {
return AppBar();
}
class UserDetailDrawer extends StatefulWidget {
#override
_UserDetailDrawerState createState() => _UserDetailDrawerState();
}
class _UserDetailDrawerState extends State<UserDetailDrawer> {
bool showUserDetails = false;
Widget _buildDrawerList() {
return ListView(
children: <Widget>[
ListTile(
title: Text("Visão geral"),
leading: Icon(Icons.dashboard),
onTap: () {
print("Visão geral");
},
),
ListTile(
title: Text("Another tile??"),
leading: Icon(Icons.question_answer),
),
],
);
}
Widget _buildUserDetail() {
return Container(
color: Colors.lightBlue,
child: ListView(
children: [
ListTile(
title: Text("User details"),
leading: Icon(Icons.info_outline),
)
],
),
);
}
#override
Widget build(BuildContext context) {
return Drawer(
child: Column(children: [
UserAccountsDrawerHeader(
accountName: Text("Cleudice Santos"),
accountEmail: Text("cleudice.ms#gmail.com"),
onDetailsPressed: () {
setState(() {
showUserDetails = !showUserDetails;
});
},
),
Expanded(child: showUserDetails ? _buildUserDetail() : _buildDrawerList())
]),
);
}
}
I'm trying to replace the increment flutter app code, by using Streams from Dart API without using scoped_model or rxdart.
So I read this and watched this, but could not get it work for me, my codes are:
StreamProvider.dart:
import 'package:flutter/widgets.dart';
import 'businessLogic.dart';
import 'dart:async';
class Something {
final _additionalContrllerr = StreamController<int>();
Sink<int> get addition => _additionalContrllerr.sink;
Stream<int> get itemCount => _additionalContrllerr.stream;
}
class StreemProvider extends InheritedWidget {
final Something myBloc; // Business Logic Component
StreemProvider({
Key key,
#required this.myBloc,
Widget child,
}) : super(key: key, child: child);
#override
bool updateShouldNotify(InheritedWidget oldWidget) => true;
static Something of(BuildContext context) =>
(context.inheritFromWidgetOfExactType(StreemProvider) as StreemProvider)
.myBloc;
}
main.dart:
import 'package:flutter/material.dart';
import 'package:flutter_app/StreemProvider.dart';
void main() => runApp(MyApp(
textInput: Text("Provided By the Main"),
));
class MyApp extends StatefulWidget {
final Widget textInput;
MyApp({this.textInput});
#override
State<StatefulWidget> createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
bool checkBoxValue = false;
int _counter = 0;
#override
Widget build(BuildContext ctxt) {
var x = Something(); //// Not sure if have to use this!
return StreemProvider(
myBloc: x, //// Not sure about this!!
child: MaterialApp(
home: SafeArea(
child: Scaffold(
body: new Center(
child: new Column(
children: <Widget>[
widget.textInput,
Text("clickec $_counter times"),
Text("clickec ${x.itemCount.listen((int i) => i)} times"),
/// How to get the value of i??!
Checkbox(
value: checkBoxValue,
onChanged: (bool newValue){
setState(() {
checkBoxValue = newValue;
});
}
)
],
)),
floatingActionButton: Incrementer(_increment),
// floatingActionButton: Incrementer(x),
),
),
),
);
}
_increment() {
setState(() {
_counter += 1;
});
}
}
class Incrementer extends StatefulWidget {
final Function increment;
Incrementer(this.increment);
#override
State<StatefulWidget> createState() {
return IncrementerState();
}
}
class IncrementerState extends State<Incrementer>{
#override
Widget build(BuildContext ctxt) {
final myBloc = StreemProvider.of(context);
return new FloatingActionButton(
//onPressed: widget.increment,
// How ot get the latest value!!
onPressed: () async {
var y = await myBloc.itemCount.last;
if (y.isNaN) y = 0;
myBloc.addition.add(y+1);
},
child: new Icon(Icons.add),
);
}
}
don't know the restrictions on rx_dart, but I can only try to answer by you using it. lol
your bloc doesnt define wht to listen in your input stream, this is how I could get it to work
counter_bloc.dart
import 'package:rxdart/rxdart.dart';
import 'dart:async';
class CounterBloc {
int _count = 0;
ReplaySubject<int> _increment = ReplaySubject<int>();
Sink<int> get increment => _increment;
BehaviorSubject<int> _countStream = BehaviorSubject<int>(seedValue: 0);
Stream<int> get count => _countStream.stream;
CounterBloc() {
_increment.listen((increment) {
_count += increment;
_countStream.add(_count);
});
}
}
In the constructor the listen method is set for that stream. for each increment sent, it'll increment the counter and send the current count to another stream.
In main.dart, removed the _counter property since that's now being handled by the BLOC. and to display I used a stream builder.
also added a second fab, with a +2 increment to test the logic.
hope this helps you model your bloc class. :)
a good bloc reference: https://www.youtube.com/watch?v=PLHln7wHgPE
main.dart
import 'counter_bloc.dart';
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> {
CounterBloc bloc = CounterBloc();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
StreamBuilder<int>(
stream: bloc.count,
initialData: 0,
builder: (BuildContext c, AsyncSnapshot<int> data) {
return Text(
'${data.data}',
style: Theme.of(context).textTheme.display1,
);
},
),
],
),
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
onPressed: () {
bloc.increment.add(2);
},
tooltip: 'Increment 2',
child: Text("+2"),
),
FloatingActionButton(
onPressed: () {
bloc.increment.add(1);
},
tooltip: 'Increment 1',
child: Text("+1"),
),
],
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
Thanks a lot to vbandrade his answer helped me figuring t out. the solution worked with me is:
I need to have 2 StreamController if I need to listen to a sink in my bloc Business Logic Component, then process and stream the output to other elements.
The counter_bloc.dart is:
import 'dart:async';
class CounterBloc {
int _count = 0;
// The controller to stream the final output to the required StreamBuilder
final _counter = StreamController.broadcast<int>();
Stream<int> get counter => _counter.stream;
// The controller to receive the input form the app elements
final _query = StreamController<int>();
Sink<int> get query => _query.sink;
Stream<int> get result => _query.stream;
// The business logic
CounterBloc() {
result.listen((increment) { // Listen for incoming input
_count += increment; // Process the required data
_counter.add(_count); // Stream the required output
});
}
void dispose(){
_query.close();
_counter.close();
}
}
And the main.dart is:
import 'counter_bloc.dart';
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
State<StatefulWidget> createState() {
return _MyHomePageState();
}
}
class _MyHomePageState extends State<MyHomePage> {
var bloc = CounterBloc();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
StreamBuilder<int>( // Listen to the final output sent from the Bloc
stream: bloc.counter,
initialData: 0,
builder: (BuildContext c, AsyncSnapshot<int> data) {
return Text(
'${data.data}',
style: Theme.of(context).textTheme.display1,
);
},
),
],
),
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
onPressed: () {
bloc.query.add(2); // Send input to the Bloc
},
tooltip: 'Increment 2',
child: Text("+2"),
),
FloatingActionButton(
onPressed: () {
bloc.query.add(1); // Send input to the Bloc
},
tooltip: 'Increment 1',
child: Text("+1"),
),
],
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
A simple implementation
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Counter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
int _counter = 0;
final StreamController<int> _streamController =
StreamController<int>.broadcast();
Stream<int> get _stream => _streamController.stream;
void incrementCounter() {
_counter++;
_streamController.add(_counter);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter demo'),
),
body: Center(
child: StreamBuilder<int>(
stream: _stream,
builder: (ctxt, snapshot) {
if (snapshot.hasData) {
return Text(
'You have pushed this button ${snapshot.data} times');
}
return Text('You have pushed this button ${0} times');
}),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
incrementCounter();
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Problem: Shared preference bool value is null on startup even though I have given it a value if prefs.getBool('myBool') returns null (though my shared preferences value should already be set and saved). It does, however, work by the time I press a button (I assume because it has finished running the async code).
Question: How can I force shared preferences to load on startup (so my value is not null) without having to press the print button?
Example Code:
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
MyApp({Key key}) : super(key: key);
#override
createState() => new MyAppState();
}
class MyAppState extends State<MyApp> {
final padding = const EdgeInsets.all(50.0);
#override
void initState() {
super.initState();
MySharedPreferences.load();
MySharedPreferences.printMyBool();
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
body: new Padding(
padding: padding,
child: new Column(
children: <Widget>[
new Padding(
padding: padding,
child: new RaisedButton(
child: new Text('Save True'),
onPressed: () => MySharedPreferences.save(myBool: true),
),
),
new Padding(
padding: padding,
child: new RaisedButton(
child: new Text('Save False'),
onPressed: () => MySharedPreferences.save(myBool: false),
),
),
new Padding(
padding: padding,
child: new RaisedButton(
child: new Text('Print myBool'),
onPressed: () => MySharedPreferences.printMyBool(),
),
),
],
),
),
),
);
}
}
class MySharedPreferences {
static bool _myBool;
static void load() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
_myBool = prefs.getBool('myBool') ?? false;
}
static void save({myBool: bool}) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
_myBool = myBool;
await prefs.setBool('myBool', _myBool);
}
static void printMyBool() {
print('myBool: ${_myBool.toString()}');
}
}
Results:
On startup, myBool: null is printed. Once the button is pressed, myBool: false/true is then printed.
Your problem is that you call load() and printMyBool() in quick succession. Because load() is async calling it hasn't executed any of its code, it has only scheduled it. So, printMyBool executes before the body of load.
There's no need to put static functions in a class - just declare them as top level functions. Also, you don't really want _myBool to be a global - it should be part of a Widget's state. That way when you update it, Flutter knows what parts of your tree to redraw.
I've restructured your code to remove the redundant statics.
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
MyApp({Key key}) : super(key: key);
#override
createState() => new MyAppState();
}
const EdgeInsets pad20 = const EdgeInsets.all(20.0);
const String spKey = 'myBool';
class MyAppState extends State<MyApp> {
SharedPreferences sharedPreferences;
bool _testValue;
#override
void initState() {
super.initState();
SharedPreferences.getInstance().then((SharedPreferences sp) {
sharedPreferences = sp;
_testValue = sharedPreferences.getBool(spKey);
// will be null if never previously saved
if (_testValue == null) {
_testValue = false;
persist(_testValue); // set an initial value
}
setState(() {});
});
}
void persist(bool value) {
setState(() {
_testValue = value;
});
sharedPreferences?.setBool(spKey, value);
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Padding(
padding: pad20,
child: new Text(
_testValue == null ? 'not ready' : _testValue.toString()),
),
new Padding(
padding: pad20,
child: new RaisedButton(
child: new Text('Save True'),
onPressed: () => persist(true),
),
),
new Padding(
padding: pad20,
child: new RaisedButton(
child: new Text('Save False'),
onPressed: () => persist(false),
),
),
new Padding(
padding: pad20,
child: new RaisedButton(
child: new Text('Print myBool'),
onPressed: () => print(_testValue),
),
),
],
),
),
),
);
}
}
Add condition ?? when you get value from preference.
int intValue = prefs.getInt('intValue') ?? 0;
Use conditional operator(??) to assign values if shared preference returning null
bool _testValue;
#override
void initState() {
super.initState();
SharedPreferences.getInstance().then((prefValue) => {
setState(() {
_name = prefValue.getString('name')?? false;
_controller = new TextEditingController(text: _name);
})
});
}
For any one still experiencing this issue, it's because there is still a race condition in the accepted answer.
To fix it, use this package to wait for the layout to load first
You can use FutureBuilder to make async operations.
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.