- 投稿日:2021-01-09T18:49:13+09:00
【Flutter】 無駄なリビルドを防ぐたった1つの方法
Flutter では StatelessWidget や StatefulWidget(以下、Widget) の
build()
が頻繁に呼ばれる(リビルドされる)ことを、 【Flutter】build() でやってはいけない 3 つのこと では説明しました。
build()
が頻繁に呼ばれること自体は Flutter フレームワークとして想定通りの動作ですので、ここに「変な」処理を書かない限り リビルドが原因でパフォーマンスが低下するようなことは通常ありませんが、アプリの規模が大きくなり Widget の入れ子(以下「Widget ツリー」と呼びます)が深くなってくると少しずつ「リビルド範囲が大きすぎる」ためにパフォーマンスが落ちてくる場合があるのも事実です。この記事では、 UI の変化が必要ない Widget の無駄なリビルドを防いでパフォーマンスを改善する にはどうすれば良いのかを理解するために、 Flutter フレームワークがリビルドを行わない条件を確認しつつ、その条件を満たすための実装について考えていきたいと思います。
インスタンスが変わらなければリビルドは発生しない
まず Flutter フレームワークが Widget をリビルドしない条件ですが、それは リビルド対象の Widget のインスタンスがリビルド前後で同一である 場合です。
通常以下のように
SomeWidget
のbuild()
の中でOtherWidget
を生成している場合、class SomeWidget extends StatelessWidget { @override Widget build(BuildContext context) { return OtherWidget(); } }
_SomeWidgetState
のbuild()
が呼び出されるとOtherWidget
のbuild()
もそれに続けて呼び出されます。 もしOtherWidget
のbuild()
の中でさらに別の Widget が生成されている場合はその Widget のbuild()
も呼び出され、以下同様に Widget ツリーの末端に到達するまでbuild()
が連続で呼ばれます。これを リビルドの伝播 と呼んだりします。何も考えずに上記のようにコーディングすると、
OtherWidget
のインスタンスは_SomeWidgetState
のbuild()
が呼び出されるたびに新しく生成されるため、前回のbuild()
で生成したインスタンスとは別のものになってしまいます。しかし、ここにひと工夫入れて 何度
build()
が呼ばれたとしても同じOtherWidget
インスタンスを使い回す ことで、 Flutter フレームワークに対してそのインスタンスをリビルドする必要がない、と判断させることができます。1Widget のインスタンスを使い回すための 3 つのテクニック
では、インスタンスが変わらなければリビルドが発生しないことを、以下のサンプルアプリで確かめていきたいと思います。
このアプリでは、 + アイコンをタップすると横の数字がカウントアップします。その上には固定の文言が表示されています。
ソースコードは以下のようになっています。(レイアウトを整えるためのコードは省略しています)
labeled_counter.dart/// カウントアップでリビルドが発生する StatefulWidget class LabeledCounter extends StatefulWidget { @override _LabeledCounterState createState() => _LabeledCounterState(); } class _LabeledCounterState extends State<LabeledCounter> { var _counter = 0; @override Widget build(BuildContext context) { return Column( children: [ SomeFixedWidget(), // _LabeledCounterState の build() が呼ばれてもリビルドさせないようにしたい Row( children: [ Text('$_counter', style: TextStyle(fontSize: 32)), IconButton( icon: Icon(Icons.add), onPressed: () => setState(() { _counter++; }), ), ], ), ], ); } } /// リビルドを発生させたくない Widget class SomeFixedWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Text('I don\'t want to rebuild this widget.'); } }通常このように記述すると、先ほど説明した通り
_LabeledCounterState
のbuild()
が呼び出されたタイミングでSomeFixedWidget
のインスタンスも新しく生成され、SomeFixedWidget
のbuild()
が続けて呼び出されてしまいます。しかし今回のアプリでは、
SomeFixedWidget
の内容は状態(_counter
の値)によって変化しないため、無駄なリビルドが発生しないように修正していきたいと思います。
SomeFixedWidget
のインスタンスが_LabeledCounterState
のリビルドごとに変わらなければSomeFixedWidget
のリビルドが発生しないのは先述した通りですが、ここからはそれを実現するための具体的な方法として以下の 3 つを見ていきたいと思います。const を使う
まず一番取り入れやすいのが
const
をつけてインスタンスを生成する方法です。
const
は Dart の文法の1つで、 コンパイル時に 1 つだけインスタンスを生成し、何度同じ処理が実行されても 1 つのインスタンスを使い回す という仕組みです。つまり、
const
を付けなかった場合、サンプルアプリのSomeFixedWidget()
はこの処理が呼び出されるたびに毎回違うインスタンスを生成しますが、const
をつけた場合、const SomeFixedWidget()
は何度この処理が呼び出されたとしてもコンパイル時に生成された SomeFixedWidget インスタンスを再利用します。2
const
をつけるためには、インスタンスを生成されるSomeFixedWidget
クラスにconst
コンストラクタを用意しておく必要があります。some_fixed_widget.dart/// リビルドを発生させたくない Widget class SomeFixedWidget extends StatelessWidget { // const コンストラクタを用意 const SomeFixedWidget(); ..以下略..
const
コンストラクタが用意できたら、あとはSomeFixedWidget
のインスタンスを生成するコードにconst
をつけるだけです。 先ほどのサンプルアプリのコードは以下のように修正できます。/// const を使ってリビルドを回避するパターンのサンプル class UseConstPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('const を使う例')), body: LabeledCounter(), ); } } /// リビルドを発生させたくない Widget class SomeFixedWidget extends StatelessWidget { // const コンストラクタを用意する const SomeFixedWidget(); @override Widget build(BuildContext context) { return Text('I don\'t want to rebuild this widget.'); } } /// カウントアップでリビルドが発生する StatefulWidget class LabeledCounter extends StatefulWidget { @override _LabeledCounterState createState() => _LabeledCounterState(); } class _LabeledCounterState extends State<LabeledCounter> { var _counter = 0; @override Widget build(BuildContext context) { return Column( children: [ const SomeFixedWidget(), // const をつけてインスタンスを生成する Row( ..省略.. ), ], ); } }この対策は
Text
やPadding
など、 Flutter が用意する Widget でも、その Widget にconst
コンストラクタが定義されている限り同様に使えます。Lint でコードをチェックすると「
const
がついていない」旨の警告が出る場合がありますが、それはこの例のようにconst
コンストラクタを利用するだけで無駄なリビルドを防げる ためです。(単純にインスタンスの生成コストを抑えるのが理由の場合もあります)State にキャッシュする
別の方法として、
生成した Widget インスタンスを State にキャッシュする
という方法も考えられます。これは
StatefulWidget
のドキュメントにも記載されているパフォーマンス改善の工夫で、以下のように記載されています。If a subtree does not change, cache the widget that represents that subtree and re-use it each time it can be used. It is massively more efficient for a widget to be re-used than for a new (but identically-configured) widget to be created.
訳) サブツリーが変化しない場合、そのサブツリーを表す Widget をキャッシュして使い回してください。 Widget を使い回すのは新しい(でも内容は同一の) Widget を再生成するよりも効率的です。https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html
やり方は単純で、 State クラスのフィールドに Widget を保持する変数を宣言するのと同時にインスタンスを生成してしまうだけです。
class _LabeledCounterState extends State<LabeledCounter> { var _counter = 0; // State のフィールドで Widget のインスタンスを生成しておく final _widgetCache = SomeFixedWidget(); ..以下略..ポイントは
State
のフィールド でこれをやるということです。何度も説明している通り、 Widget は Flutter フレームワークによって何度もリビルドされ、インスタンスが再生成されます。つまり、 Widget のフィールドにキャッシュを置いたとしても、その Widget 自体が破棄&再生成がされてしまうため、キャッシュの意味がなくなってしまうのです。一方で State は Widget ツリーの構造に変化がない限り破棄されることはありませんので、 State にキャッシュしておくことでより長い間同じインスタンスを使い回せる、というわけです。これは
StatelessWidget
にはできない、StatefulWidget
だからこそ可能な工夫といえます。この工夫を取り入れたサンプルアプリのコードは以下のようになります。
use_cache_page.dart/// State でのキャッシュでリビルドを回避するパターンのサンプル class UseCachePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('State でキャッシュする例')), body: LabeledCounter(), ); } } /// カウントアップでリビルドが発生する [StatefulWidget] class LabeledCounter extends StatefulWidget { @override _LabeledCounterState createState() => _LabeledCounterState(); } class _LabeledCounterState extends State<LabeledCounter> { var _counter = 0; // State のフィールドで Widget のインスタンスを生成しておく final _widgetCache = SomeFixedWidget(); @override Widget build(BuildContext context) { // レイアウト関連の記述は省略しています return Column( children: [ _widgetCache, // _widgetCache を使い回す Row( ..省略.. ), ], ); } }もちろん、
const
コンストラクタが使える場合はconst
を優先して使った方が、たとえ State が破棄されたとしても同じインスタンスが使いまわされるため効率的です。記述も少なく不具合も起こりづらいです。何らかの理由で
const
が使えない場合(たとえば状況に応じてキャッシュする Widget の引数を変えたい場合など)はこの方法を検討してみてください。外から渡す
3 つめに説明するのが、「外から渡す」という方法です。今まではサンプルアプリの
LabeledCounter
内で完結する方法でしたが、この方法ではLabeledCounter
を使う クラスにも手を入れます。つまり、
LabeledCounter
クラスのコンストラクタに「使いまわしたい Widget」を受け取るための引数を用意し、それをフィールドに保持して使い回す、という方法です。この方法が他の 2 つと違う点は、
LabeledCounter
を使う側が自由に固定表示部分の Widget を指定できる、という点です。例えば今回のサンプルアプリの場合、
LabeledCounter
クラスを以下のように修正します。class LabeledCounter extends StatefulWidget { // リビルドさせたくない Widget を外から受け取る final Widget label; const LabeledCounter({Key key, this.label}) : super(key: key); ..以下略..そして、この
LabeledCounter
クラスを使う側で、以下のようにSomeFixedWidget
を生成して引数に渡します。/// LabeledCounter を利用する Widget class UseInjectionPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('外からインスタンスを渡す例')), body: LabeledCounter( label: SomeFixedWidget(), // ここで SomeFixedWidget インスタンスを生成して渡す ), ); } }こうすることで、 LabeledCounter を利用する
UseInjectionPage
がリビルドされない限りは(つまり + ボタンがタップされただけの場合は)SomeFixedWidget
インスタンスが再生成されることはないため、何度_LabeledCounterState
のbuild()
が呼ばれたとしてもインスタンスが変わることはなく、SomeFixedWidget
のリビルドも発生しない、というわけです。この方法を取り入れると、サンプルアプリは以下のように修正できます。
use_injection_page.dart/// 外から Widget のインスタンスを渡してリビルドを回避するパターンのサンプル class UseInjectionPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('外からインスタンスを渡す例')), body: LabeledCounter( label: SomeFixedWidget(), // ここで [SomeFixedWidget] インスタンスを生成して渡す ), ); } } /// カウントアップでリビルドが発生する [StatefulWidget] class LabeledCounter extends StatefulWidget { // リビルドさせたくない Widget を外から受け取る final Widget label; const LabeledCounter({Key key, this.label}) : super(key: key); @override _LabeledCounterState createState() => _LabeledCounterState(); } class _LabeledCounterState extends State<LabeledCounter> { var _counter = 0; @override Widget build(BuildContext context) { return Column( children: [ widget.label, // 外から受け取った Widget を使い回す Row( ..省略.. ), ], ); } }まとめ
リビルドが発生した(つまり
build()
が呼ばれた) Widget の中に StatelessWidget / StatefulWidget があるとき、それらも続けてリビルドされるかどうかは インスタンスが同一かどうか で決定します。そのため、親の Widget の状態の変化に応じて自身の UI を変化させる必要がないのであれば、この記事で説明したような方法を使って同一のインスタンスを使い回すことで、無駄なリビルドを防ぐことが可能です。
とはいえ、無駄なリビルドを防ぐためにこれらの工夫を「入れなければならない」かどうかは状況次第です。 Flutter はビルドを大量に行ってもパフォーマンスが落ちないように工夫されているフレームワークですので、わざわざこれらの工夫を入れて(記述を増やして)リビルド範囲を狭めようと努力するのは、アプリの規模が大きくなり Widget ツリーが深くなってリビルドごとに画面の更新がカクつくようになった時に検討する程度で良いと思います。(1つ目の
const
をつける方法はほぼデメリットなくさっと行えるので習慣化すると良いと思いますが)今回のサンプルアプリは以下のリポジトリに PUSH してあります。
chooyan-eng/prevent_rebuild_sample | GitHub
Flutter 自体のソースコードが読めるようになるとさらに詳しくこのあたりの仕組みがわかるかと思いますが、まずはとりあえずできることとして今回紹介したサンプルプロジェクトを上記の GitHub から落とすなり自分で書いてみるなりして実行し、手元でブレークポイントを張りながら動作確認してみると良いでしょう。この記事には書ききれなかった発見があると思います。
なぜそのような挙動になるのか、興味のある方はまず
Element
について理解した上で、 Flutter のソースコードの Element.updateChildren あたり の処理を追ってみると良いでしょう。 ↩当然、引数を変えてインスタンスを生成するようなことはできません。詳しくは Language tour | Dart を確認してください。 ↩
- 投稿日:2021-01-09T06:06:15+09:00
2021年、業務アプリ開発歴17年が初めてFlutter Firebase Dartを触りたくなった話
筆者のスペック:Delphi の人です。業務アプリたるやみたいなことを普段は考えています。
- ERP業務アプリケーション開発17年
- 言語:Java, JavaScript (Delphi, COBOL, C++, SQL?, HTML?, CSS?, 趣味では色々
- フレームワーク?:Spring, jQuery,
- 他:Windows, UNIX全般, LINUX, データベースは何でも
で3層構造を主に触って食ってきました。タイトルだけは今時風にしてみました。
以下 Web制作を諦めた初心者が半年でアプリをリリースした話(Flutter) を読んで全く単語が頭に入ってこなかったのと、頭から入りたい質として「あーん?こいつら信用できるん?」なんて、浮かんだ疑念を解き明かすために書きます。結論
- Flutter
- Firebase
- Dart
を使ってバックエンドはNginx/Django/PostgreSQL あたり、で問題なく(というより最適だ位に)モバイルアプリは作れそうだ、と理解しました。
冒頭から手のひらを反すような結論になります。
三連休に本気出したらFlutterでアプリを作成できるか検証してみた を見つけたからこの3連休で本気出すか...どうかは約束しない。以下、枯れた技術だけは知っているけれどイマドキの単語は全く、という方の、2021年のお供になればと思い書きます。
わからなかった単語1: Flutter
Flatter
https://ejje.weblio.jp/content/flatter
主な意味: おべっかを使う、おもねる、お世辞を言う
ではない。(本当に間違えた。)
Flutter
公式:https://flutter.dev/
Wikipedia: https://ja.wikipedia.org/wiki/FlutterFlutter (フラッター) は、Googleによって開発されたフリーかつオープンソースのモバイルアプリケーションフレームワークである。FlutterはAndroidやiOS向けのアプリケーションの開発に利用されている。
学生の個人開発にもFlutterがオススメな理由(リリース例あり) があるが
全てDartのみで書ける
Storyboard, XML, CSSを触る必要がありませんDartのみで書けるってどういう意味やねん。状態であった。
わからなかった単語2: Dart
Dart
公式:https://dart.dev/
Wikipedia: https://ja.wikipedia.org/wiki/DartDart(ダートもしくはダーツ。当初は Dash と呼ばれていた)はGoogleによって開発されたウェブ向けのプログラミング言語である。ウェブブラウザ組み込みのスクリプト言語であるJavaScriptの代替となることを目的に作られた。
Flutter入門のためのDart入門。
なるほど、FlutterとDartの組み合わせはどうやらテッパンらしい。わからなかった単語3: Firebase
Firebase
公式: https://firebase.google.com/
Wikipedia: https://ja.wikipedia.org/wiki/FirebaseFirebase(ファイアベース)は、2011年にFirebase, Inc.が開発したモバイル・Webアプリケーション開発プラットフォームで、その後2014年にGoogleに買収された。
Web制作を諦めた初心者が半年でアプリをリリースした話(Flutter)に戻ると「Flutter×Firebaseで作成しました」とあったわけだが、Firebase、これも分からなかった。
Firebaseで1時間で簡単なWebチャットアプリが作れるハンズオン
1時間で作れる、そんなに嬉しいの?というか拡張性、信頼性、あるの?わからなかった背景: 何でそんなフレームワーク達を使うの?
この疑問は端的には以下2つから来ている。
- そんな新しいフレームワーク信用できるの?
- えっ、また新しい言語覚えるのツライ...
疑問1. Flutterは信用できるのか
Flutterがいい理由 にある。
Androidの開発をJavaやkotlinの完全ネイティブで開発すると Activity、Fragment、Viewと複数のライフライクルを管理することになり、わかりづらくデバッグしづらいバグが発生します。
- Flutterは複雑なライフサイクルがラッピングされているためシンプル
- AndroidとiOSで共通のソースコードで実装できる。
- 標準のUIライブラリでAndroidとiOS両方対応している。
疑問2. 新しい言語覚えるのツライ
同記事で引用されている Android開発はFlutterでやる方がいい説 が私にはわかりやすかった。
良くも悪くもDart言語 であるということはすなわちDart言語の文法は、ほとんどJavaです。JavaScriptに近いかもしれませんが、Javaを知っている人の学習コストは限りなくゼロに近い
らしい。【翻訳記事】なぜFlutterにおいてDartを使用するのか? が詳しいので更に読むと良い。
結論:アーキテクチャ選定のガイドラインに則り
以上より Flutter, Firebase, Dart、それだけ話題になっていることがわかった。
使えるのではないかなとようやく理解できました。ガイドラインとは
技術選定/アーキテクチャ設計で後悔しないためのガイドライン という記事を昨年末読んだ。
- そもそもアーキテクチャ・技術選定に時間をかけるべきか。
- あなたの技術的モチベーションをどの程度考慮したほうがいいのか。
今回私が Flutter, Firebase, Dart に興味を持った動機は、「趣味でアプリ開発できるかな?」程度。上記を考えると「時間はかけないべき」だし「私のモチベーションだけを考慮すればいい」。しかし多少将来を考えると、少しでもほかの誰かも触れたり、中身に興味を持てたり、あるいは日常業務(記事冒頭参照)に学びを還元できる要素があったほうがいい。
アーキテクチャ選定のガイドライン まとめ には以下のようにある。
最も大事なのは、そのような「失敗」をリカバリーできる体制やチーム、組織を作り上げることです。そのためには、その時考えたことや予測をしっかりと文書化し残していくことが大事になります。
そういう意味で当記事は「Flutter Firebase Dart」などという私にとっては初見のマイナー扱いだった技術たち。それらが、意外と強固で信頼性に足りそうだという記録として残したく書いたということになります。
補足:Dart言語の人気
2019年にわざわざ学ばなくてもいいプログラミング言語 で堂々1位。
疑問はStack Overflowで解決しながら淡々と利用されている、そういう言語であり、それを目指している。Kotlin: https://kotlinlang.org/ はそれとして。
KotlinとJavaができる人向けDart速習 なんてのもあるし、まあJavaがベースにあるヒトにとってはどちらでも、かじってみてもいいのかなという印象です。
一方で もしあなたが急にAndroidアプリを業務で作るはめになった場合の選択肢(2021年初頭版)。あ、業務で作るとなるとそれはまた別の話よね、とおもったので念のためここに追記。以上2021年、(比較的)おっさん向けのモバイルアプリ「完全に理解したい」ための記事でした。
その後のHello, My Flutter Worldの記録はここにメモった。