Call method of a widget from another widget - flutter

I just started flutter, this is a basic question but I am not able to solve.
I created a stateful widget and I need to call the setState() method on click of a button. The button is not part of this stateful widget. The button is present in the footer of application.
complete application code:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
build(BuildContext context) {
return new MaterialApp(
title: "My app title",
home: new Scaffold(
appBar: new AppBar(
title: new Text("My App"),
backgroundColor: Colors.amber,
),
body: new Container(
child: new Center(
child: new MyStateFullWidget(),
),
),
persistentFooterButtons: <Widget>[
new FlatButton(
onPressed: () {
// I need to call the update() of MyStateFullWidget/MyStateFullWidgetState class
},
child: new Text("Click Here"),
color: Colors.amber,
textColor: Colors.white,
),
],
));
}
}
class MyStateFullWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new MyStateFullWidgetState();
}
}
class MyStateFullWidgetState extends State<MyStateFullWidget> {
int count = 0;
#override
Widget build(BuildContext context) {
return new Text("Count: $count");
}
update() {
setState() {
count++;
}
}
}

I need to call the setState() method on click of a button
You may have a few options (or alternatives) to achieve the end result (all with different tradeoffs):
Elevate the state (i.e. count) to an Inherited Widget above the Button and Display Widget. This may be the easiest or most appropriate.
Leverage some kind of Action-based communication such as Flutter Redux (where you dispatch an action, which affects the display widget via a StoreConnector and rebuilds). This can be seen as just another way to 'elevate' state. However, this requires a whole new dependency and a lot of overhead given your simple example, but I wanted to point it out.
You can create some kind of Stream and StreamController that the Display widget subscribes/listens to. However, this may be overkill and I'm not sure how appropriate representing button clicks over a stream would be.
There may be other solutions that I'm not thinking of; however, remember that the goal of reactive UI is to keep state simple.
So if you have multiple leaf widgets that care about a piece of state (want to know it, want to change it), it might mean that this belongs to a higher level component (e.g a App-level piece of state, or maybe some other common ancestor - but both could use Inherited Widget to prevent passing the piece of state all around)

You should use your Scaffold in the State instead of using in StatelessWidget.
Here is the working solution.
class MyApp extends StatelessWidget {
#override
build(BuildContext context) {
return MaterialApp(
title: "My app title",
home: MyStateFullWidget());
}
}
class MyStateFullWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return MyStateFullWidgetState();
}
}
class MyStateFullWidgetState extends State<MyStateFullWidget> {
int count = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("My App"),
backgroundColor: Colors.amber,
),
body: Container(
child: Center(
child:Text("Count: $count"),
),
),
persistentFooterButtons: <Widget>[
FlatButton(
onPressed: update,
child: Text("Click Here"),
color: Colors.amber,
textColor: Colors.white,
),
],
);
}
void update() {
setState(() {
count++;
});
}
}

Related

How to interact with a child Stateful Widget from a parent Stateless Widget?

Context :
Say I have a custom DisplayElapsedTime Widget which is Stateful and holds a Text Widget. With a Timer, every 1sec, the text value is updated with the current elapsed time given by a Stopwatch.
And now, say I have a page which is Stateless and has the DisplayElapsedTime Widget as a child.
What I would like to do :
On the click of a "Start" button in my page, I would like to start the DisplayElapsedTime (which means starting the Stopwatch and the Timer).
From my page, I would also like to have access to the elapsed time value of the Stopwatch "whenever I want".
Why I am having a hard time :
So far (see: I use Stateless Widget alongside BLoC almost everytime. Am I wrong?), I have almost always worked with Stateless Widget alongside the pattern BLoC and never used Stateful. Currently, having extremely long and complex Widgets, I am starting to sense the "limits" of not using the better of the two worlds. But I don't quite fully understand how the Widgets should be interacting between one another.
I really cannot find the solution to my problem anywhere (or am really bad at searching). However, surely, I cannot be the first person to want to have "control" over a Stateful Widget from a Stateless Widget, right ?
Thank you so much in advance for any help.
If I understand your question correctly, let me try to explain this using the most familiar app of all time, the beginning counter app.
This snippet contains a single StatefulWidget that controls its ability to rebuild using its setState method _incrementCounter. So, the value is incremented and the widget is rebuilt whenever the StatefulWidget calls the setState method inside itself.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
#override
State<MyHomePage> 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>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
A StatefulWidget can fully rebuild itself, and when doing so, it may also rebuild all its children downstream of it in the widget tree (but not always, as const widgets are not rebuilt). To get another widget to rebuild a parent widget (upstream of it in the widget tree), you need to have that StatefulWidget's setState function. This can be done using a callback function. A callback function is made by the parent widget and passed to a child widget. So, in the following example, I have made a StatelessWidget with a button, which controls its parent widget because it calls its parent's callback function; notice that I give:
ExampleStlessWidget(counter: _counter, fx: _incrementCounter),
and not:
ExampleStlessWidget(counter: _counter, fx: _incrementCounter()),
Passing _incrementCounter() with the parenthesis calls it at the moment it is passed, while _incrementCounter allows it to be called downstream in the widget tree.
Use the callback function in the child widget by calling it anywhere (notice the parentheses).
onPressed: () {
fx();
},
Here is the new code
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
#override
State<MyHomePage> 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: ExampleStlessWidget(counter: _counter, fx: _incrementCounter),
);
}
}
class ExampleStlessWidget extends StatelessWidget {
const ExampleStlessWidget({
super.key,
required int counter,
required this.fx,
}) : _counter = counter;
final int _counter;
final Function fx;
#override
Widget build(BuildContext context) {
return Column(
children: [
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
ElevatedButton(
onPressed: () {
fx();
},
child: const Text('Click me'),
),
],
);
}
}
A bloc involves an inherited widget, which allows for monitoring the state throughout the widget tree and rebuilds widgets depending on that state. So, a bloc doesn't need a StatefulWidget to change UI. It would help if you did not look at one tool's ability to rebuild widgets as bad or good. It would be best to look at StatefulWidgets and BLoC as different tools for different jobs.
I hope this helps. Happy coding.

Is it possible to keep a widget while navigating to a new page?

In flutter, you can for example have two pages with their own sets of widgets that when you navigate from one page to the other, the widgets on the screen will change to the other one.
But is it possible to have a completely seperate layer of widgets, that is drawn on top of all pages and it's widgets would remain when the navigation happens?
Completely possible!
Method 1: Subnavigators
Make the widget you want to stay consistent be at the same level or higher up in the widget tree than the navigator. For example (MaterialApp widget creates navigator automatically at its level)
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Column(
children: [
Container(
height: 100,
color: Colors.red,
),
Expanded(
child: MaterialApp(
home: PageOne(),
),
),
],
));
}
}
class PageTwo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(),
appBar: AppBar(
title: Text("Page Two"),
),
);
}
}
class PageOne extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
child: Text("Open Page Two"),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => PageTwo()),
);
},
),
),
appBar: AppBar(
title: Text("Page One"),
),
);
}
}
This will keep a red container over any navigation. You could use a Stack instead of a Column if you wanted an overlay. At this point we have two navigators because we have two MaterialApps. Calling navigator.of(context) retrieves the closest one in the widget tree, which will be the subnavigator, allowing our navigation to only affect everything under the second MaterialApp widget
Method 2: Keys
I won't go too in depth on these here. But keys are a way to identify widget across rebuilds. You can give the widget you want to stay consistent a key, and it will be considered the same widget on rebuilds. Check out more information on keys in flutter: https://medium.com/flutter/keys-what-are-they-good-for-13cb51742e7d

'setState' not working in navigated page if custom color is used

For some reason, whenever I navigate to another route using the way described in flutter's documentation i.e https://flutter.dev/docs/cookbook/navigation/navigation-basics, and if I have used custom color in the following way:
color: Color(0xff0e0f26),
in that route, the setState method doesn't work in it. However, if I use color in the following way: color: Colors.blue, the setState method works. I have no idea what is causing this. I want to use a color value that is not present amongst the colors that flutter provides. How do I fix this? The full code along with explanation (using comments) is here:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'test',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: homepage(),
);
}
}
class homepage extends StatefulWidget{
#override
_homepageState createState() => _homepageState();
}
class _homepageState extends State<homepage>{
#override
Widget build(BuildContext context){
return Scaffold(
body: Container(
alignment: Alignment.center,
color: Color(0xff0e0f26),
child: RaisedButton(
onPressed: (){
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
},
),
)
);
}
}
class SecondRoute extends StatefulWidget{
#override
_SecondRouteState createState() => _SecondRouteState();
}
class _SecondRouteState extends State<SecondRoute>{
bool test = false;
#override
Widget build(BuildContext context){
return Scaffold(
appBar: AppBar(
title: Text("Second"),
),
body: Container(
alignment: Alignment.center,
color: Color(0xff0e0f26), //Here, if I use 'color: Colors.blue', setState works.
child:Column(
children: [
RaisedButton(
onPressed: (){ //When the button is pressed, setState is triggered.
setState((){ //This should theoretically rebuild the widget with 'test' becoming true
//, thus showing the text widget below in the screen, but it doesn't.
test = true;
});
},
),
test ? Text("HELLO"):SizedBox(), //I want 'test' to become true, thus making the text
//widget come on screen.
],
),
),
);
}
}
Thank you.
Your text ist just too dark.
If you use
test ? Text("HELLO", style: TextStyle(color: Colors.white30),):SizedBox(),
it should work. This just makes your text lighter. You should use ElevatedButton instead of RaisedButton, since RaisedButton is deprecated and could cause problems as well.

How to redraw widget on Flutter when a value changes?

Suppose the following simple StatefulWidget example:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _MyAppState();
}
}
class _MyAppState extends State<MyApp> {
int value = 0;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('App Example')),
body: Row(children:[
Text("hello"),
RaisedButton(
textColor: Colors.white,
color: Colors.blue,
onPressed: (){setState(() { value+=1; });},
child: new Text("Add"),
)
]),
),
);
}
}
My main problem is: how do I redraw Text("hello") every time value changes? I'm using Text as an example, but it could be an widget that has an internal state, and I'd like to redraw it when the value changes. It does not necessarily depends on the value but I want to redraw anyways when value changes.
I don't have much idea ab8 it. I do this kind of stuff by wrapping it into an container and setting height and width equal to some variable.
If your question is if you just want to change the state of that specific widget(Example the Text widget) without disturbing the other widgets. This are some of the state management's which might help other than SetState (My personal fav. BLOC and REDUx).
Update this line:-
Text("hello ${value}"),
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _MyAppState();
}
}
class _MyAppState extends State<MyApp> {
int value = 0;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('App Example')),
body: Center(
child: Column(children: [
Text("hello ${value}"),
RaisedButton(
textColor: Colors.white,
color: Colors.blue,
onPressed: () {
setState(() {
value += 1;
});
},
child: new Text("Add"),
)
]),
),
),
);
}
}

Flutter - Pass state change function to child widget and update state

I am new to flutter and building a sample app to learn it. In the above screenshot, I have created multiple widgets. My main widget contains the following widget.
Boy Girl Selector
Common Card
CounterButton (Plus or Minus)
Calculate Button
My main widget has two counter - age & weight.
CommonCard has below property :
incrementFunction() : I am setting this value from MainWidget as below.
decrementFunction()
ageIncrement() {
setState(() {
age++;
});
}
ageDecrement() {
setState(() {
age--;
});
}
value : age declared in main widget is passed to this value.
CounterButton has below property.
onPressed: increment or decrement function from parent widget is passed here through card widget.
If I keep whole code in main widget then it is working properly. But if I create multiple widget and pass increment and decrement function as argument in child widget onPressed on plus and minus is not working propely. Please share your thoughts. I am missing some fundamental of communication between child and parent widget.
There are different ways to achieve what you like as there are a couple of different state management techniques such as Dependency Injection, ChangeNotifier, BLoC, and so on (search for Flutter State Management for more details).
Here's an example of how you can achieve this on the famous counter example. This example is using dependency injection (we are passing the increment function to the a child widget as callback function). You can copy the code and past it on DartPad to quickly test it and see how it works:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
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>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
SizedBox(height: 50),
MySecondButton(secondButtonIncrement: incrementCounter),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
class MySecondButton extends StatelessWidget {
MySecondButton({Key key, this.secondButtonIncrement}) : super(key: key);
final VoidCallback secondButtonIncrement;
#override
Widget build(BuildContext context) {
return FlatButton(
child: Text("Second Button"),
onPressed: () {
secondButtonIncrement();
},
color: Colors.blue);
}
}
I hope that helps.