- 武田大輝
- 約 3,100 文字
- 2,800 View
目次
はじめに
Dart/Flutter連載 の1本目です。
Flutterでウィジェットを開発するとき、 Stateless Widget や Stateful Widget を継承したクラスを作成することが一般的だと思います。一方でクラスを定義せずとも、ウィジェットを返却するFunctionを定義することで同様のことが実現できるのでは? と考えたことはないでしょうか。
本記事では前者をClass Widge, 後者をFunctional Widgetと称して以下説明をしていきます。
簡単なサンプルを示してみましょう。
Class Widge
class SampleWidget extends StatelessWidget { |
Functional Widget
Widget sampleWidget() { |
恐らく多くの方がFunctional Widgetはあまり良くないと思っていると思いますが、その理由を明確に説明できるでしょうか。
本記事では、2つの違いや使い分けについて整理したいと思います。
TL;DR
Flutterが公式に公開している動画でも本件について触れられており、パフォーマンス最適化や予期せぬバグの回避、テスタビリティ(本記事では割愛しています)という観点で Class Widgeの利用が推奨されています。
2つの違い
冒頭のサンプルで記述したClass WidgeとFunctional Widgetをそれぞれ利用した場合、アプリケーションの見た目はどちらも変わりません。
2つの一番の違いは生成されるウィジェットツリーの構造です。それぞれのウィジェットツリーは次のようになります。
Class Widge
ParentWidget |
Functional Widget
ParentWidget |
ウィジェットツリーの構造は、Flutterがウィジェットをリビルドする際の挙動に影響します。
Functional WidgetはClass Widgeに比べて、パフォーマンスが最適化されない可能性があり、また予期せぬバグが発生する可能性が高まります。
以下、具体的に説明していきましょう。
具体例
リビルドの最適化
これは紹介した動画でも述べられている例になります。
下記のようにクリック時に状態を変更するようなボタンをFunctional Widgetとして切り出した場合を考えてみます。この場合、ボタンをクリックした場合には大元のウィジェット全体のリビルドが実行されてしまいます。
class BigUIElementState extends State<BigUIElement> { |
このボタンが変更する状態のスコープが限定的な場合(例えばいいねボタンの様にクリックによってボタン自身の色を変更するようなケース)は、Functional Widgetではなく Stateful Widget として切り出した方がリビルドの範囲を限定できるため、パフォーマンスの面で優れています。
class BigUIElementState extends State<BigUIElement> { |
ただこの例は、状態のスコープを最小化すべきというのが主要なポイントであって、Class WidgeとFunctional Widgetの本質的な違いの例としては少しズレているように筆者は感じてしまったので、もう1つリビルドの最適化に着目した例を示しましょう。
リビルドの最適化 その2
先ほどの例は切り出すウィジェットが状態を保持する前提でしたが、下記のように状態を持たないウィジェットの場合はどうでしょうか。Functional Widgetの場合はParentElementの状態が変わるたびにsampleWidget()が呼び出され、内部で返却しているウィジェットが都度再生成されることになります。
class ParentElementState extends State<ParentElement> { |
この場合も、Functional Widgetではなく Stateless ウィジェットとして切り出すことで、リビルドを最適化できます(const constructorが利用できることが前提となります)。。
下記のように Stateless Widget として切り出した場合はParentElementがリビルドされた場合でもSampleWidgetのリビルドは実行されません。
|
誤ったBuild Contextの参照
例えば Builder Widget を使用するようなコードにおいて、Build Contextの1つに任意の別の名前(ここでは innerContext)を指定すると、下層のウィジェットにて古いBuild Contextを参照できてしまいます。
class ParentWidget extends StatelessWidget { |
Class Widgeとして切り出すことでこのような予期せぬバグを防ぐことができます。
class ParentWidget extends StatelessWidget { |
ウィジェット Keyによるリビルド制御
少し無理やりな例ですが、下記のようにボタンクリックによって、四角のコンテナが円形にアニメーションする例を考えてみましょう。circle()メソッドとsquare()メソッドで返却されるウィジェットはどちらも Container Widgetであるため、RuntimeTypeが同じであり、アニメーションがうまく機能しません。
https://dartpad.dev/?id=ab9ef6401c4687811ea59f44adfa8ee7
class ParentWidgetState extends State<ParentWidget> { |
それぞれの Container Widget にKeyを指定すればうまく機能します。
ウィジェット Keyの詳細は割愛しますが、気になる方は下記の記事などを参考にすると良いでしょう。
https://qiita.com/kurun_pan/items/f91228cf5c793ec3f3cc
Widget square() { |
このような予期せぬ不具合もClass Widgeとして切り出しておけば ウィジェットのKeyを意識せずとも未然に防ぐことが可能です。
https://dartpad.dev/?id=a69d57ea09802753676a46efc8390d15
class ParentWidgetState extends State<ParentWidget> { |
使い分け
ここまでみてきた通り、基本的には原則Class Widgeを利用する形が良いでしょう。
ただしFunctional Widgetそれ自体が問題を引き起こすものではなく、リファクタを目的としたプライベートなFunctional Widgetであれば、Functional Widgetの方がスマートに記述できるシーンがあると考えています。
下記のように、ウィジェット自体が分岐によって切り替わるようなケースにおいて、buildメソッド内が肥大化しているため、Switchのロジックを切り出したくなったとします。
class ParentWidget extends StatelessWidget { |
このような場合は、Class Widgeではなく Functional Widgetの方がより簡潔にかつ分かりやすく記述できるのではないでしょうか。
Class Widge
class ParentWidget extends StatelessWidget { |
Functional Widget
class ParentWidget extends StatelessWidget { |
このようにSwitchや三項演算子などにより、既にClass Widgeとして定義されているウィジェットを返却するためのロジックのみを切り出したいような場合(言い換えればFunuctional ウィジェット自体が構造化されたウィジェットを定義せず、Privateな関数として広く再利用されないような場合)は Functional Widgetの利用を許容しても良い気がしています。
おわりに
原則Class Widgeの利用が推奨されるべきであり、開発時のルールとしてFunctional Widgetは禁止にして問題ないと思います。
ただ最後に記述したとおり、Functional Widgetを使いたくなるようなシーンがいくつかあるような気がしており、(筆者もうまく明文化ができていないですが)そのようなケースが他にもあればコメントいただけますと幸いです。