Flutter - Update parant widget class UI on child button click - flutter

I have such kind of scenario
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Hello"),
),
body: Container(
child: ChildWidget(
listControl: this.sentToScreenBuildJson,
notifyParent: refresh,
),
),
);
}
this is my parent build method where I have added ChildWidget a another statfulscreen and passing is a json and a refresh funtion
as per json child will able to draw UI
and on button click I am able to get callback to refresh method.
refresh() {
print("I get refreshed from child");
setState(() {
print("I get refreshed from child in setState");
this.sentToScreenBuildJson = this.newJson;
});
}
on button click both print get execute but UI is not updating as per newJson.
Like I am expecting that as setState run parent has to call build with passing updated json.
which is not working.
thanks for any help.

When you want to pass data from Child to Parent you should use NotificationListener at parent and dispatch Notification from child.
Instance of Notification class will be having data that you can consume in Parent using NotificationListener.
Mostly all the Flutter Widgets are using this technique, for example tab controller receive OverscrollNotification when user reaches to the last tab and still try to swipe.
Following is the demo that you can use to understand how you can use NotificationListener in your code.
import 'package:flutter/material.dart';
void main() => runApp(ParentWidget());
class ParentWidget extends StatefulWidget {
ParentWidget({Key key}) : super(key: key);
#override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
String _text = 'You have not pressed the button yet';
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: NotificationListener<IntegerNotification>(
onNotification: (IntegerNotification notification) {
setState(() {
print(notification);
_text = 'You have pressed button ${notification.value} times';
});
return true;
},
child: Column(
children: <Widget>[
Text(_text),
ChildWidget(),
],
)
),
),
);
}
}
class ChildWidget extends StatefulWidget {
const ChildWidget({Key key}) : super(key: key);
#override
_ChildWidgetState createState() => _ChildWidgetState();
}
class _ChildWidgetState extends State<ChildWidget> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return RaisedButton(onPressed: (){
IntegerNotification(++_counter).dispatch(context);
},child: Text('Increment counter'),);
}
}
#immutable
class IntegerNotification extends Notification{
final int value;
const IntegerNotification(this.value);
String toString(){
return value.toString();
}
}

Update parant widget class UI on child button click
This is a common use case in flutter and flutter has built in InheritedWidget class for these kind of purpose. You may either directly use it for your purpose or use some ready made package solution which uses InheritedWidget behind the scenes like Provider.

An alternative to #Darish's answer, you can declare a static variable in your class 1, access that static variable in class 2 and then update the state of the variable in the class 2.
For example:
import 'package:flutter/material.dart';
class Demo extends StatefulWidget {
static UserObject userObject;
#override
_Demo createState() => _Demo();
}
class _Demo extends State<Demo> {
#override
void initState() {
Demo.userObject = new UserObject(name: "EXAMPLE NAME");
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xfff3f3f3),
appBar: AppBar(title: Text("DEMO")),
body: InkWell(
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => HeroClass()));
},
child: Center(
child: Hero(
tag: "tag-demo-id",
child: Container(
color: Colors.black,
padding: EdgeInsets.all(20),
child: Text("${Demo.userObject.name} -> CLICK HERE",
style: TextStyle(color: Colors.white)))))));
}
}
class HeroClass extends StatefulWidget {
#override
_HeroClassState createState() => _HeroClassState();
}
class _HeroClassState extends State<HeroClass> {
final myController = TextEditingController();
#override
void initState() {
myController.text = Demo.userObject.name;
super.initState();
}
#override
void dispose() {
// Clean up the controller when the widget is removed from the widget tree.
// This also removes the _printLatestValue listener.
myController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("DEMO HERO")),
body: Hero(
tag: "tag-demo-id",
child: Container(
child: TextField(
controller: myController,
),
)),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
Demo.userObject.name = myController.text;
});
},
child: Icon(Icons.save),
));
}
}
// object class
class UserObject {
String name;
UserObject({this.name});
UserObject.fromJson(Map<String, dynamic> json) {
name = json['name'];
}
}

Related

Flutter setState function doesn't work when used to change class member

i have the following codes,
class mWidget extends StatefulWidget {
mWidget({super.key, required this.text});
String text;
#override
State<mWidget> createState() => _mWidgetState();
}
class _mWidgetState extends State<mWidget> {
#override
Widget build(BuildContext context) {
return Center(
child: Text(widget.text),
);
}
}
This is my custom widget,
class _MainState extends State<Main> {
var n = mWidget(text: "Hi");
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
n,
ElevatedButton(
onPressed: () {
setState(() {
n.text = "Hello";
});
},
child: Text("Click me"),
),
],
),
);
}
}
And this is the code in the main.dart file.
The problem is that pressing the button doesn't change the output on the screen unless a hot reload even though I am calling the setState function.
I wonder why is that.
Thanks in advance!
You made a couple of mistakes in this!
In your code, you made a widget named mWidget and created an instance of it, it is not the right approach to access any widget using an instance, as state of instances cannot be updated.
You are using the state of mWidget outside of its scope, where it is not accessible.
You can use keys to achieve what you want. (It is not advisable to use this for large-scale project)
Here is a small code which can help you to achieve the functionality you want.
class mWidget extends StatefulWidget {
mWidget({Key? key, required this.text}) : super(key: key);
String text;
#override
State<mWidget> createState() => _mWidgetState();
}
class _mWidgetState extends State<mWidget> {
String text = "";
#override
void initState() {
text = widget.text;
super.initState();
}
void updateValue(String newData) {
setState(() {
text = newData;
});
}
#override
Widget build(BuildContext context) {
return Center(
child: Text(text),
);
}
}
class _Main extends StatefulWidget {
const _Main({Key? key}) : super(key: key);
#override
State<_Main> createState() => _MainState();
}
class _MainState extends State<_Main> {
GlobalKey<_mWidgetState> _mWidgetStateKey = GlobalKey(); // This is the key declaration of _mWidgetState type
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
mWidget(text: "Hi", key: _mWidgetStateKey),
ElevatedButton(
onPressed: () =>
_mWidgetStateKey.currentState!.updateValue("Hello"), // Calling the method of _mWidgetState class.
child: Text("Click me"),
),
],
),
);
}
}
You can reinitialize the n on easy approach like
n = mWidget(text: "Hello");
Or use state-management property like riverpod/bloc. callback method may also help. I am using ValueNotifier, you dont need to make theses statefulWidget
class Main extends StatefulWidget {
const Main({super.key});
#override
State<Main> createState() => _MainState();
}
class _MainState extends State<Main> {
final ValueNotifier textNotifier = ValueNotifier('Hi');
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
mWidget(text: textNotifier),
ElevatedButton(
onPressed: () {
setState(() {
textNotifier.value = "Hello";
});
},
child: Text("Click me"),
),
],
),
);
}
}
class mWidget extends StatefulWidget {
mWidget({super.key, required this.text});
ValueNotifier text;
#override
State<mWidget> createState() => _mWidgetState();
}
class _mWidgetState extends State<mWidget> {
#override
Widget build(BuildContext context) {
return Center(
child: ValueListenableBuilder(
valueListenable: widget.text,
builder: (context, value, child) => Text(value),
));
}
}

Flutter Dart setState not working: Unhandled Exception: setState() called in constructor: ...(lifecycle state: created, no widget, not mounted)

I am new to dart / flutter. I am changing the default app created by flutter create command. This is the code (I want to show information from EventSource):
import 'dart:async';
import 'package:w3c_event_source/event_source.dart';
import 'package:flutter/material.dart';
import 'blocs/main_bloc.dart';
void main() async {
runApp(MyApp());
final events = EventSource(Uri.parse("https://example.com/broadcast/"));
final subscription = events.events.listen((MessageEvent message) {
var myhomepagestate = MyHomePageState();
myhomepagestate.eventsource_message = '${message.data}';
//print('${message.data}');
});
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'EXAMPLE',
home: MyHomePage(title: 'EXAMPLE'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
MyHomePageState createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
var _myeventsource = "";
void set eventsource_message(message) {
//if (!mounted) return;
setState(() {
_myeventsource = message;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'EventSource:',
),
Text(
'$_myeventsource',
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
The error is at this command:
setState(() {
_myeventsource = message;
});
The error is: Unhandled Exception: setState() called in constructor: MyHomePageState#8034c(lifecycle state: created, no widget, not mounted)
I have searching for one day and can't find the solution. Can anybody give a hint what I should look into? Can I just name the element, then change the element directly like HTML DOM?
Thanks.
You are instantiating a MyHomePageState widget that you will never need to do. Instead, you have to instantiate the MyHomePage. It does not work in this context though because MyHomePage is still not mounted anywhere. You are just instantiating it and trying to set the value.
Remove this part from the main method:
final events = EventSource(Uri.parse("https://example.com/broadcast/"));
final subscription = events.events.listen((MessageEvent message) {
var myhomepagestate = MyHomePageState();
myhomepagestate.eventsource_message = '${message.data}';
//print('${message.data}');
});
Modify your MyHomePageState like this (Add initState and dispose methods. They are like constructors and destructors):
class MyHomePageState extends State<MyHomePage> {
String _myeventsource = "";
/**
void set eventsource_message(message) {
//if (!mounted) return;
setState(() {
_myeventsource = message;
});
}
**/
// Init state is called once when the widget is mounted. It's more like a constructor for your stateful widget which gets called by flutter itself when the widget is mounted.
// You don't need to call this method explicitly.
// Note that it won't be called again on hot reload. Refer to the documentation for more about it
#override
void initState() {
super.initState();
final events = EventSource(Uri.parse("https://example.com/broadcast/"));
events.events.listen((MessageEvent message) {
setState(() => _myeventsource = message.data);
}
}
// And this method is like a destructor.
// Called automatically by flutter when this widget is removed.
#override
void dispose() {
// Remember to remove the listeners here
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'EventSource:',
),
Text(
'$_myeventsource',
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

Flutter setState() not refreshing widget when called from stateful child

I have two stateful widgets: ParentWidget and ChildWidget.
The ChildWidget has a gesture detector wrapping a container and text. When I call onTap the state of the Parent (status increased by 1) is updated properly but setState() is not refreshing the UI.
I tried everything: global keys, inherited widget but nothing works.
Interestingly if I change the ChildWidget to a stateless one then everything start working. Any ideas would be super helpful.
pk
Here is the code:
import 'package:flutter/material.dart';
import 'package:hexcolor/hexcolor.dart';
class ParentWidget extends StatefulWidget {
#override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
int status = 1;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Hexcolor('#1c486d'),
title: Text(
'Test',
),
),
body: ChildWidget(
child: GestureDetector(
onTap: () {
status = status + 1;
setState(() {}); // this is the problematic piece of code.
},
child: Container(
color: Colors.blue,
width: 100,
height: 100,
child: Text('PARENT:' + status.toString()),
),
),
),
);
}
}
class ChildWidget extends StatefulWidget {
final Widget child;
ChildWidget({this.child});
#override
_ChildWidgetState createState() => _ChildWidgetState(child);
}
class _ChildWidgetState extends State<ChildWidget> {
Widget child;
_ChildWidgetState(this.child);
#override
Widget build(BuildContext context) {
return child;
}
}
You can pass the parent's status to the ChildWidget so that when the parent's status changes, the ChildWidget's state changes and its build method be called.
body: GestureDetector(
onTap: () {
setState(() {
status = status + 1;
});
},
child: ChildWidget(status: status),
),
);
}
}
class ChildWidget extends StatefulWidget {
final int status;
ChildWidget({this.status});
#override
_ChildWidgetState createState() => _ChildWidgetState();
}
class _ChildWidgetState extends State<ChildWidget> {
_ChildWidgetState();
#override
Widget build(BuildContext context) {
return Container(
color: Colors.blue,
width: 100,
height: 100,
child: Text('PARENT:' + widget.status.toString()),
);
}
}
Try not making the child final
class ChildWidget extends StatefulWidget {
Widget child;
ChildWidget({this.child});
#override
_ChildWidgetState createState() => _ChildWidgetState(child);
}

How to use `GlobalKey` to maintain widgets' states when changing parents?

In Emily Fortuna's article (and video) she mentions:
GlobalKeys have two uses: they allow widgets to change parents
anywhere in your app without losing state, or they can be used to
access information about another widget in a completely different part
of the widget tree. An example of the first scenario might if you
wanted to show the same widget on two different screens, but holding
all the same state, you’d want to use a GlobalKey.
Her article includes a gif demo of an app called "Using GlobalKey to ReuseWidget" but does not provide source code (probably because it's too trivial). You can also see a quick video demo here, starting at 8:30 mark: https://youtu.be/kn0EOS-ZiIc?t=510
How do I implement her demo? Where do I define the GlobalKey variable and how/where do I use it? Basically for example, I want to display a counter that counts up every second, and have it on many different screens. Is that something GlobalKey can help me with?
The most common use-case of using GlobalKey to move a widget around the tree is when conditionally wrapping a "child" into another widget like so:
Widget build(context) {
if (foo) {
return Foo(child: child);
}
return child;
}
With such code, you'll quickly notice that if child is stateful, toggling foo will make child lose its state, which is usually unexpected.
To solve this, we'd make our widget stateful, create a GlobalKey, and wrap child into a KeyedSubtree.
Here's an example:
class Example extends StatefulWidget {
const Example({Key key, this.foo, this.child}) : super(key: key);
final Widget child;
final bool foo;
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
final key = GlobalKey();
#override
Widget build(BuildContext context) {
final child = KeyedSubtree(key: key, child: widget.child);
if (widget.foo) {
return Foo(child: child);
}
return child;
}
}
I would not recommend using GlobalKey for this task.
You should pass the data around, not the widget, not the widget state. For example, if you want a Switch and a Slider like in the demo, you are better off just pass the actual boolean and double behind those two widgets. For more complex data, you should look into Provider, InheritedWidget or alike.
Things have changed since that video was released. Saed's answer (which I rewarded 50 bounty points) might be how it was done in the video, but it no longer works in recent Flutter versions. Basically right now there is no good way to easily implement the demo using GlobalKey.
But...
If you can guarantee that, the two widgets will never be on the screen at the same time, or more precisely, they will never be simultaneously inserted into the widget tree on the same frame, then you could try to use GlobalKey to have the same widget on different parts of the layout.
Note this is a very strict limitation. For example, when swiping to another screen, there is usually a transition animation where both screens are rendered at the same time. That is not okay. So for this demo, I inserted a "blank page" to prevent that when swiping.
How to:
So, if you want the same widget, appearing on very different screens (that hopefully are far from each other), you can use a GlobalKey to do that, with basically 3 lines of code.
First, declare a variable that you can access from both screens:
final _key = GlobalKey();
Then, in your widget, have a constructor that takes in a key and pass it to the parent class:
Foo(key) : super(key: key);
Lastly, whenever you use the widget, pass the same key variable to it:
return Container(
color: Colors.green[100],
child: Foo(_key),
);
Full Source:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatelessWidget {
final _key = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Global Key Demo")),
body: PageView.builder(
itemCount: 3,
itemBuilder: (context, index) {
switch (index) {
case 0:
return Container(
color: Colors.green[100],
child: Foo(_key),
);
break;
case 1:
return Container(
color: Colors.blue[100],
child: Text("Blank Page"),
);
break;
case 2:
return Container(
color: Colors.red[100],
child: Foo(_key),
);
break;
default:
throw "404";
}
},
),
);
}
}
class Foo extends StatefulWidget {
#override
_FooState createState() => _FooState();
Foo(key) : super(key: key);
}
class _FooState extends State<Foo> {
bool _switchValue = false;
double _sliderValue = 0.5;
#override
Widget build(BuildContext context) {
return Column(
children: [
Switch(
value: _switchValue,
onChanged: (v) {
setState(() => _switchValue = v);
},
),
Slider(
value: _sliderValue,
onChanged: (v) {
setState(() => _sliderValue = v);
},
)
],
);
}
}
Update: this was an old approach to tackle the state management and not recommended anymore,please see my comments on this answer and also check user1032613's answer below
Global keys can be used to access the state of a statefull widget from anywhere in the widget tree
import 'package:flutter/material.dart';
main() {
runApp(MaterialApp(
theme: ThemeData(
primarySwatch: Colors.indigo,
),
home: App(),
));
}
class App extends StatefulWidget {
#override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
GlobalKey<_CounterState> _counterState;
#override
void initState() {
super.initState();
_counterState = GlobalKey();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
children: <Widget>[
Counter(
key: _counterState,
),
],
)),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.navigate_next),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) {
return Page1(_counterState);
}),
);
},
),
);
}
}
class Counter extends StatefulWidget {
const Counter({
Key key,
}) : super(key: key);
#override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int count;
#override
void initState() {
super.initState();
count = 0;
}
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: () {
setState(() {
count++;
});
},
),
Text(count.toString()),
],
);
}
}
class Page1 extends StatefulWidget {
final GlobalKey<_CounterState> counterKey;
Page1( this.counterKey);
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Row(
children: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: () {
setState(() {
widget.counterKey.currentState.count++;
print(widget.counterKey.currentState.count);
});
},
),
Text(
widget.counterKey.currentState.count.toString(),
style: TextStyle(fontSize: 50),
),
],
),
),
);
}
}

Flutter: preserve state when changing body in Drawer

I am creating a Flutter application with a navigation drawer by using the Drawer class of the Material library. The Widget containing the Drawer is a StatefulWidget and the Scaffold's content is displayed according to the selected item on the navigation drawer. The content is either WidgetOne or WidgetTwo, both maintaining their own state as StatefulWidgets. See the code example below.
At the moment, when I change from one widget to another and back, the whole state of the earlier displayed widget is reloaded. This is not ideal, since both widgets have network calls from an API, and need to be redrawn accordingly.
What I've tried so far
Implementing AutomaticKeepAliveClientMixin on both sub widgets, as suggested here: https://stackoverflow.com/a/50074067/4009506. However, this does not seem to work.
Using an IndexedStack as suggested here: https://stackoverflow.com/a/54999503/4009506. This loads all widgets directly, even if they are not yet displayed.
Code
class DrawerWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() => _DrawerState();
}
class _DrawerState extends State<DrawerWidget> {
Widget _activeWidget;
#override
void initState() {
_activeWidget = FirstWidget();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Drawer demo")),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
ListTile(
title: Text("First Widget"),
onTap: () {
setState(() {
_activeWidget = FirstWidget();
});
},
),
ListTile(
title: Text("Second Widget"),
onTap: () {
setState(() {
_activeWidget = SecondWidget();
});
},
),
],
),
),
body: _activeWidget);
}
}
class FirstWidget extends StatefulWidget {
// [..]
}
class SecondWidget extends StatefulWidget {
// [..]
}
Desired result
WidgetOne and WidgetTwo are only loaded on initial load (after selecting them in the Drawer). Switching to another widget and back should not reload the widget if it was already loaded earlier. The sub widgets should not load all directly, only when they are initially pressed.
Actual result
Both FirstWidget and SecondWidget are reloaded and redrawn each time they are selected in the Drawer.
I resolved this issue by using a PageView and implementing AutomaticKeepAliveClientMixin on all sub widgets:
class DrawerWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() => _DrawerState();
}
class _DrawerState extends State<DrawerWidget> {
final _pageController = PageController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Drawer demo")),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
ListTile(
title: Text("First Widget"),
onTap: () {
_pageController.jumpToPage(0);
},
),
ListTile(
title: Text("Second Widget"),
onTap: () {
_pageController.jumpToPage(1);
},
),
],
),
),
body: PageView(
controller: _pageController,
children: <Widget>[
FirstWidget(),
SecondWidget()
],
physics: NeverScrollableScrollPhysics()
));
}
}
class FirstWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() => _FirstWidgetState();
}
class _FirstWidgetState extends State<FirstWidget> with AutomaticKeepAliveClientMixin<FirstWidget> {
// [..]
#override
bool get wantKeepAlive => true;
}
class SecondWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() => _SecondWidgetState();
}
class _SecondWidgetState extends State<SecondWidget> with AutomaticKeepAliveClientMixin<SecondWidget> {
// [..]
#override
bool get wantKeepAlive => true;
}
Now, all widgets are only loaded upon initial switch in the navigation drawer and do not get reloaded when switching back.