constでFlutterのパフォーマンスを改善する
昨年入社したアプリ開発担当の W です。よろしくお願いします。エンジニアブログの 5 記事目を任されました。よ〜し、ぼくがんばっちゃうぞ〜!
弊社では、Flutter を使ってアプリケーション開発を行っています。アプリケーション開発において、パフォーマンスは重要な要素です。Flutter アプリのパフォーマンスを改善する手法は様々なものがありますが、今回はその中でも const を用いた余計なリビルドの防止 についてご紹介します。
constの基本
Dart には const というキーワードがあります。const はコンパイル時定数を示すためのキーワードで、値を変えようとするコードを書くとコンパイルエラーになります。似たようなキーワードに final がありますが、こちらは変数の再代入を禁止するキーワードであって、代入した値を変化させるコードはコンパイルできます。
また、const をクラスのコンストラクタに指定し、利用するときにも const を指定することで、クラスのインスタンスもコンパイル時定数とすることができます。その場合、以下のコードの通り、コンストラクタに同じ値を渡す限り、同一のインスタンスとなります。
var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);
assert(identical(a, b)); // They are the same instance!
ここで Flutter の話に戻りますが、const を使うことで余計なリビルド(buildメソッドの再実行)を防ぐことが出来ます。良い例として flutter create で生成したときに作られる MyHomePage Widget があります。
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
setState によって build メソッドが呼ばれると、AppBar, Text, Center, Column 等、const のついていない Widget のインスタンスが全て作り直され、別のインスタンスとなります。カウンターを表示している Text も、引数が定数でないため、const がついておらず、やはり作り直されます。
しかし、const のついている Text, Icon に関しては、インスタンスがコンパイル時に確定する定数となるので、作り直されません。インスタンスが作り直されないので、リビルドも行われません。
このように、const をつけることで、余計なリビルドを抑える事ができます。
Dart のコンストラクタの new, const
Dart ではコンストラクタを呼び出す際の new, const を省略できます。詳しくは ドキュメント をご確認ください。
new の省略
Dart では基本的に new を書くことはありません。
var p1 = Point(2, 2);
var p2 = Point.fromJson({'x': 1, 'y': 2});
// 上記は以下と同じ
var p1 = new Point(2, 2);
var p2 = new Point.fromJson({'x': 1, 'y': 2});
const の省略
文脈上 const であることが自明な場合は const を省略できます。
// const がたくさんあるが、 pointAndLine が const なので、文脈上内側のものは全て const のはず
const pointAndLine = const {
'point': const [const ImmutablePoint(0, 0)],
'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
};
// 下記のように省略可能
const pointAndLine = {
'point': [ImmutablePoint(0, 0)],
'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};
const を積極的に使うコツ
メリットの大きい const ですが、積極的につけようとすると、いくつかの工夫が必要です。
linter の活用
flutter_lints を導入し、適切な設定を行うことで、 const が付与可能な場所を警告してくれるようになります。付け忘れを防いでくれるので是非設定しておきましょう。
Container を避ける
Container は残念ながら const コンストラクタを持っていません。そのため、const コンストラクタを持つ他の Widget に置き換えた方がパフォーマンス面では良いことが多いです。例えば Widget の大きさを固定したい時は SizedBox が使用できますし、余白を持たせたい時は Padding を使用できます。いずれも const コンストラクタを持っています。
// BAD
Container(height: 20.0),
Container(
padding: const EdgeInsets.only(top: 8.0),
child: const HogeWidget(),
);
// GOOD
const SizedBox(height: 20.0),
const Padding(
padding: EdgeInsets.only(top: 8.0),
child: HogeWidget(),
);
とはいえ、Container を使えば複数のレイアウトを 1 つのウィジェットで実現でき、インデントも深くならないという利点があるので、避けるのが正しいとは限らないと思います。
Riverpod を使う
以下は flutter create を少し書き換えたコードですが、 _counter が const でないため、それを引数で受け取る CountWidget は const が付けられず、同様に Center にも const が付けられません。
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CountWidget(count: _counter),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
class CountWidget extends StatelessWidget {
const CountWidget({Key? key, this.count}) : super(key: key);
final int count;
@override
Widget build(BuildContext context) {
return Text(
'$count',
style: Theme.of(context).textTheme.headline4,
);
}
}
このように、コンストラクタの引数を使って変化する値をやり取りする場合、const を使えるケースは限定的になります。
Flutter では InheritedWidget を使い、コンストラクタ引数を使わずに変化する値を子 Widget まで届けているケースがあります。Theme の仕組み などがそれにあたります。これを利用すれば、親に const をつけつつ、変化する値に応じて表示する内容を変化させることができます。
InheritedWidget を使いやすくした Provider と、更に発展させた Riverpod というライブラリがあり、私の担当するアプリでは Riverpod を採用しています。Riverpod を使うと、上記のコードを以下のように改善できます。
final counterProvider = StateNotifierProvider((ref) {
return Counter();
});
class Counter extends StateNotifier<int> {
Counter() : super(0);
void increment() => state++;
}
class MyHomePage extends ConsumerWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: const Center(
child: CountWidget(),
),
floatingActionButton: FloatingActionButton(
onPressed: ref.read(counterProvider.notifier).increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
class CountWidget extends ConsumerWidget {
const CountWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Text(
'${ref.watch(counterProvider)}',
style: Theme.of(context).textTheme.headline4,
);
}
}
抜粋 ↓
body: const Center(
child: CountWidget(),
),
Center に const をつけることが出来ました。この例は Widget ツリーが非常に浅いため、パフォーマンスにはほとんど影響しないと思われますが、もっと深いツリーの場合は効果的なことがあります。
終わりに
const 付与は割と手軽に負荷を減らすことが出来るので積極的に行っていきたいですね。勿論、パフォーマンスチューニングの手法は const 付与だけでなく、様々な方面から行っていく必要があります。上記で紹介しきれなかったもの含めて、様々な事を先輩達から教えてもらい開発を行っています。私も日々理解を深めながら開発をしています。