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を否定するものではありません。