Preserve widget state when temporarily removed from tree in Flutter - flutter

I'm trying to preserve the state of a widget, so that if I temporarily remove the stateful widget from the widget tree, and then re-add it later on, the widget will have the same state as it did before I removed it. Here's a simplified example I have:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#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, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool showCounterWidget = true;
#override
Widget build(BuildContext context) {
return Material(
child: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
showCounterWidget ? CounterButton(): Text("Other widget"),
SizedBox(height: 16,),
FlatButton(
child: Text("Toggle Widget"),
onPressed: (){
setState(() {
showCounterWidget = !showCounterWidget;
});
},
)
],
),
),
);
}
}
class CounterButton extends StatefulWidget {
#override
_CounterButtonState createState() => _CounterButtonState();
}
class _CounterButtonState extends State<CounterButton> {
int counter = 0;
#override
Widget build(BuildContext context) {
return MaterialButton(
color: Colors.orangeAccent,
child: Text(counter.toString()),
onPressed: () {
setState(() {
counter++;
});
},
);
}
}
Ideally, I would not want the state to reset, therefor the counter would not reset to 0, how would I preserve the state of my counter widget?

The reason why the widget loose its state when removed from the tree temporarily is, as Joshua stated, because it loose its Element/State.
Now you may ask:
Can't I cache the Element/State so that next time the widget is inserted, it reuse the previous one instead of creating them anew?
This is a valid idea, but no. You can't.
Flutter judges that as anti-pattern and will throw an exception in that situation.
What you should instead do is to keep the widget inside the widget tree, in a disabled state.
To achieve such thing, you can use widgets like:
IndexedStack
Visibility/Offstage
These widgets will allow you to keep a widget inside the widget tree (so that it keeps its state), but disable its rendering/animations/semantics.
As such, instead of:
Widget build(context) {
if (condition)
return Foo();
else
return Bar();
}
which would make Foo/Bar loose their state when switching between them
do:
IndexedStack(
index: condition ? 0 : 1, // switch between Foo and Bar based on condition
children: [
Foo(),
Bar(),
],
)
Using this code, then Foo/Bar will not loose their state when doing a back and forth between them.

Widgets are meant to store transient data of their own within their scope and lifetime.
Based on what you have provided, you are trying to re-create CounterButton child widget, by removing and adding it back to the widget tree.
In this case, the counter value that is under the CounterButton was not saved or not saving in the MyHomePage screen, the parent widget, without any reference to a view model or any state management within or at the top level.
A more technical overview how Flutter renders your widgets
Ever wonder what is the key if you try to create a constructor for a widget?
class CounterButton extends StatefulWidget {
const CounterButton({Key key}) : super(key: key);
#override
_CounterButtonState createState() => _CounterButtonState();
}
keys (key) are identifiers that are automatically being handled and used by the Flutter framework to differentiate the instances of widgets in the widget tree. Removing and adding the widget (CounterButton) in the widget tree resets the key assigned to it, therefore the data it holds, its state are also removed.
NOTE: No need to create constructors for the a Widget if it will only contain key as its parameter.
From the documentation:
Generally, a widget that is the only child of another widget does not need an explicit key.
Why does Flutter changes the key assigned to the CounterButton?
You are switching between CounterButton which is a StatefulWidget, and Text which is a StatelessWidget, reason why Flutter identifies the two objects completely different from each other.
You can always use Dart Devtools to inspect changes and toggle the behavior of your Flutter App.
Keep an eye on #3a4d2 at the end of the _CounterButtonState.
This is the widget tree structure after you have toggled the widgets. From CounterButton to the Text widget.
You can now see that the CounterButton ending with #31a53, different from the previous identifier because the two widgets are completely different.
What can you do?
I suggest that you save the data changed during runtime in the _MyHomePageState, and create a constructor in CounterButton with a callback function to update the values in the calling widget.
counter_button.dart
class CounterButton extends StatefulWidget {
final counterValue;
final VoidCallback onCountButtonPressed;
const CounterButton({Key key, this.counterValue, this.onCountButtonPressed})
: super(key: key);
#override
_CounterButtonState createState() => _CounterButtonState();
}
class _CounterButtonState extends State<CounterButton> {
#override
Widget build(BuildContext context) {
return MaterialButton(
color: Colors.orangeAccent,
child: Text(widget.counterValue.toString()),
onPressed: () => widget.onCountButtonPressed(),
);
}
}
Assuming you named your variable _counterValue in the _MyHomePageState, you can use it like this:
home_page.dart
_showCounterWidget
? CounterButton(
counterValue: _counterValue,
onCountButtonPressed: () {
setState(() {
_counterValue++;
});
})
: Text("Other widget"),
In addition, this solution will help you re-use CounterButton or other similar widgets in other parts of your app.
I've added the complete example in dartpad.dev.
Andrew and Matt gave a great talk how Flutter renders widgets under the hood:
https://www.youtube.com/watch?v=996ZgFRENMs
Further reading
https://medium.com/flutter-community/flutter-what-are-widgets-renderobjects-and-elements-630a57d05208
https://api.flutter.dev/flutter/widgets/Widget/key.html

The real solution to this problem is state management. There are several good solutions for this available as concepts and flutter packages. Personally I use the BLoC pattern regularly.
The reason for this is that widget state is meant to be used for UI state, not application state. UI state is mostly animations, text entry, or other state that does not persist.
The example in the question is application state as it is intended to persist longer than the live time of the widget.
There is a little Tutorial on creating a BLoC based counter which could be a good starting point.

Related

StatelessWidget gets rebuilt even though parameters stay the same; notifyListeners() with ChangeNotifier

I've got two StatelessWidgets, one is a child of another. There is also a progress update function which updates the state in the external TransferState.
Once updateProgress function is being called the TransferIndicator widget gets rebuilt immediately. On the other hand, its parent (TransfersListTile) build method isn't called.
It works as expected, however I can't really work out what's the mechanism that's being used here. How Flutter decides to rebuild the: _TransferIndicator given that the parameter is a string hash that's not being changed, but only used as a lookup ID to reach the map in TransferState and load the status and progress.
Documentation: https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html says:
"The build method of a stateless widget is typically only called in
three situations: the first time the widget is inserted in the tree,
when the widget's parent changes its configuration, and when an
InheritedWidget it depends on changes."
If: notifyListeners(); function is removed, the widget doesn't get rebuilt.
It seem to be closely related to: ChangeNotifier, but couldn't find the exact info how it works.
In doc here: https://docs.flutter.dev/development/data-and-backend/state-mgmt/simple#changenotifier there is an example involving ChangeNotifier, however doesn't the receiving widget need to be wrapped around: Consumer (https://docs.flutter.dev/development/data-and-backend/state-mgmt/simple#consumer)?
In my case there is no Consumer wrapping.
class TransfersListTile extends StatelessWidget {
TransfersListTile(this.transfer, {Key? key}) : super(key: key);
final Transfer transfer;
#override
Widget build(BuildContext context) {
return ListTile(
leading: _TransferIndicator(transfer.hash),
title: Text(transfer.name!),
);
}
}
class _TransferIndicator extends StatelessWidget {
const _TransferIndicator(this.hash, {Key? key}) : super(key: key);
final String? hash;
#override
Widget build(BuildContext context) {
final status = context.select((TransferState s) => s.map[hash]?.status) ?? TransferStatus.pending;
final progress = context.select((TransferState s) => s.map[hash].progress.percentage);
return CircularProgressIndicator(
value: status == TransferStatus.completed ? 100 : (progress / 100),
);
}
}
function:
class TransferState with ChangeNotifier {
updateProgress(String hash, TransferProgress progress) {
map[hash]?.progress = progress;
notifyListeners();
}
}
and provider part:
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => TransferState(),
],
child: MyApp(),
)
);
More info about the select method and other convenience methods can be found on the provider (https://pub.dev/packages/provider) package site.
Excerpt:
The easiest way to read a value is by using the extension methods on
[BuildContext]:
context.watch(), which makes the widget listen to changes on T
context.read(), which returns T without listening to it
context.select<T, R>(R cb(T value)), which allows a widget to listen to only a small part of T.

stateful widget error, A constant constructor can't call a non-constant super constructor of 'State'

here is my public git.
https://github.com/CrazyBunnyz/Sociominer_V2/
there are 2 branches on that git.
the main is my original code with stateless for most of the part. the problem comes when I try to integrate my code with ExpansionTile where I need state to detect on onExpansionChanged. i try to implement the example but I keep getting the error below. can anyone please help. you can see both code before and after the error from both branches.
ps : already tried ChatMember({Key? key, this.deviceScreenType}) : super(key: key); it pop other error.
the full issue can be found in https://github.com/flutter/flutter/issues/118311. can anyone help ?
You need to use widget clsss instead of state class. Remove params from ChatMember and use widget class.
class MyStatefulWidget extends StatefulWidget {
final DeviceScreenType? deviceScreenType;
const MyStatefulWidget({super.key, this.deviceScreenType});
#override
State<MyStatefulWidget> createState() => ChatMember();
}
class ChatMember extends State<MyStatefulWidget> {
#override
Widget build(BuildContext context) {
Now to access widget variable do,
Column(
children: List.generate(
members.length,
(index) => MemberCard(
member: members[index],
deviceScreenType: widget.deviceScreenType, //this
),
),
),
Next issue is you are naming differently and it's confusing,
Your case
MyStatefulWidget is a widget class
ChatMember is state class
You need to use MyStatefulWidget like,
Container(
width: 400,
child: MyStatefulWidget(deviceScreenType: deviceScreenType),
),
You can check the pr

Flutter: Is it possible to know if you're currently off stage?

I have a number of pages in my app wrapped in Offstage widgets. Each page makes use of the provider package to render based on state updates (e.g. the user does something, we make a network call and display the result).
As the pages are wrapped in Offstage widgets, the build() methods (and subsequent network calls) are called even if it's not the current page.
Is there a way inside the build() method to know if the widget is currently off stage (and if so, skip any expensive logic)?
I'm assuming I can work something with global state etc, but I was wondering if there was anything built-in in relation to the Offstage widget itself, similar to mounted
You can try finding the parent OffStage widget and see if the offstage property is true or false
#override
Widget build(BuildContext context) {
final offstageParent = context.findAncestorWidgetOfExactType<Offstage>();
if (offstageParent != null && offstageParent.offstage == false) {
// widget is currently offstage.
print('offstaged child');
} else {
// widget is not offstage
print('non-offstaged child');
}
return const Text('Example Widget');
}
I made a custom-made mechanism for the goal you wanna achieve:
First, I am declaring a new Map<String, bool> in a separate file alone that will hold the offStage bool value with the key of each class widget.
Map<String, bool> offStageMap = {};
then in the implementation of the StatefulWidget where the offstage widget is in:
class ExampleWidget extends StatefulWidget {
ExampleWidget({super.key}) {
widgetMapKey = runtimeType.toString();
}
late final String widgetMapKey;
#override
State<ExampleWidget> createState() => _ExampleWidgetState();
}
class _ExampleWidgetState extends State<ExampleWidget> {
final bool defaultIsOffStaged = false;
bool? localStateIsOffStages;
#override
void initState() {
offStageMap[widget.widgetMapKey] ??= defaultIsOffStaged;
super.initState();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
bool previousIsOffStaged = offStageMap[widget.widgetMapKey]!;
setState(() {
localStateIsOffStages =
offStageMap[widget.widgetMapKey] = !previousIsOffStaged;
});
},
child: Offstage(
offstage: localStateIsOffStages ?? offStageMap[widget.widgetMapKey]!,
child: Container(),
),
);
}
} },
child: Offstage(
offstage: localStateIsOffStages ?? offStageMap[widget.widgetMapKey]!,
child: Container(),
),
);
}
}
let me explain what this is about.
first I declared a defaultIsOffStaged where it should be the initial offStage value when nothing is saved in that map.
when that widget is inserted in the widget tree (initState() called), the widget.widgetMapKey of the ExampleWidget widget will be saved in that map with the value of the default one which is defaultIsOffStaged.
offStageMap[widget.widgetMapKey] ??= defaultIsOffStaged;
in the offstage property o the OffStage widget, in this line:
offstage: localStateIsOffStages ?? offStageMap[widget.widgetMapKey]!,
the nullable localStateIsOffStages will be null for the first time since it has no value yet, so offStageMap[widget.widgetMapKey]! which equals to defaultIsOffStaged will be the bool value of offstage.
until now what we have, is a map containing the key that belongs only to the ExampleWidget which is its widget.widgetMapKey with its offStage value, right?
now from all places in your app, you can get the offStage value of that widget with its widgetMapKey like this:
print(offStageMap[ExampleWidget().widgetMapKey]); // true
now let's say you want to change the offstage property of that widget, in my code I used a simple example of GestureDetector, so when we tap in the Text("toggle offstage") area, it toggles offStage, here is what happens:
we got the existing value in the map:
bool previousIsOffStaged = offStageMap[widget.widgetMapKey]!;
then assign the opposite of it, to that widget key in the map, and the localStateIsOffStages bool variable which was nullable, now it has a value.
and as normal so the state updates I wrapped it in a SetState(() {})
now the widget's offstage will be toggled, and every time the widget key in the map will be updated with that new value.
the localStateIsOffStages I declared just to hold the local state when this is happening while the StatefulWidget state updates.
after the StatefulWidget is disposed of (when you pop the route as an example) and open that route again, the initState() will execute but since we have now an entry in the map, it's not null so nothing will happen inside initState().
the localStateIsOffStages will be null, so the offStage property of the Offstage widget will be the value from the map, which is the previous value before the widget is disposed.
that's it, from other places you can check for the offstage value of that specific widget like this:
print(offStageMap[ExampleWidget().widgetMapKey])
you can do it for all your widget pages, so you will have a map containing the offStage values of them all.
I take it one step up, and made those methods that I guess they will help:
this will return a List with the pages where the value is true.
List<String> offstagedPages() {
List<String> isOffStagedPages = [];
offStageMap.forEach((runtimeType, isOffStaged) {
if (isOffStaged) {
isOffStagedPages.add(runtimeType);
}
});
return isOffStagedPages;
}
this will return a true if a page is off staged and false if not:
bool isPageWidgetOffStaged(String runtimeType) {
if (offStageMap.containsKey(runtimeType)) {
return offStageMap[runtimeType]!;
}
return false;
}
Hope this helps a little.
Maybe it's not applicable to you, but you might be able to solve it by simply not using Offstage. Consider this app:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
MyApp({super.key});
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool showFirst = true;
void switchPage() {
setState(() {
showFirst = !showFirst;
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Stack(children: [
Offstage(offstage: !showFirst,child: A("first", switchPage)),
Offstage(offstage: showFirst,child: A("second", switchPage)),
]))));
}
}
class A extends StatelessWidget {
final String t;
final Function onTap;
const A(this.t, this.onTap, {Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
print('$t is building');
return TextButton(onPressed: ()=> onTap(), child: Text(t));
}
}
You will notice by the prints that both pages are build. But if you rewrite it like this without Offstage, only the visible one is build:
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Stack(children: [
if (showFirst) A("first", switchPage),
if (!showFirst) A("second", switchPage),
]))));
}
If you want to just keep state alive your pages , you can use https://api.flutter.dev/flutter/widgets/AutomaticKeepAliveClientMixin-mixin.html , you may check this blog for example usage, https://medium.com/manabie/flutter-simple-cheatsheet-4370a68f98b3
If you are using Navigator, you can just extends NavigatorObserver. Then you will get didpush and didpop, use state to manage elementlifecycle, you will get page onPause and onResume fun.

what does it mean when we see people calling widget in dart?

I have seen many times people calling widget. sth inside the code.
May I know what it is actually doing?
For example code below, (highlighted part is my confusion)
class _MyOwnClassState extends State<MyOwnClass> {
#override
Widget build(BuildContext context) {
return ListTile(
title: Container(
child: Column(children: makeWidgetChildren(**widget.jsonObject)**),
),
);
}
}
In flutter's StatefulWidget, we have the following architecture.
You have a StatefulWidget like this,
class MyOwnClass extends StatefulWidget {
State createState () => _MyOwnClassState();
}
And you have a State class for your StatefulWidget like this,
class _MyOwnClassState extends State<MyOwnClass> {
}
Now, State class is meant to house variables that tend to change in order for your UI to be rebuilt.
So you can have variables in your State that you can update using setState.
But what if you had some data that doesn't change and you want to avoid putting them inside the State class.
That's where your StatefulWidget comes to play.
You can store variables in your MyOwnClass and the widget variable inside the State class gives you a way to access them.
For example,
class MyOwnClass extends StatefulWidget {
int numberThatDoesntChange = 1;
State createState () => _MyOwnClassState();
}
You can access them in your State class like this,
class _MyOwnClassState extends State<MyOwnClass> {
Widget build(BuildContext context) {
return Text('$widget.numberThatDoesntChange');
}
}
Apart from this, your StatefulWidget has many more internal instance members that you can access inside of your State class using the widget variable.
The widget refers to the actual view that renders on the screen. It extends the StatefulWidget class of the flutter framework and overrides the createState() method. The createState() method is used to create the instance of state class. We will look into createState().
The state class is used to maintain the state of the widget so that it can be rebuilt again. It extends the State class of the flutter framework and overrides the build method.
The framework calls build() method again and again whenever setState() method is called. The setState() method notifies the framework that the internal state of this object has changed and it should be rebuilt. Suppose we change the value of text in StatefulWidget then we need to call setState().
Edit As Nisanth pointed outh in his comment - I missed your question completely; please ignore the below....
Let me try my answer, I don't think others are getting your point.
In your exapmle, Column(children: x) expect a list of Widgets.
You have two options - either provide this list directly:
class _MyOwnClassState extends State<MyOwnClass> {
#override
Widget build(BuildContext context) {
return ListTile(
title: Container(
child: Column(children: <Widget>[SomeWidget()]),
),
);
}
}
Or if you have more complex code that generates widget - based on input parameters, or you have the same widget generated multiple times and you want to avoid the code duplication - you would create the separate function to do the job.
Something like:
class _MyOwnClassState extends State<MyOwnClass> {
List<Widget> makeWidgetChildren(int param) {
/*
some very complex logic here
/*
if (param>3 && param<4) {
return List<Widget>.generate(4, (index)=>SomeWidget1(index));
} else {
return <Widget>[Center()];
}
}
#override
Widget build(BuildContext context) {
return ListTile(
title: Container(
child: Column(children: makeWidgetChildren(**widget.jsonObject)**),
),
);
}
}
So basically, it is just to make the code nicer; and to avoid having code repeated over and over again in the build function.

What's the design benefit of Flutter's (Widget)State/StatefulWidget pattern?

My docs and Flutter videos, the explanation of the design of the StatefulWidget (+(Widget)State) is that it:
promotes a declarative design (good)
formalizes the process by which Flutter to efficiently decide which components need to be re-rendered (also good)
From the example:
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) {...}
}
However:
since we have to explicitly remember call setState in order to invalidate the state, is this really a declarative design?
Flutter doesn't automatically detect changes in the State object and decide to call build (although it could have), and so it doesn't really formalize/automate/make-safe the invalidation of view components. Since we have to explicitly call setState, what's the benefit of the Flutter's (Widget)State/StatefulWidget pattern over, let's say:
class MyHomePage extends StatefulWidget // Define dirty method
{
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
int _counter = 0;
_incrementCounter() {
_counter++;
this.dirty(); // Require the view to be rebuilt. Arranges generateView to be called.
}
#override
Widget generateView(BuildContext context) {return ... rendering description containing updated counter ... ;}
}
... which would place the same burden of marking the UI dirty on the programmer, is no less decalrative, and avoids additional abstraction that obfuscates the intention of the program.
What have I missed? What's the benefit of separating of StatefulWidget from (Widget)State in Flutter?
[Before people chime in with MVC comments, note that the Flutter model rather explicitly only manages only the widget's state and its tightly coupled to the UI's Widget through the build method - there is no separation of concern here and it doesn't have a lot to say about larger application state that's not attached to a view.]
[Also, moderators, these not the same questions: Why does Flutter State object require a Widget?, What is the relation between stateful and stateless widgets in Flutter?. My question is one about what's the benefit of the present design, not how this design works.]
Update: #RĂ©mi Rousselet -- Here's a declarative example with only a new state class needing to be declared. With some work, you could even get rid of that (though it may not be better).
This way of declaring interaction with need didn't require (the user) declaring two new circularly type-referencing class, and the widget that is changing in response to state is decoupled from the state (its constructed a pure function of the state and does not need to allocate the state).
This way of doing things doesn't survive hot-reload. (sad face).
I suspect this is more of an issue with hot-reload, but if there's a way to make it work it would be great,
import 'dart:collection';
import 'package:flutter/material.dart';
////////////////////////////////
// Define some application state
class MyAppState with ChangeSubscribeable<MyAppState> {
/***
* TODO. Automate notifyListeners on setter.
* Binds changes to the widget
*/
int _counter;
get counter => _counter;
set counter(int c) {
_counter = c;
notifyListeners(); // <<<<<< ! Calls ... .setState to invalidate widget
}
increment() {
counter = _counter + 1;
}
MyAppState({int counter: 0}) {
_counter = counter;
}
}
void main() => runApp(MyApp5());
class MyApp5 extends StatelessWidget {
#override
Widget build(BuildContext context) {
// Declare the mutable state.
// Note because the state is not coupled to any particular widget
// its possible to easily share the state between concerned.
// StateListeningWidgets register for, and are notified on changes to
// the state.
var state = new MyAppState(counter: 5);
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text('Flutter Demo'),
),
body: Center(
child: Column(
children: [
// When the button is click, increment the state
RaisedButton(
onPressed: () => {
state.increment(),
print("Clicked. New state: ${state.counter}")
},
child: Text('Click me'),
),
// Listens for changes in state.
StateListeningWidget(
state,
// Construct the actual widget based on the current state
// A pure function of the state.
// However, is seems closures are not hot-reload.
(context, s) => new Text("Counter4 : ${s.counter}"),
),
],
))),
);
}
}
// //////////////////////
// Implementation
// This one is the onChange callback should accept the state.
//typedef OnChangeFunc<ARG0> = void Function(ARG0);
typedef OnChangeFunc = void Function();
mixin ChangeSubscribeable<STATE> {
final _listener2Notifier =
new LinkedHashMap<Object, OnChangeFunc>(); // VoidFunc1<STATE>>();
List<OnChangeFunc> get _listeners => List.from(_listener2Notifier.values);
void onChange(listenerKey, OnChangeFunc onChange) {
// onChange(listenerKey, VoidFunc1<STATE> onChange) {
assert(!_listener2Notifier.containsKey(listenerKey));
_listener2Notifier[listenerKey] = onChange;
print("Num listeners: ${_listener2Notifier.length}");
}
void removeOnChange(listenerKey) {
if (_listener2Notifier.containsKey(listenerKey)) {
_listener2Notifier.remove(listenerKey);
}
}
void notifyListeners() {
// _listener2Notifier.forEach((key, value)=>value(state));
// Safer, in-case state-update triggers add/remove onChange:
// Call listener
_listeners.forEach((value) => value());
}
}
typedef StateToWidgetFunction<WIDGET extends Widget,
STATE extends ChangeSubscribeable>
= WIDGET Function(BuildContext, STATE);
void noOp() {}
class _WidgetFromStateImpl<WIDGET extends Widget,
STATE extends ChangeSubscribeable> extends State<StatefulWidget> {
STATE _state;
// TODO. Make Widget return type more specific.
StateToWidgetFunction<WIDGET, STATE> stateToWidgetFunc;
_WidgetFromStateImpl(this.stateToWidgetFunc, this._state) {
updateState(){setState(() {});}
this._state.onChange(this, updateState);
}
#override
Widget build(BuildContext context) => stateToWidgetFunc(context, this._state);
#override
dispose() {
_state.removeOnChange(this);
super.dispose();
}
}
class StateListeningWidget<WIDGET extends Widget,
STATE extends ChangeSubscribeable> extends StatefulWidget {
STATE _watched_state;
StateToWidgetFunction<WIDGET, STATE> stateToWidgetFunc;
StateListeningWidget(this._watched_state, this.stateToWidgetFunc) {}
#override
State<StatefulWidget> createState() {
return new _WidgetFromStateImpl<WIDGET, STATE>(
stateToWidgetFunc, _watched_state);
}
}
I've been directed at the ChangeProvider pattern: https://github.com/flutter/samples/blob/master/provider_counter/lib/main.dart
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Flutter Demo Home Page'),),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
Consumer<Counter>( // <<< Pure. Hidden magic mutable parameter
builder: (context, counter, child) => Text(
'${counter.value}',
style: Theme.of(context).textTheme.display1,
),),],),),
floatingActionButton: FloatingActionButton(
onPressed: () =>
// <<< Also a hidden magic parameter
Provider.of<Counter>(context, listen: false).increment(),
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
... but this also suffers problems:
its not clear to reader of what the state requirements are or how to provide them -- the interface (at least in this github example HomePage) example does not require Counter as a formal parameter. Here we have new HomePage() that has configuration that is not provided in its parameters - this type of access suffers similar problems to global variables.
access to state is by class type, not object reference - so its not clear (or at least straightforward) what to do if you want two objects of the same type (e.g. shippingAddress, billingAddress) that are peers in the model. To resolve this, the state model likely needs to be refactored.
I think I'm with user48956 on this. (Catchy name by the way).
Unfortunately, the Flutter authors seem to have suffixed their View class with the word 'State'. This has rather confused the whole Flutter state management discussions.
I think the purpose of the two classes is actually to make the painting more performant but it comes with a very heavy plumbing cost for us developers.
As to the naming convention:
The dirty flag approach allows the widget painter to optimise their painting without knowing about our state, thereby alleviation the need for two classes.
Also generateView() is kinda meaningful (unless of course, you start using these widgets to hold model-fragments (as per Package:provider).