LayoutBuilder optimization
Summary
#This guide explains how to migrate Flutter applications after the LayoutBuilder optimization.
Context
#LayoutBuilder and SliverLayoutBuilder call the builder function more often than necessary to fulfill their primary goal of allowing apps to adapt their widget structure to parent layout constraints. This has led to less efficient and jankier applications because widgets are rebuilt unnecessarily.
This transitively affects OrientationBuilder as well.
In order to improve app performance
the LayoutBuilder optimization was made,
which results in calling the builder
function less often.
Apps that rely on this function to be called with a certain frequency may break. The app may exhibit some combination of the following symptoms:
- The
builder
function is not called when it would before the upgrade to the Flutter version that introduced the optimization. - The UI of a widget is missing.
- The UI of a widget is not updating.
Description of change
#Prior to the optimization the builder function passed to LayoutBuilder
or
SliverLayoutBuilder
was called when any one of the following happened:
LayoutBuilder
is rebuilt due to a widget configuration change (this typically happens when the widget that usesLayoutBuilder
rebuilds due tosetState
,didUpdateWidget
ordidChangeDependencies
).LayoutBuilder
is laid out and receives layout constraints from its parent that are different from the last received constraints.LayoutBuilder
is laid out and receives layout constraints from its parent that are the same as the constraints received last time.
After the optimization the builder function is no longer called in the latter case. If the constraints are the same and the widget configuration did not change, the builder function is not called.
Your app can break if it relies on the relayout to cause the rebuilding of the
LayoutBuilder
rather than on an explicit call to setState
. This usually
happens by accident. You meant to add setState
, but you forgot because the app
continued functioning as you wanted, and therefore nothing reminded you to add
it.
Migration guide
#Look for usages of LayoutBuilder
and SliverLayoutBuilder
and make sure to
call setState
any time the widget state changes.
Example: in the example below the contents of the builder function depend
on the value of the _counter
field. Therefore, whenever the value is updated,
you should call setState
to tell the framework to rebuild the widget. However,
this example may have previously worked even without calling setState
, if the
_ResizingBox
triggers a relayout of LayoutBuilder
.
Code before migration (note the missing setState
inside the onPressed
callback):
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(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: Counter(),
);
}
}
class Counter extends StatefulWidget {
Counter({Key key}) : super(key: key);
@override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return Center(child: Container(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return _ResizingBox(
TextButton(
onPressed: () {
_counter++;
},
child: Text('Increment Counter')),
Text(_counter.toString()),
);
},
),
));
}
}
class _ResizingBox extends StatefulWidget {
_ResizingBox(this.child1, this.child2);
final Widget child1;
final Widget child2;
@override
State<StatefulWidget> createState() => _ResizingBoxState();
}
class _ResizingBoxState extends State<_ResizingBox>
with SingleTickerProviderStateMixin {
Animation animation;
@override
void initState() {
super.initState();
animation = AnimationController(
vsync: this,
duration: const Duration(minutes: 1),
)
..forward()
..addListener(() {
setState(() {});
});
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 100 + animation.value * 100,
child: widget.child1,
),
SizedBox(
width: 100 + animation.value * 100,
child: widget.child2,
),
],
);
}
}
Code after migration (setState
added to onPressed
):
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(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: Counter(),
);
}
}
class Counter extends StatefulWidget {
Counter({Key key}) : super(key: key);
@override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return Center(child: Container(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return _ResizingBox(
TextButton(
onPressed: () {
setState(() {
_counter++;
});
},
child: Text('Increment Counter')),
Text(_counter.toString()),
);
},
),
));
}
}
class _ResizingBox extends StatefulWidget {
_ResizingBox(this.child1, this.child2);
final Widget child1;
final Widget child2;
@override
State<StatefulWidget> createState() => _ResizingBoxState();
}
class _ResizingBoxState extends State<_ResizingBox>
with SingleTickerProviderStateMixin {
Animation animation;
@override
void initState() {
super.initState();
animation = AnimationController(
vsync: this,
duration: const Duration(minutes: 1),
)
..forward()
..addListener(() {
setState(() {});
});
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 100 + animation.value * 100,
child: widget.child1,
),
SizedBox(
width: 100 + animation.value * 100,
child: widget.child2,
),
],
);
}
}
Watch for usages of Animation
and LayoutBuilder
in the same widget.
Animations have internal mutable state that changes on every frame. If the
logic of your builder function depends on the value of the animation, it may
require a setState
to update in tandem with the animation. To do that, add an
animation listener that calls setState
, like so:
Animation animation = … create animation …;
animation.addListener(() {
setState(() {
// Intentionally empty. The state is inside the animation object.
});
});
Timeline
#This change was released in Flutter v1.20.0.
References
#API documentation:
Relevant issue:
Relevant PR:
除非另有说明,本文档之所提及适用于 Flutter 的最新稳定版本,本页面最后更新时间: 2024-04-04。 查看文档源码 或者 为本页面内容提出建议。