I have a simple flutter app. It has a stateful widget in the Home Page with a simple Scaffold which only has a Column. One of the children in the column is a custom widget called buildBodyLayout(). This widget has its own column with some Text widgets, and another custom widget called buildButton(). This new widget has a button which needs to setState of a variable in the Home view. I pass the value of the variable when calling the widget. But each widget is in its own dart file since I am re-using the same widget in other pages.
How do I setState the main stateful widget from inside custom widgets?
If I write everything inside the same page, it all works fine. How do I use a widget in a different dart file to set the sate of a parent widget?
Sample Code
Home Stateful Widget
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
int changeValue;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Text("Welcome to my App"),
Text("The Change Value is: $changeValue"),
buildBodyLayout(changeValue),
],
),
);
}
}
buildBodyLayouot Widget
Widget buildBodyLayout(int value){
return Column(
children: [
Text("Press the + and - Buttons to change Value"),
buildButtons(value),
],
);
buildButtons Widget
Widget buildButtons(int value){
return Column(
children: [
RaisedButton(
child: Text("Increase Value"),
onPressed: (){
value = value + 1; //THIS SHOULD SET STATE
}) ,
RaisedButton(
child: Text("Decrease Value"),
onPressed: (){
value = value - 1; //THIS SHOULD SET STATE
})
],
);
}
}
Thank you for any help!
Widgets in flutter are classes that extend the widget(StatefulWidget, StatelessWidget) class.
In your code your using functions to build your widgets, which is not recommended.
You can see the difference between class widgets and function widgets here:
What is the difference between functions and classes to create reusable widgets?
Aside from that, using function or classes, to solve your problem you need to use a callback.
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
int changeValue = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Text("Welcome to my App"),
Text("The Change Value is: $changeValue"),
buildBodyLayout(changeValue,
addToValue: (int increment){
setState((){
changeValue += increment;
});
}
),
],
),
);
}
}
Widget buildBodyLayout(int value, Function(int newValue) addToValue){
return Column(
children: [
Text("Press the + and - Buttons to change Value"),
buildButtons(value, addToValue),
],
);
}
Widget buildButtons(int value, Function(int newValue) addToValue){
return Column(
children: [
RaisedButton(
child: Text("Increase Value"),
onPressed: (){
addToValue(1);
}),
RaisedButton(
child: Text("Decrease Value"),
onPressed: (){
addToValue(-1);
})
],
);
}
You also don't need to put your widgets in different files to reuse them, but it's recommended that you do that.
Related
I have created a custom widget for list tile where I need to change icon of fav_outline to fav or google but it does not change, although items are adding and removing working properly but only icon does not change..it was working well before I created a custom widget for it
I have BottomNavigation for two screens alimonies and fav movies like that...
when I click on fav icon of all movie screen...it adds to fav movie ,,that's working fine, but only icon does not change
here is my coding of custom widget and my all movie screen..both are stateful
class MyCard extends StatefulWidget {
final Movie e;
final VoidCallback onfavtab;
MyCard({required this.e,required this.onfavtab});
#override
State<MyCard> createState() => _MyCardState();
}
class _MyCardState extends State<MyCard> {
#override
Widget build(BuildContext context) {
return Card(
child: ListTile(
title: Text(widget.e.name.toString()),
subtitle: Text(widget.e.language),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: userlist[0].favmovies.contains(e)==true?Icon(Icons.favorite): Icon(Icons.favorite_outline),onPressed: widget.onfavtab),
IconButton(onPressed: (){}, icon: Icon(Icons.delete)),
],
),
leading: CircleAvatar(
backgroundImage: NetworkImage(widget.e.imageurl),
),
),
);
}
}
and here is my allmovie screen coding where I call custom widget
lass _AllMoviesState extends State<AllMovies> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(children: movielist.map((e) => MyCard(e: e, onfavtab: (){
if(userlist[0].favmovies.contains(e)==true)
userlist[0].favmovies.remove(e);
else
userlist[0].favmovies.add(e);
//looks like this is not working
setState(() {
print(userlist[0].favmovies);
});
},
)).toList(),),
);
}
}
put the icon which you need to edit it in provider class
and whene you click it just change the icon and notify it.
let's say I have an app with the following setup:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(),
Expanded(child: MainLoginScreen()),
],
),
));
}
}
I would like to know how can I navigate only the MainLoginScreen widget from the MainMenu with any .push() method.
(I found a way to navigate from a context inside the mainloginscreen,by wrapping it with a MaterialApp widget, but what if I want to use the MainMenu widget instead, which has another context)
There is a general agreement that a 'screen' is a topmost widget in the route. An instance of 'screen' is what you pass to Navigator.of(context).push(MaterialPageRoute(builder: (context) => HereGoesTheScreen()). So if it is under Scaffold, it is not a screen. That said, here are the options:
1. If you want to use navigation with 'back' button
Use different screens. To avoid code duplication, create MenuAndContentScreen class:
class MenuAndContentScreen extends StatelessWidget {
final Widget child;
MenuAndContentScreen({
required this.child,
});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(),
Expanded(child: child),
],
),
),
);
}
}
Then for each screen create a pair of a screen and a nested widget:
class MainLoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MenuAndContentScreen(
child: MainLoginWidget(),
);
}
}
class MainLoginWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
// Here goes the screen content.
}
}
2. If you do not need navigation with 'back' button
You may use IndexedStack widget. It can contain multiple widgets with only one visible at a time.
class MenuAndContentScreen extends StatefulWidget {
#override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState(
initialContentIndex: 0,
);
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
int _index;
_MainMenuAndContentScreenState({
required int initialContentIndex,
}) : _contentIndex = initialContentIndex;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
// A callback that will be triggered somewhere down the menu
// when an item is tapped.
setContentIndex: _setContentIndex,
),
Expanded(
child: IndexedStack(
index: _contentIndex,
children: [
MainLoginWidget(),
SomeOtherContentWidget(),
],
),
),
],
),
),
);
}
void _setContentIndex(int index) {
setState(() {
_contentIndex = index;
});
}
}
The first way is generally preferred as it is declrative which is a major idea in Flutter. When you have the entire widget tree statically declared, less things can go wrong and need to be tracked. Once you feel it, it really is a pleasure. And if you want to avoid back navigation, use replacement as ahmetakil has suggested in a comment: Navigator.of(context).pushReplacement(...)
The second way is mostly used when MainMenu needs to hold some state that needs to be preserved between views so we choose to have one screen with interchangeable content.
3. Using a nested Navigator widget
As you specifically asked about a nested Navigator widget, you may use it instead of IndexedStack:
class MenuAndContentScreen extends StatefulWidget {
#override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState();
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
final _navigatorKey = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
navigatorKey: _navigatorKey,
),
Expanded(
child: Navigator(
key: _navigatorKey,
onGenerateRoute: ...
),
),
],
),
),
);
}
}
// Then somewhere in MainMenu:
final anotherContext = navigatorKey.currentContext;
Navigator.of(anotherContext).push(...);
This should do the trick, however it is a bad practice because:
MainMenu knows that a particular Navigator exists and it should interact with it. It is better to either abstract this knowledge with a callback as in (2) or do not use a specific navigator as in (1). Flutter is really about passing information down the tree and not up.
At some point you would like to highlight the active item in MainMenu, but it is hard for MainMenu to know which widget is currently in the Navigator. This would add yet another non-down interaction.
For such interaction there is BLoC pattern
In Flutter, BLoC stands for Business Logic Component. In its simpliest form it is a plain object that is created in the parent widget and then passed down to MainMenu and Navigator, these widgets may then send events through it and listen on it.
class CurrentPageBloc {
// int is an example. You may use String, enum or whatever
// to identify pages.
final _outCurrentPageController = BehaviorSubject<int>();
Stream<int> _outCurrentPage => _outCurrentPageController.stream;
void setCurrentPage(int page) {
_outCurrentPageController.sink.add(page);
}
void dispose() {
_outCurrentPageController.close();
}
}
class MenuAndContentScreen extends StatefulWidget {
#override
_MenuAndContentScreenState createState() => _MenuAndContentScreenState();
}
class _MenuAndContentScreenState extends State<MenuAndContentScreen> {
final _currentPageBloc = CurrentPageBloc();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.grey[200],
child: Row(
children: [
MainMenu(
currentPageBloc: _currentPageBloc,
),
Expanded(
child: ContentWidget(
currentPageBloc: _currentPageBloc,
onGenerateRoute: ...
),
),
],
),
),
);
}
#override
void dispose() {
_currentPageBloc.dispose();
}
}
// Then in MainMenu:
currentPageBloc.setCurrentPage(1);
// Then in ContentWidget's state:
final _navigatorKey = GlobalKey();
late final StreamSubscription _subscription;
#override
void initState() {
super.initState();
_subscription = widget.currentPageBloc.outCurrentPage.listen(_setCurrentPage);
}
#override
Widget build(BuildContext context) {
return Navigator(
key: _navigatorKey,
// Everything else.
);
}
void _setCurrentPage(int currentPage) {
// Can't use this.context, because the Navigator's context is down the tree.
final anotherContext = navigatorKey?.currentContext;
if (anotherContext != null) { // null if the event is emitted before the first build.
Navigator.of(anotherContext).push(...); // Use currentPage
}
}
#override
void dispose() {
_subscription.cancel();
}
This has advantages:
MainMenu does not know who will receive the event, if anybody.
Any number of listeners may listen on such events.
However, there is still a fundamental flaw with Navigator. It can be navigated without MainMenu knowledge using 'back' button or by its internal widgets. So there is no single variable that knows which page is showing now. To highlight the active menu item, you would query the Navigator's stack which eliminates the benefits of BLoC.
For all these reasons I still suggest one of the first two solutions.
I was always wondering, why I have to "lift the state up" of some widget down in the tree, to reflect changes in the widget's UI..
Can't I just simply have multiple stateful Widgets? and for example, import the lowest stateful widget down the tree into my top-level Widget, and from there could I not just call some method of my widget which triggers its setState() method and just updates that part in the DOM tree that is concerned with my widget?
And secondly, I would then have to move my properties and other important parts from the lower widget also up into my higher state widget, risking to clutter that class with unrelated functions and properties at some time, in my opinion, React solves that way better by just passing method callbacks down as it suits...
And is there always only one stateful widget in my Flutter app at the highest level?
Parents don't have access to child widgets/states, only the other way around. So you can not "import the lowest stateful widget down the tree into my top-level Widget"
You only have to "lift the state up" when you want to share that state between different branches of the widget tree. Then the children can lookup the parent and access the shared state.
Don't be afraid of StatefulWidget - you can use as many as you need
Research Flutter state management solutions which exist exactly for the purpose of keeping things separated (ChangeNotifier, Provider, BLOC, Redux, etc.)
Can I have multiple widgets with state in my Widgets tree?
The answer is, yes you can create multiple StatefulWidget in your widget. You can also create a callback function from the lowest StatefulWidget with Function(yourcallback). In my opinion, flutter also support component base model, and make as dynamic as possible to customize our own widget.
For example:
child widget
child widget has it's state.
class Child extends StatefulWidget {
final int counter;
final Function(int childCounter) callback; //here is your callback
const Child({
Key key,
this.counter,
this.callback,
}) : super(key: key);
#override
_ChildState createState() => _ChildState();
}
class _ChildState extends State<Child> {
String _childState = '';
#override
Widget build(BuildContext context) {
return Container(
color: Colors.green[400],
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text("Child"),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.exposure_minus_1_rounded),
onPressed: () {
this.widget.callback(this.widget.counter - 1); //here is your callback
setState(() {
_childState = "minus one";
});
}),
IconButton(
icon: Icon(Icons.plus_one_rounded),
onPressed: () {
this.widget.callback(this.widget.counter + 1); //here is your callback
setState(() {
_childState = "plus one";
});
})
],
),
Text(_childState)
],
),
);
}
}
Parent Widget
the parent get the callback from the child.
class Parent extends StatefulWidget {
#override
_ParentState createState() => _ParentState();
}
class _ParentState extends State<Parent> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Multiple State"),
),
body: Container(
child: Column(
children: [
Child(
counter: _counter,
callback: (childCounter) { //here is callback from the child
print(childCounter);
setState(() {
_counter = childCounter;
});
},
),
Text("Parent"),
Center(
child: Text(_counter.toString()),
)
],
),
),
);
}
}
to change the child state from it's parent
you can use didUpdateWidget() here you can see the docs
I want to draw three widgets inside a column but first widget to be drawn after 10 min.
{Edit: After getting answer from Nickname i tried Timer.duration but it throws error: The method 'Timer' isn't defined for the type '_MainScreenState'. Try correcting the name to the name of an existing method, or defining a method named 'Timer'. What should i do now}
Thank you
class MainScreenPortraitPageUI extends StatefulWidget {
#override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreenPortraitPageUI> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("App title goes here")),
body: Column(
children: [
Text('I want this widget to be drawn after some delay),
Text("Second widget"),
Text("Third widget"),
],
),
);
}
}
Try something like this. The idea is:
-you set a local variable that controls if your Text is drawn or not (true/false).
-in init state setup the timer that will after 10 minutes flip the variable to true. It will also call setState(), so your screen will be rebuilt.
(I didn't try to compile this, so it could throw some errors on the first try):
class MainScreenPortraitPageUI extends StatefulWidget {
#override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreenPortraitPageUI> {
bool drawFirstWidget=false;
#override
void initState() {
super.initState();
Timer(Duration(minutes: 10), () {
setState(() {drawFirstWidget=true});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("App title goes here")),
body: Column(
children: [
if (drawFirstWidget) Text('I want this widget to be drawn after some delay'),
Text("Second widget"),
Text("Third widget"),
],
),
);
}
}
Try wrapping it in a Timer function and see how that works?
Timer(Duration(minutes: 10), () {
Text('I want this widget to be drawn after some delay');
});
you will need to import 'dart:async'; for this function to work.
I have a small flutter application with several stateful and stateless widgets.
My drawer widget looks like follows:
class AppDrawer extends StatelessWidget{
#override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
_createDrawerHeader(),
ListTile(
title: Text('Scenario'),
onTap: () {
Navigator.pop(context);
Navigator.pushReplacementNamed(context, Routes.scenario);
},
),
...
This works fine for switche between my widgets. I now have a simple counter widget that stores a counter variable in its state.
class Counter extends StatefulWidget {
static const String routeName = '/Counter';
int _counter = 0;
#override
_CounterState createState() => new _CounterState();
}
class _CounterState extends State<Counter> {
void _increment() {
setState(() {
widget._counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: <Widget>[
new RaisedButton(
onPressed: _increment,
child: new Text('Increment'),
),
new Text('Count: ${widget._counter}'),
],
),
drawer: AppDrawer(),
);
}
}
If I switch between this and my other widgets with the drawer and go back to the counter widgets the counter is always 0. Looks like the state is initialized everytime.
I am a beginner using flutter and I thougt I can save a variable within this state. I think I am wrong. As my search didn't get my some usable results maybe you can give me an idea how to solve this or just provide some links with information.
Thanks for your help :)
I suggest learning some sort of State Management practice's. You will eventually need to learn this because this is an essential step.Try provider its easy and simple to use.