はじめに
Flutterで開発をしていて、Widgetの構造が複雑になった際にある程度の意味のあるまとまりでWidgetを切り分けることがよくあると思います。宣言的UI自体、ネストが深くなりがちで1つのクラス内で複雑なUIをネストさせると可読性も下がってメンテナンスがしにくくなることが考えられます。
たとえば、TwitterのこのUIをFlutterでつくるとします。
皆さんだったらどのようにWidgetを切り分けて作っていくでしょうか?
そのときに、この枠で囲ったWidgetたちはそれぞれmethodとして分けるかStatelessWidgetとしてclassで分けるか、どちらで分ければいいのか。というのが今回の話です。
TL;DR
こちらの和訳のような記事です。基本的にはclassで分けるのがよさそうです。
本題
さきほどのUIを作るとき、意味のあるまとまりでWidgetを返すprivate methodとしてWidgetを切り分けるでしょうか?それともprivateなStatelessWidgetのclassとして切り分けるでしょうか?どちらでも同じUIを作ることができます。
それぞれざっくりWidgetを組んだ例を載せようと思います。
methodで分ける例
class TweetItem extends StatelessWidget {
const MyHomePage();
@override
Widget build(BuildContext context) {
return Row(
children: [
Image.network(''), // アイコン
_tweetContent(), // Tweet情報
],
);
}
}
Widget _tweetContent() {
return Column(
children: [
_header(), // 名前、id、日時
_body(), // 内容
_bottom(), // リプライアイコン、リツイートアイコン、...
],
);
}
Widget _header() {
return Container();
}
Widget _body() {
return Container();
}
Widget _bottom() {
return Container();
}
classで分ける例
class TweetItem extends StatelessWidget {
const MyHomePage();
@override
Widget build(BuildContext context) {
return Row(
children: [
Image.network(''), // アイコン
_TweetContent(), // Tweet情報
],
);
}
}
class _TweetContent extends StatelessWidget {
const _TweetContent({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
_Header(), // 名前、id、日時
_Body(), // 内容
_Bottom(), // リプライアイコン、リツイートアイコン、...
],
);
}
}
class _Header extends StatelessWidget {
const _Header({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container();
}
}
class _Body extends StatelessWidget {
const _Body({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container();
}
}
class _Bottom extends StatelessWidget {
const _Bottom({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container();
}
これらは見た目は同じになりますが、生成されるWidget Treeは異なります。これが重要な違いです。
methodで分けた場合
Row
Image
Column
Container
Container
Container
classで分けた場合
Row
Image
_TweetContent
Column
_Header
Container
_Body
Container
_Bottom
Container
こちらのstackoverflowでは、methodでWidgetを分けることでバグが発生したりパフォーマンスの最適化ができなかったりするということが書かれています。
methodを使えば必ずバグが発生するということは無いようですが、classを使えばこれらの問題の影響を受けない保証がされるみたいです。
いくつかのmethodとclassで分けるときに起こる挙動の違いの例が紹介されています。
- AnimatedSwitcherがうまく動かない例( https://dartpad.dev/1870e726d7e04699bc8f9d78ba71da35 )
- classで分けることでより細かい範囲でrebuildできてパフォーマンス向上できる例( https://dartpad.dev/a869b21a2ebd2466b876a5997c9cf3f1 )
- methodを使うことでBuildContextを誤って使うことにより、InheritedWidgetを使うときにバグを生む可能性があることを示した例( https://dartpad.dev/06842ae9e4b82fad917acb88da108eee )
まとめ
stackoverflowの回答ではそれぞれの場合で以下のメリットがまとめられています。
- classで分けたとき
- パフォーマンスの最適化が可能(constコンストラクタ、より細かい範囲でのrebuild)
- 異なるWidgetを切り替えるときmethodは以前の状態を再利用することがあるが、classにすることでリソースが正しく処理されるようになる
- ホットリロードが最適に動く
- devtoolでWidget TreeにClass Widgetが表示されるので画面に描画されているレイアウトがわかりやすくなる
-
debugFillProperties
をオーバーライドして、Widgetに渡されたパラメータを表示することができる -
ProviderNotFound
のようなエラーが発生した場合に、classで分けた場合は現在build中のWidget名を表示するが、methodでWidget Treeをbuildした場合、エラーに役立つ名前が表示されない - keyを指定できる
- context APIが使える
- methodで分けたとき
- ボイラープレートを減らしてコード量が減る
ミスリーディングしてはいけないのは、methodでわけることで問題が起こるというわけではなく、classを使えば起こり得る問題を解決できるということみたいです。
そもそも、Widgetを返すmethodで全て事足りるのであればStateless Widgetは存在しないですよね。
上記で書いたように、特にmethodでもclassでもどっちでもいい場合は最優先でclassでWidgetを分けるのがよさそうです。
methodで分けるときがどういうときがあるかというと、複数WidgetをColumn
やRow
で分けるのではなく、List<Widget>
で返したいときなどかなと思います。
あとは、Widgetを分けてバケツリレーさせるプロパティが多くなってきたとき少量ながらもclassで分けたときは新しくclass内でコンストラクタの引数に渡ってきた値を受け取るプロパティを宣言する必要があり、メモリ使用量が増えそうなのでprivate methodで定義するとかもあるっちゃありそうです。