Reputation: 6158
I am new to flutter and the way I get StatefulWidget's state is add a state property to widget, eg:
// ignore: must_be_immutable
class _CustomContainer extends StatefulWidget {
_CustomContainer({Key key}) : super(key: key);
@override
__CustomContainerState createState() {
state = __CustomContainerState();
return state;
}
__CustomContainerState state;
void changeColor() {
if (state == null) return;
// call state's function
this.state.changeColor();
}
}
class __CustomContainerState extends State<_CustomContainer> {
var bgColor = Colors.red;
@override
Widget build(BuildContext context) {
return Container(
width: 200,
height: 200,
color: bgColor,
);
}
void changeColor() {
setState(() {
bgColor = Colors.blue;
});
}
}
usage:
final redContainer = _CustomContainer();
final button = FlatButton(
onPressed: () {
// call widget function
redContainer.changeColor();
},
child: Text('change color'),
);
It works, but I wonder is there any hidden danger?
Upvotes: 4
Views: 4189
Reputation: 6158
Long time gone and it's time to share my knowledge.
GlobalKey
is very useful in this case.
You can get the state
of a Widget
via it's GlobalKey
.
For example there is a RedContainer
with a method changeWidth
:
class RedContainer extends StatefulWidget {
const RedContainer({Key? key}) : super(key: key);
@override
RedContainerState createState() => RedContainerState();
}
class RedContainerState extends State<RedContainer> {
double _width = 100;
@override
Widget build(BuildContext context) {
return Container(
width: _width,
height: 100,
color: Colors.red,
);
}
void changeWidth() {
setState(() {
_width = 200;
});
}
}
Then there is a page with this RedContainer
:
class GlobalKeyDemoPage extends StatelessWidget {
GlobalKeyDemoPage({Key? key}) : super(key: key);
final _redKey = GlobalKey<RedContainerState>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('global key')),
body: SingleChildScrollView(
child: Column(
children: [
RedContainer(key: _redKey),
ElevatedButton(
onPressed: () {
_redKey.currentState?.changeWidth();
},
child: Text('change RedContainer width'),
),
],
),
),
);
}
}
As you see I pass a GlobalKey
to RedContainer
then I can get it's state
very easily:
_redKey.currentState;
And call it's public method:
_redKey.currentState?.changeWidth();
Upvotes: 4
Reputation: 28010
You'll notice it's very awkward to manipulate Flutter widgets in an imperative fashion, like in the question. This is because of the declarative approach Flutter has taken to building screens.
The approach / philosophy of Flutter UI is a declarative UI vs. an imperative UI.
The example in the question above leans toward an imperative approach.
A declarative approach:
Below I've tried to convert the imperative approach above, into a declarative one.
CustomContainer
is declared with a color
; state known / kept outsideCustomContainer
& used in its construction.
After construction, you cannot impose a color change on CustomContainer
. In an imperative framework you would expose a method, changeColor(color)
and call that method and the framework would do magic to show a new color.
In Flutter, to change color of CustomContainer
, you declare CustomContainer
with a new color.
import 'package:flutter/material.dart';
/// UI object holds no modifiable state.
/// It configures itself once based on a declared color.
/// If color needs to change, pass a new color for rebuild
class CustomContainer extends StatelessWidget {
final Color color;
CustomContainer(this.color);
@override
Widget build(BuildContext context) {
return Container(
color: color,
child: Text('this is a colored Container'),
);
}
}
/// A StatefulWidget / screen to hold state above your UI object
class DeclarativePage extends StatefulWidget {
@override
_DeclarativePageState createState() => _DeclarativePageState();
}
class _DeclarativePageState extends State<DeclarativePage> {
var blue = Colors.blueAccent.withOpacity(.3);
var red = Colors.redAccent.withOpacity(.3);
Color color;
// state (e.g. color) is held in a context above your UI object
@override
void initState() {
super.initState();
color = blue;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Declarative Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomContainer(color),
// color "state" ↑ is passed in to build/rebuild your UI object
RaisedButton(
child: Text('Swap Color'),
onPressed: () {
setState(() {
toggleColor();
});
}
)
],
),
),
);
}
void toggleColor() {
color = color == blue ? red : blue;
}
}
Read more on declarative vs imperative on Flutter.dev.
Performance-wise it seems wasteful to rebuild the entire screen when a single widget, way down in the widget tree needs rebuilding.
When possible (and sensible) it's better to wrap the particular elements that have state & need rebuilding in a StatefulWidget, rather than wrapping your entire page in a StatefulWidget and rebuilding everything. (Likely, this wouldn't even be a problem, I'll discuss further below.)
Below I've modified the above example, moving the StatefulWidget from being the entire DeclarativePage, to a ChangeWrapper widget.
ChangeWrapper will wrap the CustomContainer (which changes color).
DeclarativePage is now a StatelessWidget and won't be rebuilt when toggling color of CustomContainer.
import 'package:flutter/material.dart';
class ChangeWrapper extends StatefulWidget {
@override
_ChangeWrapperState createState() => _ChangeWrapperState();
}
class _ChangeWrapperState extends State<ChangeWrapper> {
final blue = Colors.blueAccent.withOpacity(.3);
final red = Colors.redAccent.withOpacity(.3);
Color _color; // this is state that changes
@override
void initState() {
super.initState();
_color = blue;
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomContainer(_color),
RaisedButton(
child: Text('Swap Color'),
onPressed: () {
setState(() {
toggleColor();
});
}
)
],
);
}
void toggleColor() {
_color = _color == blue ? red : blue;
}
}
/// UI object holds no state, it configures itself once based on input (color).
/// If color needs to change, pass a new color for rebuild
class CustomContainer extends StatelessWidget {
final Color color;
CustomContainer(this.color);
@override
Widget build(BuildContext context) {
return Container(
color: color,
child: Text('this is a colored Container'),
);
}
}
class DeclarativePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print('Declarative Page re/built');
return Scaffold(
appBar: AppBar(
title: Text('Declarative Page'),
),
body: Center(
child: ChangeWrapper(),
),
);
}
}
When running this version of the code, only the ChangeWrapper widget is rebuilt when swapping colors via button press. You can watch the console output for "Declarative Page re/built" which is written to debug console only once upon first screen build/view.
If DeclarativePage was huge with hundreds of widgets, isolating widget rebuilds in the above manner could be significant or useful. On small screens like in the first example at top or even or average screens with a couple dozen widgets, the difference in savings are likely negligible.
Flutter was designed to operate at 60 frames per second. If your screen can build / rebuild all widgets within 16 milliseconds (1000 milliseconds / 60 frames = 16.67 ms per frame), the user will not see any jankiness.
When you use animations, those are designed to run at 60 frames (ticks) per second. i.e. the widgets in your animation will be rebuilt 60 times each second the animation runs.
This is normal Flutter operation for which it was designed & built. So when you're considering whether your widget architecture could be optimized it's useful to think about its context or how that group of widgets will be used. If the widget group isn't in an animation, built once per screen render or once per human button tap... optimization is likely not a big concern.
A large group of widgets within an animation... likely a good candidate to consider optimization & performance.
Btw, this video series is a good overview of the Flutter architecture. I'm guessing Flutter has a bunch of hidden optimizations such as Element re-use when a Widget hasn't materially/substantially changed, in order to save on CPU cycles instantiating, rendering & positioning Element objects.
Upvotes: 8