対象者
- Flutterを使用してUIのパフォーマンスを最適化したい開発者
- アニメーションやスクロールのスムーズさを向上させたいエンジニア
- RepaintBoundaryの効果的な使用方法を理解し、適切に適用したい方
はじめに
FlutterでUIを構築する際、パフォーマンスの最適化は重要な要素です。特にアニメーションや頻繁に更新されるウィジェットを含む複雑なUIでは、不要な再描画によるパフォーマンスの低下が顕著になります。
Flutterの描画システムでは、ウィジェットツリーに変更があると、そのウィジェットと子孫ウィジェットが再描画されます。しかし、一部のウィジェットの変更だけでもツリー全体が再描画されると、無駄な処理が発生し、パフォーマンスに影響を与えます。
この問題を解決するためにFlutterには RepaintBoundary というウィジェットが用意されています。本記事では RepaintBoundary の仕組みと活用方法について解説します。
RepaintBoundaryとは
RepaintBoundary は、その子ウィジェットとそのサブツリーを、親ウィジェットの更新による不要な再描画から分離するウィジェットです。
仕組み
RepaintBoundaryで囲まれたウィジェットは 独立したレイヤーとして扱われる。- 内部のウィジェットが更新されても 外部には影響しない。
- 外部が更新されても 内部のウィジェットは再描画されない。
- 再描画範囲を限定することでパフォーマンス向上 が期待できる。
RepaintBoundaryのユースケース
RepaintBoundary は以下のような場合に特に効果的です。
1. 複雑なアニメーションの最適化
アニメーションを含むウィジェットを RepaintBoundary で囲むことで、アニメーションによる再描画を分離し、他のウィジェットへの影響を最小限にできます。
2. スクロールパフォーマンスの向上
ListView や GridView などのスクロール可能なリストでは、スクロール中に多くのウィジェットが再描画されます。RepaintBoundary を使用することで、リスト全体の不要な再描画を防ぐことができます。
3. インタラクティブなウィジェットの最適化
スライダーやボタン、チェックボックスなどの頻繁に更新されるウィジェットは RepaintBoundary で囲むことで、周囲のUIへの影響を減らせます。
4. 頻繁に更新されるウィジェット
時計やプログレスバーのような頻繁に更新されるウィジェットに RepaintBoundary を適用すると、再描画の負荷を軽減できます。
RepaintBoundaryの注意点
1. 過剰な使用は避ける
RepaintBoundary は、新しいレイヤーを作成するため、メモリ使用量が増加します。必要以上に使用すると、逆にパフォーマンスが低下する可能性があります。
2. パフォーマンスオーバーレイで確認
Flutterの Performance Overlay を有効にすることで、RepaintBoundary が適切に機能しているかどうかを可視化できます。
3. 適切な場所に配置
頻繁に更新されるウィジェットやアニメーションを含むウィジェットに RepaintBoundary を配置すると、再描画の負荷を最適化できます。
4. デバッグフラグ debugRepaintRainbowEnabled を活用
再描画の範囲を可視化するには、debugRepaintRainbowEnabled を true に設定します。
void main() {
debugRepaintRainbowEnabled = true;
runApp(MyApp());
}
ベストプラクティス
RepaintBoundaryの使用は控えめに
RepaintBoundary自体が若干のオーバーヘッドを追加するため、多用すると逆効果になる可能性があります。- 常にパフォーマンスオーバーレイと再描画レインボーを使用して、不要な再描画が発生している箇所を特定しましょう。
静的または独立したセクションをラップ
- 頻繁に再描画する必要のないUIの部分を
RepaintBoundaryでラップすると効果的です。
RepaintBoundaryのコストに注意
RepaintBoundaryはパフォーマンスを向上させる一方で、新たなレイヤーを作成するため、CPUとメモリのオーバーヘッドが発生します。- 複雑なアニメーションや頻繁に変更されるウィジェットの分離 など、明確なメリットがある場所に限定して使用しましょう。
- 過剰に使用すると、境界が蓄積され、パフォーマンスの利点が相殺される 可能性があります。
Performance Overlayなどのツールを活用し、影響を監視・テストすることを推奨します。
RepaintBoundaryの実装例
このコードでは、RepaintBoundary を使用することで アニメーションのパフォーマンスをどのように最適化できるか を確認できます。
具体的には、以下の点を検証できます:
RepaintBoundaryなし の場合、アニメーションの影響が親ウィジェット (Scaffold) に及び、不要な再描画が発生すること。RepaintBoundaryあり の場合、アニメーションが個別のレイヤーで処理され、親ウィジェットの不要な再描画を防ぐことができること。debugRepaintRainbowEnabledを利用し、どの部分が再描画されているのかを視覚的に確認 できること。
この実験を通じて、Flutter における RepaintBoundary の実際の効果と適用すべき場面を理解できる ようになります。
RepaintBoundary の適用箇所
このコードでは RollingDash (回転するアイコン) に RepaintBoundary を適用しています。
final isAvailable = _counter % 2 == 1;
return isAvailable
? RepaintBoundary(
child: RollingDash(
angele: angle,
),
)
: RollingDash(angele: angle);
_counterの値によってRepaintBoundaryを適用するかどうかを切り替えるisAvailable == trueのときはRepaintBoundaryでRollingDashを囲むisAvailable == falseのときはRollingDashをそのまま描画
目的:
RepaintBoundaryを適用した場合としなかった場合の違いを確認できるようにするRepaintBoundaryを適用することで、親 (Scaffold) の再描画を防ぐことができる
RollingDash のアニメーション**
class RollingDash extends StatelessWidget {
const RollingDash({
super.key,
required this.angele,
});
final double angele;
@override
Widget build(BuildContext context) {
return Transform.rotate(
angle: angele,
child: Icon(
Icons.flutter_dash,
size: 128,
),
);
}
}
_controller.valueに基づいてIcons.flutter_dashを回転Transform.rotate(angle: angele)を使用し、angeleの値に応じて回転角度を変更
ポイント:
RollingDashはAnimatedBuilderのbuilder内で回転する- この回転が原因で再描画が発生する
- そのため、
RepaintBoundaryでラップすることで、影響範囲を限定できる
debugRepaintRainbowEnabled を活用
このコードでは、Flutter の 再描画デバッグ機能 を利用しています。
void main() {
debugRepaintRainbowEnabled = true;
runApp(const MyApp());
}
- 再描画されるウィジェットを可視化 できる
RepaintBoundaryの効果を検証可能RollingDashのアニメーション中に、 どの部分が再描画されているかが色付きで表示される
デバッグ方法
debugRepaintRainbowEnabled = true;を有効化RepaintBoundaryのchildにRollingDashを設定isAvailableを切り替えて、 再描画範囲の違い を確認
RepaintBoundary の効果を検証**
RepaintBoundary を適用しない場合
RollingDashのアニメーションにより、 親ウィジェット (Scaffold) まで再描画される- 不要な再描画が発生し、フレームレートが低下する可能性あり
RepaintBoundary を適用した場合
RollingDashのアニメーションが 個別のレイヤーで処理される- 親 (
Scaffold) や他の UI 部分には影響を与えない - パフォーマンスが向上し、スムーズなアニメーションを実現
まとめ
RepaintBoundary は、Flutterアプリのパフォーマンス最適化に役立つ強力なツールです。ただし、過剰な使用はメモリ消費を増やすため、適切な場所に適用することが重要です。パフォーマンスオーバーレイを使用して最適な適用箇所を特定し、よりスムーズなUIを実現しましょう!
参考
- youtube.com – RepaintBoundary (Widget of the Week)
- api.flutter.dev – RepaintBoundary class – widgets library – Dart API
- 10 Flutter Widgets Probably Haven’t Heard Of (But Should Be Using!)
- The Magic of Repaint Boundary!
ソース(main.dartにコピペして動作確認用)
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'package:flutter/rendering.dart';
void main() {
debugRepaintRainbowEnabled = true;
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
showPerformanceOverlay: true,
checkerboardRasterCacheImages: true,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
late final _controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 1000,
))
..repeat();
@override
Widget build(BuildContext context) {
final isAvailable = _counter % 2 == 1;
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
final angle = _controller.value * 2 * math.pi;
return Center(
child: isAvailable
? RepaintBoundary(
child: RollingDash(
angele: angle,
))
: RollingDash(angele: angle),
);
}),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: const Icon(Icons.add),
),
);
}
}
class RollingDash extends StatelessWidget {
const RollingDash({
super.key,
required this.angele,
});
final double angele;
@override
Widget build(BuildContext context) {
return Transform.rotate(
angle: angele,
child: Icon(
Icons.flutter_dash,
size: 128,
),
);
}
}
-
Next
Flutter開発 with Devin