If I have a list (of ListTiles for example) that can be added to, removed from, and swapped, what would be the best way to animate these changes? I am using a reorderable list if that makes a difference. Right now my list has no animations, I just call setState when data is changed.
I think you need AnimatedList...I wrote an example.
You can only set Duration when you want to insert into the list or delete from the List and this is achieved by creating a GlobalKey of AnimatedListState...
I wrote an example code for inserting
class Pool extends StatelessWidget {
final keys = GlobalKey<AnimatedListState>();
var list = List.generate(3, (i) => "Hello $i");
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: AnimatedList(
key: keys,
initialItemCount: list.length,
itemBuilder: (context, index, animation) {
return SlideTransition(
position: animation.drive(
Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
.chain(CurveTween(curve: Curves.ease))),
child: ListTile(
title: Text(list[index]),
),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
list.insert(0, "NothingYay");
keys.currentState.insertItem(0, duration: Duration(seconds: 2));
},
),
);
}
}
I hope this helps you.
Check Flutter Widget Of The Week (AnimatedList)
You can also try using AutomaticAnimatedList
https://pub.dev/packages/automatic_animated_list
It automatically computes which items to animate for you.
class ItemsAnimatedList extends StatelessWidget {
final List<ItemModel> items;
const ItemsList({
Key key,
this.items,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return AutomaticAnimatedList<ItemModel>(
items: items,
insertDuration: Duration(seconds: 1),
removeDuration: Duration(seconds: 1),
keyingFunction: (ItemModel item) => Key(item.id),
itemBuilder:
(BuildContext context, ItemModel item, Animation<double> animation) {
return FadeTransition(
key: Key(item.id),
opacity: animation,
child: SizeTransition(
sizeFactor: CurvedAnimation(
parent: animation,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
),
child: ListTile(title: Text(item.name)),
),
);
},
);
}
}
I assume you are trying to implement the swipe feature in your list.
There is a gesture named Dissmisable
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
// MyApp is a StatefulWidget. This allows us to update the state of the
// Widget whenever an item is removed.
class MyApp extends StatefulWidget {
MyApp({Key key}) : super(key: key);
#override
MyAppState createState() {
return MyAppState();
}
}
class MyAppState extends State<MyApp> {
final items = List<String>.generate(3, (i) => "Item ${i + 1}");
#override
Widget build(BuildContext context) {
final title = 'Dismissing Items';
return MaterialApp(
title: title,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return Dismissible(
// Each Dismissible must contain a Key. Keys allow Flutter to
// uniquely identify Widgets.
key: Key(item),
// We also need to provide a function that tells our app
// what to do after an item has been swiped away.
onDismissed: (direction) {
// Remove the item from our data source.
setState(() {
items.removeAt(index);
});
// Then show a snackbar!
Scaffold.of(context)
.showSnackBar(SnackBar(content: Text("$item dismissed")));
},
// Show a red background as the item is swiped away
background: Container(color: Colors.red),
child: ListTile(title: Text('$item')),
);
},
),
),
);
}
}
I'm trying to start a new screen within an onTap but I get the following error:
Navigator operation requested with a context that does not include a
Navigator.
The code I am using to navigate is:
onTap: () { Navigator.of(context).pushNamed('/settings'); },
I have set up a route in my app as follows:
routes: <String, WidgetBuilder>{
'/settings': (BuildContext context) => new SettingsPage(),
},
I've tried to copy the code using the stocks sample application. I've looked at the Navigator and Route documentation and can't figure out how the context can be made to include a Navigator. The context being used in the onTap is referenced from the parameter passed into the build method:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
SettingsPage is a class as follows:
class SettingsPage extends Navigator {
Widget buildAppBar(BuildContext context) {
return new AppBar(
title: const Text('Settings')
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: buildAppBar(context),
);
}
}
TLDR: Wrap the widget which needs to access to Navigator into a Builder or extract that sub-tree into a class. And use the new BuildContext to access Navigator.
This error is unrelated to the destination. It happens because you used a context that doesn't contain a Navigator instance as parent.
How do I create a Navigator instance then ?
This is usually done by inserting in your widget tree a MaterialApp or WidgetsApp. Although you can do it manually by using Navigator directly but less recommended. Then, all children of such widget can access NavigatorState using Navigator.of(context).
Wait, I already have a MaterialApp/WidgetsApp !
That's most likely the case. But this error can still happens when you use a context that is a parent of MaterialApp/WidgetsApp.
This happens because when you do Navigator.of(context), it will start from the widget associated to the context used. And then go upward in the widget tree until it either find a Navigator or there's no more widget.
In the first case, everything is fine. In the second, it throws a
Navigator operation requested with a context that does not include a Navigator.
So, how do I fix it ?
First, let's reproduce this error :
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Center(
child: RaisedButton(
child: Text("Foo"),
onPressed: () => Navigator.pushNamed(context, "/"),
),
),
);
}
}
This example creates a button that attempts to go to '/' on click but will instead throw an exception.
Notice here that in the
onPressed: () => Navigator.pushNamed(context, "/"),
we used context passed by to build of MyApp.
The problem is, MyApp is actually a parent of MaterialApp. As it's the widget who instantiate MaterialApp! Therefore MyApp's BuildContext doesn't have a MaterialApp as parent!
To solve this problem, we need to use a different context.
In this situation, the easiest solution is to introduce a new widget as child of MaterialApp. And then use that widget's context to do the Navigator call.
There are a few ways to achieve this. You can extract home into a custom class :
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHome()
);
}
}
class MyHome extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
child: Text("Foo"),
onPressed: () => Navigator.pushNamed(context, "/"),
),
);
}
}
Or you can use Builder :
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Builder(
builder: (context) => Center(
child: RaisedButton(
child: Text("Foo"),
onPressed: () => Navigator.pushNamed(context, "/"),
),
),
),
);
}
}
Hy guys, i have the same problem. This is occur for me. The solution what i found is very simple. Only what i did is in a simple code:
void main() {
runApp(MaterialApp(
home: YOURAPP() ,
),
);
}
I hope was useful.
Make sure your current parent widget not with same level with MaterialApp
Wrong Way
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('Title'),
),
body: Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: RaisedButton(
onPressed: () {
//wrong way: use context in same level tree with MaterialApp
Navigator.push(context,
MaterialPageRoute(builder: (context) => ScanScreen()));
},
child: const Text('SCAN')),
)),
),
);
}
}
Right way
void main() => runApp(MaterialApp(
title: "App",
home: HomeScreen(),
));
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('Title'),
),
body: Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: RaisedButton(
onPressed: () {
//right way: use context in below level tree with MaterialApp
Navigator.push(context,
MaterialPageRoute(builder: (context) => ScanScreen()));
},
child: const Text('SCAN')),
)),
);
}
}
Just like with a Scaffold you can use a GlobalKey. It doesn't need context.
final _navKey = GlobalKey<NavigatorState>();
void _navigateToLogin() {
_navKey.currentState.popUntil((r) => r.isFirst);
_navKey.currentState.pushReplacementNamed(LoginRoute.name);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: _navKey,
...
);
}
I set up this simple example for routing in a flutter app:
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',
home: new MyHomePage(),
routes: <String, WidgetBuilder>{
'/settings': (BuildContext context) => new SettingsPage(),
},
);
}
}
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('TestProject'),
),
body: new Center(
child: new FlatButton(
child: const Text('Go to Settings'),
onPressed: () => Navigator.of(context).pushNamed('/settings')
)
)
);
}
}
class SettingsPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('SettingsPage'),
),
body: new Center(
child: new Text('Settings')
)
);
}
}
Note, that the SettingsPage extends StatelessWidget and not Navigator. I'm not able to reproduce your error.
Does this example help you in building your app? Let me know if I can help you with anything else.
You should rewrite your code in main.dart
FROM:
void main() => runApp(MyApp());
TO
void main() {
runApp(MaterialApp(
title: 'Your title',
home: MyApp(),));}
The point is to have the home property to be your first page
this worked for me, I hope it will help someone in the future
A complete and tested solution:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:my-app/view/main-view.dart';
class SplashView extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: Builder(
builder: (context) => new _SplashContent(),
),
routes: <String, WidgetBuilder>{
'/main': (BuildContext context) => new MainView()}
);
}
}
class _SplashContent extends StatefulWidget{
#override
_SplashContentState createState() => new _SplashContentState();
}
class _SplashContentState extends State<_SplashContent>
with SingleTickerProviderStateMixin {
var _iconAnimationController;
var _iconAnimation;
startTimeout() async {
var duration = const Duration(seconds: 3);
return new Timer(duration, handleTimeout);
}
void handleTimeout() {
Navigator.pushReplacementNamed(context, "/main");
}
#override
void initState() {
super.initState();
_iconAnimationController = new AnimationController(
vsync: this, duration: new Duration(milliseconds: 2000));
_iconAnimation = new CurvedAnimation(
parent: _iconAnimationController, curve: Curves.easeIn);
_iconAnimation.addListener(() => this.setState(() {}));
_iconAnimationController.forward();
startTimeout();
}
#override
Widget build(BuildContext context) {
return new Center(
child: new Image(
image: new AssetImage("images/logo.png"),
width: _iconAnimation.value * 100,
height: _iconAnimation.value * 100,
)
);
}
}
As per this comment If your navigator is inside Material context navigator push will give this error. if you create a new widget and assign it to the material app home navigator will work.
This won't work
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text("Title"),
),
body: new Center(child: new Text("Click Me")),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.add),
backgroundColor: Colors.orange,
onPressed: () {
print("Clicked");
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => new AddTaskScreen()),
);
},
),
),
);
}
}
This will work
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new HomeScreen());
}
}
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Title"),
),
body: new Center(child: new Text("Click Me")),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.add),
backgroundColor: Colors.orange,
onPressed: () {
print("Clicked");
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => new AddTaskScreen()),
);
},
),
);
}
}
I was facing the same problem and solved by removing home from MaterialApp and use initialRoute instead.
return MaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: '/',
routes: {
'/': (context) => MyApp(),
'/settings': (context) => SettingsPage(),
},
);
And
onTap: () => {
Navigator.pushNamed(context, "/settings")
},
It is Simple
instead using this normal code
`runApp(BasicBankingSystem());`
wrap it with MaterialApp
runApp(MaterialApp(home: BasicBankingSystem()));
It happens because the context on the widget that tries to navigate is still using the material widget.
The short answer for the solution is to :
extract your widget
that has navigation to new class so it has a different context when calling the navigation
When your screen is not navigated from other screen,you don't initially have access to the navigator,Because it is not instantiated yet.So in that case wrap your widget with builder and extract context from there.This worked for me.
builder: (context) => Center(
child: RaisedButton(
child: Text("Foo"),
onPressed: () => Navigator.pushNamed(context, "/"),
),
You ca use this plugin
https://pub.dev/packages/get/versions/2.0.2
in The MaterialApp assign property navigatorKey: Get.key,
MaterialApp(
navigatorKey: Get.key,
initialRoute: "/",
);
you can access Get.toNamed("Your route name");
Change your main function example:
void main() {
runApp(
MaterialApp(
title: 'Your title',
home: MyApp(),
)
);
}
use this
void main() {
runApp(MaterialApp(debugShowCheckedModeBanner: false, home: MyApp()),);
}
instead of this
void main() {runApp(MyApp());}
Wrap with materialapp
reproduce code
import 'dart:convert';
import 'package:flutter/material.dart';
void main() {
// reproduce code
runApp(MyApp());
// working switch //
// runApp(
//
// MaterialApp(debugShowCheckedModeBanner: false, home: MyApp()),);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body:
Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
height: 100,
width: 100,
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => IntroPage(Isscar4: true)),
);
},
child: RichText(
text: TextSpan(
text: 'CAR',
style: TextStyle(
letterSpacing: 3,
color: Colors.white,
fontWeight: FontWeight.w400),
children: [
TextSpan(
text: '4',
style: TextStyle(
fontSize: 25,
color: Colors.red,
fontWeight: FontWeight.bold))
],
)),
),
),
],
),
SizedBox(
height: 10,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
height: 100,
width: 100,
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => IntroPage(Isscar4: false)),
);
},
child: RichText(
text: TextSpan(
text: 'BIKE',
style: TextStyle(
letterSpacing: 3,
color: Colors.white,
fontWeight: FontWeight.w400),
children: [
TextSpan(
text: '2',
style: TextStyle(
fontSize: 25,
color: Colors.red,
fontWeight: FontWeight.bold))
],
)),
),
),
],
)
])));
}
MaterialApp Swithwidget(istrue) {
return MaterialApp(
home: Scaffold(
body: IntroPage(
Isscar4: istrue,
),
),
);
}
}
class Hi extends StatelessWidget {
const Hi({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child: Text("df"),
);
}
}
class IntroPage extends StatelessWidget {
final Isscar4;
IntroPage({
Key? key,
required this.Isscar4,
}) : super(key: key);
List<Widget> listPagesViewModel = [];
List<IntroModel> models = [];
#override
Widget build(BuildContext context) {
List<dynamic> intro = fetchIntroApi(Isscar4);
intro.forEach((element) {
var element2 = element as Map<String, dynamic>;
var cd = IntroModel.fromJson(element2);
models.add(cd);
});
models.forEach((element) {
listPagesViewModel.add(Text(""));
});
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Container(),
));
}
List fetchIntroApi(bool bool) {
var four = bool;
if (four) {
var data =
'[ {"name_Title": "title name1","description": "description1"}, {"name_Title": "title name2","description": "description2"}, {"name_Title": "title name3","description": "description3"}, {"name_Title": "title name4","description": "description4"} ]';
return json.decode(data);
} else {
var data =
'[ {"name_Title": "title name","description": "description1"}, {"name_Title": "title name2","description": "description2"}, {"name_Title": "title name3","description": "description3"} ]';
return json.decode(data);
}
}
}
class IntroModel {
String? nameTitle;
String? description;
IntroModel({this.nameTitle, this.description});
IntroModel.fromJson(Map<String, dynamic> json) {
nameTitle = json['name_Title'];
description = json['description'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['name_Title'] = this.nameTitle;
data['description'] = this.description;
return data;
}
}
class Splash extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Splash Screen',
theme: ThemeData(
primarySwatch: Colors.green,
),
home: MyState(),
debugShowCheckedModeBanner: false,
);
}
void main() {
runApp(Splash());
}
class MyState extends StatefulWidget{
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyState> {
#override
void initState() {
super.initState();
Timer(Duration(seconds: 3),
()=>Navigator.pushReplacement(context,
MaterialPageRoute(builder:
(context) =>
Login()
)
)
);
}
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center ,
children: [
Container(
child:
Image.asset("assets/images/herosplash.png"),
),
],
),
);
}
}
Builder(
builder: (context) {
return TextButton(
child: const Text('Bearbeiten'),
onPressed:(){
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const gotothesiteyouwant()),
);
});
}
),
Here, all you need is to make MaterialApp the parent of your Build. This is because the context that you've used to navigate to a different screen is finding a MaterialApp or a WidgetApp as a parent of the build.
And Since in your case, the situation is the opposite, therefore you need to modify it by either calling a new Stateless widget the parent of is the MaterialApp or by simply using a Builder as home: Builder in MaterialApp.
Hope this would help!
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.