LayoutBuilder 最佳化
LayoutBuilder 與 SliverLayoutBuilder 現在會更少次地呼叫 builder 函式。
摘要
#本指南說明在 LayoutBuilder 最佳化 後,如何遷移 Flutter 應用程式。
背景說明
#LayoutBuilder 與 SliverLayoutBuilder 為了讓應用程式能根據父層版面配置約束(layout constraints)動態調整元件(Widget)結構,會頻繁地呼叫 builder 函式,次數超過實際需求。 這導致元件被不必要地重建,使應用程式效率降低並產生卡頓現象。
這個問題也會間接影響到 OrientationBuilder。
為了提升應用程式效能,進行了 LayoutBuilder 最佳化,使得 builder
函式的呼叫次數減少。
如果應用程式依賴該函式以特定頻率被呼叫,則可能會發生錯誤。 應用程式可能會出現以下一種或多種現象:
- 在升級到包含此最佳化的 Flutter 版本後,
builder函式不再像以前一樣被呼叫。 - 某個元件(Widget)的 UI 消失。
- 某個元件(Widget)的 UI 沒有更新。
變更說明
#在最佳化之前,傳遞給 LayoutBuilder 或 SliverLayoutBuilder 的 builder 函式會在下列任一情況發生時被呼叫:
-
由於元件設定變更,
LayoutBuilder被重建 (通常發生於使用LayoutBuilder的元件因setState、didUpdateWidget或didChangeDependencies觸發重建時)。 LayoutBuilder在進行版面配置時,從父元件獲得了與上次不同的版面配置約束(layout constraints)。LayoutBuilder在進行版面配置時,從父元件獲得了與上次相同的版面配置約束。
經過最佳化後,builder 函式在第三種情況下將不再被呼叫。若約束條件相同且元件設定未變,則 builder 函式不會被呼叫。
如果你的應用程式依賴於重新版面配置(relayout)來觸發 LayoutBuilder 的重建,而不是明確呼叫 setState,則可能會發生錯誤。這通常是無意間發生的。你原本應該加入
setState,但因為應用程式仍能正常運作,導致你忘記加入。
遷移指南
#
請檢查 LayoutBuilder 與 SliverLayoutBuilder 的使用情境,並確保每當元件狀態改變時,都有呼叫 setState。
範例:在下方範例中,builder 函式的內容取決於 _counter 欄位的值。因此,每當該值被更新時,應呼叫 setState
以通知框架重建元件。不過,即使沒有呼叫 setState,如果 _ResizingBox 觸發了 LayoutBuilder
的重新版面配置,此範例過去仍可能正常運作。
遷移前的程式碼(請注意 onPressed 回呼(callback)中缺少 setState):
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,
),
],
);
}
}
遷移後的程式碼(setState 已新增至 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,
),
],
);
}
}
請留意在同一個元件(Widget)中同時使用 Animation 和 LayoutBuilder 的情況。
動畫(Animation)具有內部可變狀態,並會在每一幀發生變化。如果你的 builder 函式邏輯依賴於動畫的值,則可能需要讓 setState
隨著動畫同步更新。為此,請加入一個動畫監聽器(animation listener),並在其中呼叫
setState,如下所示:
Animation animation = … create animation …;
animation.addListener(() {
setState(() {
// Intentionally empty. The state is inside the animation object.
});
});
時程
#此變更已於 Flutter v1.20.0 版本釋出。
參考資料
#API 文件:
相關議題:
相關 PR:
Unless stated otherwise, the documentation on this site reflects Flutter 3.44.0. Page last updated on 2026-06-14. View source or report an issue.