Flutter のウィジェットは const コンストラクタを定義していれば、 const 呼び出しでリビルドを抑制することができます。
class _MyWidgetState extends State<MyWidget> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// ステートが変わってもこの [Text] はリビルドされない
const Text('You have pushed the button this many times:'),
Text('$_count'),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: const Icon(Icons.add),
),
);
}
}
しかし、上記の例でいうと FloatingActionButton
は const にできません。なぜなら、関数を渡しているからです。関数オブジェクトは const にできません。
floatingActionButton: const FloatingActionButton(
onPressed: _increment, // Error: Invalid constant value.
child: Icon(Icons.add),
),
でも渡している関数がずっと同じであるなら、リビルドを抑制できないものかと思うかもしれません。そんなときは対象のウィジェットをキャッシュしておくという方法があります。
class _MyWidgetState extends State<MyWidget> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
// この [FloatingActionButton] は [_MyWidgetState] の初回ビルド時に
// 一度だけビルドされる
late final _fab = FloatingActionButton(
onPressed: _increment,
child: const Icon(Icons.add),
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text('$_count'),
],
),
),
floatingActionButton: _fab,
);
}
}
こうすると _count
の値が変更されて親ウィジェットがリビルドされても、FloatingActionButton
はリビルドされない、という挙動を実現することができます。
ただ、この例での FloatingActionButton
や _increment
関数が、親ウィジェットの props を参照している場合は注意が必要です。そのままだと props の値が変わっても古い値を参照し続けてしまうので、キャッシュを更新するひと手間が必要になります。具体的には、StatefulWidget
であれば didUpdateWidget
ライフサイクルメソッドでキャッシュの更新を行います。
class MyWidget extends StatefulWidget {
const MyWidget({super.key, required this.incrementAmount});
final int incrementAmount;
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
int _count = 0;
void _increment() {
setState(() {
_count += widget.incrementAmount;
});
}
Widget _buildFab() {
return FloatingActionButton(
onPressed: _increment,
child: const Icon(Icons.add),
);
}
late Widget _fab = _buildFab();
@override
void didUpdateWidget(MyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// props の値が変わっていたらキャッシュを更新する
if (widget.incrementAmount != oldWidget.incrementAmount) {
_fab = _buildFab();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text('$_count'),
],
),
),
floatingActionButton: _fab,
);
}
}
もし flutter_hooks
パッケージを使っていれば、useMemoized
や useCallback
でもっと簡潔に書くこともできます。
class MyWidget extends HookWidget {
const MyWidget({super.key, required this.incrementAmount});
final int incrementAmount;
@override
Widget build(BuildContext context) {
final count = useState(0);
final increment = useCallback(() {
count.value += incrementAmount;
}, [incrementAmount]);
final fab = useMemoized(() {
return FloatingActionButton(
onPressed: increment,
child: const Icon(Icons.add),
);
}, [increment]);
return Scaffold(
appBar: AppBar(
title: const Text('Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text('${count.value}'),
],
),
),
floatingActionButton: fab,
);
}
}
…とここまで書いてきましたが、このようなキャッシュが実際のパフォーマンスに影響するケースは限定的だと思います。ウィジェットが明らかに重い処理 (どんなものがあるかぱっと思い付きませんが) を走らせているとか、親ウィジェットが文字入力や時間カウント、アニメーションなどに応じて頻繁にリビルドされる時とかでしょうかね。
さんざん巷で言われていることですが、パフォーマンス改善をちゃんとやるならベンチマークやメトリクスの計測を忘れずに!