In this counter example, reproducible on dart pad, I am expecting the child widget to rebuild when I tap on the increment button because it is in the same tree as a the build tree of CounterParentState.
That or there is something about widget tree or the setState method that I don't understand.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class Controller {
late CounterParentState view;
void attach(CounterParentState v) {
this.view = v;
}
int counter = 0;
void incrementCounter() {
counter++;
this.view.applyState();
}
}
class CounterChild extends StatelessWidget {
final Controller controller;
const CounterChild({Key? key, required this.controller}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${controller.counter}',
style: Theme.of(context).textTheme.headline4,
),
],
),
);
}
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
final controller = Controller();
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: CounterParent(
title: 'Flutter Demo Home Page',
controller: controller,
child: CounterChild(controller: controller),
),
);
}
}
class CounterParent extends StatefulWidget {
final Controller controller;
final String title;
final Widget child;
CounterParent({
Key? key,
required this.title,
required this.controller,
required this.child,
}) : super(key: key);
#override
CounterParentState createState() => CounterParentState();
}
class CounterParentState extends State<CounterParent> {
void applyState() {
setState(() {});
}
#override
void initState() {
widget.controller.attach(this);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: widget.child,
floatingActionButton: FloatingActionButton(
onPressed: widget.controller.incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Why I chose this design ?
I choose this design because I wanted to be able to re-use the CounterParent and freely replace the child of the CounterParent
class CounterStylishChild extends StatelessWidget {
final Controller controller;
const CounterStylishChild({Key? key, required this.controller}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times: (And I am stylish too)',
),
Text(
'${controller.counter}',
style: Theme.of(context).textTheme.headline4,
),
],
),
);
}
}
class MyMoreStylishApp extends StatefulWidget {
const MyMoreStylishApp({Key? key}) : super(key: key);
#override
MyMoreStylishAppState createState() => MyMoreStylishAppState();
}
class MyMoreStylishAppState extends State<MyMoreStylishApp> {
final controller = Controller();
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: CounterParent(
title: 'Counter Parent with a better looking child',
controller: controller,
child: CounterStylishChild(controller: controller),
),
);
}
}
setState is working as expected and rebuilding CounterParent, but you passed a Widget instance ("child") that is stored in CounterParent and stays constant as it's constructed in MyAppState, which is never rebuilt in your code. This is the equivalent of "storing" a widget, which some do to prevent unnecessary rebuilds.
For example, the AnimatedBuilder widget does this to prevent unnecessary rebuilds of larger children, while rebuilding the widgets that have changing values that create the animation.
The AnimatedBuilder example code may provide more understanding:
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: Container(
width: 200.0,
height: 200.0,
color: Colors.green,
child: const Center(
child: Text('Whee!'),
),
),
builder: (BuildContext context, Widget? child) {
return Transform.rotate(
angle: _controller.value * 2.0 * math.pi,
child: child,
);
},
);
}
The "complex" child (the Container) is passed to the AnimatedBuilder, which stores the widget. It can then pass this widget as a parameter in the builder that is called rapidly. The Transform widget gets its new rotation value on every call of the builder, but the potentially "complex" child is never actually rebuilt.
This is identical to the situation you have here, except your situation is unintentional. It's inadvisable to have multiple widgets so dependent on each other and organized in such a complex manner like what you have here.
Though you don't ask for this in your question, I'll provide a fix. This may not be what you want and it may defeat the purpose of this elaborate design, but it's the simple way to make a pattern such as this function. Elaboration as to why you wanted to do this would be helpful if this solution is not satisfactory.
The CounterChild should be instantiated in the CounterParent. This way, when setState is called for the parent, a new instance of the child is create and it is rebuilt.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class Controller {
late CounterParentState view;
void attach(CounterParentState v) {
this.view = v;
}
int counter = 0;
void incrementCounter() {
counter++;
this.view.applyState();
}
}
class CounterChild extends StatelessWidget {
final Controller controller;
const CounterChild({Key? key, required this.controller}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${controller.counter}',
style: Theme.of(context).textTheme.headline4,
),
],
),
);
}
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
final controller = Controller();
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: CounterParent(
title: 'Flutter Demo Home Page',
controller: controller,
),
);
}
}
class CounterParent extends StatefulWidget {
final Controller controller;
final String title;
CounterParent({
Key? key,
required this.title,
required this.controller,
}) : super(key: key);
#override
CounterParentState createState() => CounterParentState();
}
class CounterParentState extends State<CounterParent> {
void applyState() {
setState(() {});
}
#override
void initState() {
widget.controller.attach(this);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: CounterChild(controller: widget.controller),
floatingActionButton: FloatingActionButton(
onPressed: widget.controller.incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
If you'd like to keep the CounterChild where it is, you might consider using InheritedNotifier:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class Controller extends ChangeNotifier {
int counter = 0;
void incrementCounter() {
counter++;
notifyListeners();
}
}
class Counter extends InheritedNotifier<Controller> {
const Counter({
Key? key,
required Controller controller,
required Widget child,
}) : super(key: key, child: child, notifier: controller);
static Controller of(BuildContext context) {
final Controller? result = context.dependOnInheritedWidgetOfExactType<Counter>()?.notifier;
assert(result != null, 'No Controller found in context');
return result!;
}
#override
bool updateShouldNotify(Counter old) => notifier?.counter != old.notifier?.counter;
}
class CounterChild extends StatelessWidget {
const CounterChild({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${Counter.of(context).counter}',
style: Theme.of(context).textTheme.headline4,
),
],
),
);
}
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
final Controller controller = Controller();
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Counter(
controller: controller,
child: CounterParent(
title: 'Flutter Demo Home Page',
child: CounterChild(),
),
),
);
}
}
class CounterParent extends StatefulWidget {
final String title;
final Widget child;
CounterParent({
Key? key,
required this.title,
required this.child,
}) : super(key: key);
#override
CounterParentState createState() => CounterParentState();
}
class CounterParentState extends State<CounterParent> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: widget.child,
floatingActionButton: FloatingActionButton(
onPressed: Counter.of(context).incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Or if you want to preserve more of your original structure, you need to pass your MyApp setState instead of your CounterParent setState so both the parent and child are rebuilt:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class Controller extends ChangeNotifier {
int counter = 0;
void incrementCounter() {
counter++;
notifyListeners();
}
}
class CounterChild extends StatelessWidget {
final Controller controller;
const CounterChild({Key? key, required this.controller}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${controller.counter}',
style: Theme.of(context).textTheme.headline4,
),
],
),
);
}
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
late final Controller controller;
#override
void initState() {
super.initState();
controller = Controller()
..addListener(()=>setState((){}));
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: CounterParent(
controller: controller,
title: 'Flutter Demo Home Page',
child: CounterChild(controller: controller),
),
);
}
}
class CounterParent extends StatefulWidget {
final String title;
final Widget child;
final Controller controller;
CounterParent({
Key? key,
required this.title,
required this.controller,
required this.child,
}) : super(key: key);
#override
CounterParentState createState() => CounterParentState();
}
class CounterParentState extends State<CounterParent> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: widget.child,
floatingActionButton: FloatingActionButton(
onPressed: widget.controller.incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Related
I have a list of integers. Each of this item is displayed in a statefull widget by iterating the list in the build method.
import 'package:flutter/material.dart';
import 'package:widget_list/ItemWidget.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Item list state demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Item list state demo'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static int itemsCount = 0;
final List<int> _items = List.empty(growable: true);
void _add() {
setState(() {
_items.add(itemsCount++);
});
}
void _remove() {
setState(() {
_items.removeAt(0);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Row(
children: [
TextButton(
onPressed: () => _add(),
child: const Text('Add item'),
),
TextButton(
onPressed: () => _items.isNotEmpty ? _remove() : null,
child: const Text('Remove item'),
),
],
),
for (var item in _items) ItemWidget(item: item),
],
),
),
);
}
}
Each of this widget, has a statically incremented integer "id" in it's state. Both the item and the widget id are displayed.
import 'package:flutter/material.dart';
var widgetCount = 0;
class ItemWidget extends StatefulWidget {
final int item;
const ItemWidget({
required this.item,
Key? key,
}) : super(key: key);
#override
State<ItemWidget> createState() => _ItemWidgetState();
}
class _ItemWidgetState extends State<ItemWidget> {
final int widgetId = widgetCount++;
#override
Widget build(BuildContext context) {
print("Item ${widget.item} / Widget $widgetId");
return Text("Item ${widget.item} / Widget $widgetId");
}
}
When I add an item in the list, it is displayed in a newly generated widget. E.g. first item 0 is displayed in widget 0.
But if I remove an item at the beginning of the list (e.g. item 0), it's not the first widget that is destoyed, but the last one. The item 1 is then displayed in widget 0.
The widget item is final, so it cannot change. The widget ids are still the same, so the states were not rebuild. Then, why are the states no more consistent with the widgets?
This is done in FLutter desktop for Linux, v3.0.1
In the itemWidget you are creating a value from 0 so for each element that is rendered it will start from 0. please check the code below
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Item list state demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Item list state demo'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static int itemsCount = 0;
final List<ItemInfo> _items = List.empty(growable: true);
void _add() {
setState(() {
itemsCount++;
_items.add(ItemInfo(itemsCount, itemsCount));
});
}
void _remove() {
setState(() {
_items.removeAt(0);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Row(
children: [
TextButton(
onPressed: () => _add(),
child: const Text('Add item'),
),
TextButton(
onPressed: () => _items.isNotEmpty ? _remove() : null,
child: const Text('Remove item'),
),
],
),
for (var item in _items) ItemWidget(item: item),
],
),
),
);
}
}
and Itemwidget to be like this
class ItemWidget extends StatefulWidget {
final ItemInfo item;
const ItemWidget({
required this.item,
Key? key,
}) : super(key: key);
#override
State<ItemWidget> createState() => _ItemWidgetState();
}
class _ItemWidgetState extends State<ItemWidget> {
#override
Widget build(BuildContext context) {
return Text(
"Item ${widget.item.itemVal} / Widget ${widget.item.itemIndex}");
}
}
also I created a class named ItemInfo which will hold both the value and its index.
class ItemInfo {
int itemVal;
int itemIndex;
ItemInfo(this.itemVal, this.itemIndex);
}
In my Flutter project, I have the default Flutter code in home screen to update a counter by floating action button press. Here's the code of that class-
main.dart
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
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.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: UpdateWidget().showButton()
),
);
}
}
Now, I just wanted to update the counter(state variable) without making it static from a new class named UpdateWidget.
I have tried updating the variable like this way:
UpdateWidget.dart
class UpdateWidget extends StatefulWidget {
UpdateWidget({Key? key}) : super(key: key);
#override
_UpdateWidgetState createState() => _UpdateWidgetState();
GlobalKey<MyHomePageState> homeKey = GlobalKey();
Widget showButton() {
return FlatButton(
onPressed: () {
MyHomePage(key: homeKey, title: 'Hello',);
debugPrint('pressed');
homeKey.currentState?.counter = 5;
}, child: const Text('A'),
);
}
}
class _UpdateWidgetState extends State<UpdateWidget> {
double defaultScreenWidth = 400.0;
double defaultScreenHeight = 810.0;
#override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text(''),
),
);
}
}
In, UpdateWidget class, inside FlatButton onPressed block, the print message is showing but it is not updating the counter variable like the way I wanted.
So, I need a way to update this variable and show that in UI.
I'm trying to find a simple and clean way to manage my state in Flutter without using third party libraries. I chose to use ValueNotifier in combination with ValueListenableBuilder in order to be selective about what's being rebuilt in my app when changes occurs.
I've created a class to expose my state values using static variables so they can be used and get directly updated from anywhere without having to do any complicated gymnastic. I'm not sure it is a good practice (it looks to easy in comparison to other state management solutions) but it works.
Could you tell me why this potentially wouldn't be a viable solution?
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#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({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final CounterState _counterState = CounterState();
#override
Widget build(BuildContext context) {
final count = _counterState.counter;
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:',
),
ValueListenableBuilder(
valueListenable: _counterState.counter,
builder: (BuildContext context, int count, _) => Text(
'parent count: $count',
style: Theme.of(context).textTheme.headline4,
)),
const ChildWidget()
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => count.value++,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class ChildWidget extends StatefulWidget {
const ChildWidget({Key? key}) : super(key: key);
#override
State<ChildWidget> createState() => _ChildWidgetState();
}
class _ChildWidgetState extends State<ChildWidget> {
final CounterState _counterState = CounterState();
#override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: _counterState.counter,
builder: (BuildContext context, int count, _) => Center(
child: Text(
'child count: $count',
style: Theme.of(context).textTheme.headline4,
),
),
);
}
}
class CounterState {
static final ValueNotifier<int> _counter = ValueNotifier<int>(0);
ValueNotifier<int> get counter => _counter;
}
Even after reading this and this, I still can't seem to wrap my head around storing page states in Flutter.
I've built a sample app, which has a main page called MyHomePage and a second page called SecondPage. MyHomePage has a floating action button, which displays SecondPage via Navigator.push(...). The second page contains a text field with an assigned controller. I would like to preserve the text field's text after I close and reopen SecondPage.
I've tried all sorts of combinations with setting buckets, page states and keys (inspired by the links above), but I couldn't make it work.
Also I'd like to store the whole page state automatically - without the need to write/retrieve every single value manually (in case I have a lot of text fields on the page).
Here is my code:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
PageStorageKey mykey = new PageStorageKey("testkey");
class MyApp extends StatelessWidget {
final PageStorageBucket _bucket = new PageStorageBucket();
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: PageStorage(
bucket: _bucket,
child: MyHomePage(),
)
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("State demo"),
),
body: Center(
child: Column(
children: <Widget>[
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _openSecondPage,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
_openSecondPage() {
Navigator.push(context, new MaterialPageRoute(builder: (context) => new SecondPage()));
}
}
class SecondPage extends StatefulWidget {
#override
_SecondPageState createState() => _SecondPageState();
}
class _SecondPageState extends State<SecondPage> {
final _aController = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second page"),
),
body: Center(
child: TextField(
controller: _aController,
key: mykey,
autofocus: true,
),
)
);
}
}
EDIT:
Based on Ajay's answer, I was able to greatly simplify the working code. Turns out that in order to persist widget states manually, all you need is an instance of PageStorageBucket in combination with ValueKey instances.
Here are the modifications I did to Ajay's code:
Removed the after_layout plugin (initState method is sufficient).
Removed the global PageStorageKey instance (replaced it with a local ValueKey instance in the page that needs to use it).
Removed global instance of PageStorageBucket and replaced it with a final instance in MyApp, which is passed to the pages that need it via constructor attributes.
Removed PageStorage from the component tree.
Here is the resulting code (simplest working form):
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final bucket = PageStorageBucket();
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(bucket: bucket,),
);
}
}
class MyHomePage extends StatefulWidget {
final PageStorageBucket bucket;
const MyHomePage({Key key, this.bucket}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("State demo"),
),
body: Center(
child: Column(
children: <Widget>[],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _openSecondPage,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
_openSecondPage() {
Navigator.push(
context, new MaterialPageRoute(builder: (context) => new SecondPage(bucket: widget.bucket,)));
}
}
class SecondPage extends StatefulWidget {
final PageStorageBucket bucket;
const SecondPage({Key key, this.bucket}) : super(key: key);
#override
_SecondPageState createState() => _SecondPageState();
}
class _SecondPageState extends State<SecondPage> {
static const KEY_A = ValueKey("secondPage.A");
final _aController = TextEditingController();
#override
void initState() {
super.initState();
_aController.addListener(_updateValue);
String value = widget.bucket.readState(context, identifier: KEY_A) ?? "";
_aController.text = value;
}
_updateValue() {
widget.bucket.writeState(context, _aController.text, identifier: KEY_A);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second page"),
),
body: Center(
child: TextField(
controller: _aController,
autofocus: true,
),
),
);
}
}
you need to read and write the state as well.
Check out the below code.
Note: I have used after_layout to initialize the text controller.
import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
PageStorageKey mykey = new PageStorageKey("testkey");
final PageStorageBucket _bucket = new PageStorageBucket();
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: PageStorage(
bucket: _bucket,
child: MyHomePage(),
));
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("State demo"),
),
body: Center(
child: Column(
children: <Widget>[],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _openSecondPage,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
_openSecondPage() {
Navigator.push(
context, new MaterialPageRoute(builder: (context) => new SecondPage()));
}
}
class SecondPage extends StatefulWidget {
#override
_SecondPageState createState() => _SecondPageState();
}
class _SecondPageState extends State<SecondPage>
with AfterLayoutMixin<SecondPage> {
final _aController = TextEditingController();
#override
void initState() {
super.initState();
_aController.addListener(_updateValue);
}
#override
void afterFirstLayout(BuildContext context) {
String value =
_bucket.readState(context, identifier: ValueKey(mykey)) ?? "";
print(value);
_aController.text = value;
}
_updateValue() {
_bucket.writeState(context, _aController.text, identifier: ValueKey(mykey));
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second page"),
),
body: Center(
child: TextField(
controller: _aController,
key: mykey,
autofocus: true,
),
),
);
}
}
There are two stateful widgets.
The state of MyHomePage contains the counter.
The MyHomePage wraps content in a second stateful widget SubPage.
The SubPage has a child widget with data from the MyHomePage.
To clarify the problem, the first textwidget which is inside of the SubPage child doesn't update when the counter changes.
The textwidget outside of the SubPage increments as expected.
What do we have to do if we want the content of the inner stateful widget updated?
We have to use a stateful widget there. In the real application this widget has a real use-case.
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.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
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> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: Column(
children: <Widget>[
SubPage(
child: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class SubPage extends StatefulWidget {
SubPage({Key key, this.child}) : super(key: key);
final Widget child;
#override
SubPageState createState() => new SubPageState(child);
}
class SubPageState extends State<SubPage> {
final Widget child;
SubPageState(this.child);
#override
Widget build(BuildContext context) {
print("subpage build");
return this.child;
}
}
You don't have to set child as field of state. Actually it is cause of this bug. Here working code
class SubPage extends StatefulWidget {
SubPage({Key key, this.child}) : super(key: key);
final Widget child;
#override
SubPageState createState() => new SubPageState();
}
class SubPageState extends State<SubPage> {
#override
Widget build(BuildContext context) {
return widget.child;
}
}