flutter_hooksのコードを確認する記事を書きました。
記事においては、極力HookElement
とHookState
のコードに集中しているつもりです。好みや好みではないなどは、こっちのブログに分けて書こうかなと。
ということで、flutter_hooksについてどう思っているかのブログです。IMO。
結論
flutter_hooksを採用するべき理由を見つけていません。 以下、感想の箇条書きです。
- flutter_hooksは解決する問題に対して実装が過剰だと思っています
- hookが処理の共通化を目的にする以上、hookでなければできないことが現状見当たりません
- hookを(プロジェクトを跨いで)共有するコミュニティ的な活動がありません
- hookはflutter_hooksのメソッドを利用するか、プロジェクト内でメソッドを共有するぐらいの活用になっています
- initとdispose処理をまとめるだけであれば、mixinで処理をまとめることもできます
HookWidget
かStatefulWidget
のどちらを使うべきか、のレビューや議論を避けたいですHookElement
の処理にも(小さくとも)コストがあり、妥当かどうかの検討を避けたいです (useState
も同様です)HookState
の実装にミスがないかのチェックが必要ですが、妥当性のチェックがState
のものより難しいです
flutter_hooks
前提から。
筆者はflutter_hooksが導入されたプロジェクトを触っています。最大限に活用できていたかはわかりませんが、一通りのuse
メソッドの利用方法は把握しているつもりです。独自にuse
を使ったhookを書いたこともあります。flutter_hooksを依存関係に追加し、自作のライブラリからhookを提供しようかと考えたことはありますが、提供したことはありません。
次のIssueもざっと目を通しています。
hooksの導入については、Riverpodのドキュメントにページがあります。
flutter_hooksの良い点
HookElement
のuse
を成立させるための実装は、一度読んでおく価値があると思います。特にHookElement
をmixin
として定義することで、StatelessWidget
とStatefulWidget
、さらにConsumerWidget
系まで対応している面は、設計かくあるべしという印象です。
また、Element(ComponentElement
)に_hooks
を持たせることで、状態の保持をさせている点も見事だと思います。StatelessElement
とStatefulElement
の差を確認すれば明らかではありますが、3rd partyのライブラリで活用してるのは珍しいのではないかなと。結果、アプリの基盤として利用できる状態になっています。
最後に、一部のケースで(flutterが提供するような)mixin
よりも考慮することが少なくなります。useAnimationController
はTickerProviderを引数に取ることもできますが、null
とすることもできます。
null
とした場合にはuseSingleTickerProviderが呼び出されます。実装を見比べてみると、Flutterが提供するTickerProviderStateMixin
と同一の実装です。複数回useAnimationController
を呼び出したとしても、StateがSingleTickerProviderStateMixinとTickerProviderStateMixinのどちらをwith
するべきかを考える必要がありません。*1
flutter_hooksがあわないところ
他方、筆者にとってしっくりこない点です。
useContext
とmarkMayNeedRebuild
実装を追っていく中で「この実装は好みじゃないなぁ」となるのが、useContext
とmarkMayNeedRebuild
です。理由としては、
の2点です。どちらも、HookElement
がhookのベースとなることを考えると、許容せざるを得ません。この点については、次の理由とまとめて意見します。
コミュニティでhookを共有できていない
なぜuseContext
のようなhookがあるかと言えば、use
により処理(ロジック)を再利用可能な形で切り出すためです。
例えば、上のIssueでは次のhookが共有されています。
bool useIsDarkTheme() { final context = useContext(); final theme = Theme.of(context); return theme.brightness == Brightness.dark; }
hookは(共有可能な)処理をまとめ上げるものです。hookの利用が活発な場合、特定のユースケースで利用できる処理がhookとして共有されたり、ライブラリからhookが提供されるようになります。
筆者の理解では、現状、この環境はあまり広まっていません。次のリンクは、hookを共有するために「flutter_hooksを依存元に持つ」パッケージのlike数順一覧です。
例えば、graphql_flutterはuseQuery
のように、Reactと近いAPIを提供しています。これは、Reactの知識をFlutterに持ち込みたい、というモチベーションでしょう。
また、detectable_text_fieldはuseDetectableTextEditingController
を提供しています。こちらはflutter_hooksが提供するように、カスタマイズされたTextEdigintControllerをhookとして利用できるようになっています。
ただ、こういったhookの提供が一般的なプラクティスになっているかと言えば、そうは思えません。use
メソッドがflutter_hooksから提供されているため、どうしても限定的な提供になっています。
また、先述のuseIsDarkTheme
はDartの成長に伴い、拡張関数として定義できるようになりました。use
が公式に提供されていない以上、依存パッケージを増やさずに実装できる手法が、より手軽に感じられます。
extension BuildContextExt on BuildContext { bool get isDarkMode => Theme.of(this).brightness == Brightness.dark; }
なお、この議論は「社内向けのhookライブラリ」を開発している場合には、その限りではありません。HookState
を十分に理解しているリードがいる環境であれば、十分にワークすると思われます。
HookElement
のコスト
flutter_hooksは、実行時のコストが(少しですが)増します。
特にuseContext
のためにstatic領域にElementを保持したり、build処理を抑制するためにif文の判定を行うなど、仕組み上避けられない処理があります。また、HookWidget
は良いのですが、HookStatefulWidget
は(ほぼ)同じ目的の処理を2回行うことになります。DartとFlutterの処理が高速なため、フレーム遅れなどの問題は(まず)起こしませんが、本来不要な処理を走らせるのは避けたいものです。
hookをどのように実装するべきか、運用するべきかという議論も必要となります。hookの文脈はFlutterから離れています。このためFlutterにfunctional componentsやserver componentsの議論が存在せず、Reactの文脈を学びFlutterに転用しなければなりません。また、hookの実装を評価したり、処理コストに気を配る必要も生じます。
useState
useState
によるValueNotifier
は、先述の課題感を全て含むため、一例とします。
useState
の処理を追うと、値の変更時にsetState
を実行しています。よって、「setState
を開発者が呼び出さなくていい」ツールがuseState
、と筆者は考えています。
これは「必要な値 + ValueNotifier
を生成する」仕組みであり「setState
を呼び出すべきかどうかを、都度呼び出す側に倒す」仕組みです。開発者的には楽ではありますが、あえて避けなければならないほどの不便なのかな? と感じます。
また、build
メソッド内でValueNotifier
のアクセスが完結する場合には問題ないのですが、子Widgetの引数やcallbackで処理する対象になると、設計上の検討が生じます。
筆者は「ValueNotifier
を子Widgetに引き渡すよりも、callbackで処理し、値へアクセスするコードを絞りたい」と考える派です。他方、「ValueNotifier
を子Widgetに引き渡し、簡単に書きたい」と判断する派の開発者もいます。こういった見解の相違は、callbackをfunctionに切り出すかどうかの議論においても、埋める必要があるでしょう。
ValueNotifier
を利用しないのであれば、これらの議論を行う必要はありません。useState
の導入により、考慮事柄が増えてしまう、と筆者は感じています。
HookConsumerWidget
とConsumerHookStatefulWidget
影響は軽微ではありますが、議論が難しい問題としてHookConsumerWidget
があります。以下、説明です。
riverpodを採用すると、Providier/Notifierを参照するためにConsumer系のWidgetを利用します。重要なのは、ConsumerStatefulWidgetとConsumerWidget、そしてConsumerの継承関係です。
実装を見ると、StatefulWidget
→ ConsumerStatefulWidget
→ ConsumerWidget
→ Consumer
となっています。このためhooks_riverpodのHookConsumerWidgetとStatefulHookConsumerWidgetは、どちらもStatefulWidget
の継承クラスとなります。*2
これを踏まえると、hookを導入した場合に[StatelessWidget
]と[StatefulWidget
, HookWidget
,StatefulHookWidget
, [ConsumerStatefulWidget
, ConsumerWidget
, Consumer
, [HookConsumerWidget
, ConsumerHookStatefulWidget
]]]をどのように使い分けるか、を検討する必要が生じます。この議論がややこしいからと、HookConsuemrWidget
を使うことと決めるのはアリなのですが、Stateful + HookなWidgetを各所で利用することに、ちょっとしたむず痒さを筆者は覚えます。
flutter_hooksを利用しないならば、[StatelessWidget
]と[StatefulWidget
, [ConsumerStatefulWidget
, ConsumerWidget
, Consumer
]]の使い分けと考えることができます。議論やレビュー時の観点が整理されるため、筆者としては、こちらの方がシンプルな印象です。
まとめ
hooksのドキュメントにあるように、筆者はhooksを使いたいからflutter_hooksを入れるべきだと考えています。
本記事は、筆者にとってhooksはメリットよりデメリットの方が優っているように感じられることを述べているもので、hooksを否定するものではありません。