Flutter初心者にflutter_hooksは必要ないかもしれない
2024年5月頃から社内でモバイルアプリを開発するチームが立ち上がりました。
チーム全員がReactには慣れている一方でほとんどのメンバーがFlutter未経験という状態で、以下のような期待からflutter_hooksの導入を決めました。
- Widgetからロジックを切り出せるため、Widgetの見通しが良くなる・コードの記述量が減るのではないか
- 慣れているReactHooksのように書けるため、Flutterの学習コストを下げることができるのではないか
riverpod・graphql_flutterなど他に採用したライブラリのドキュメントにflutter_hooksと統合するための章が設けられており、メジャーで十分成熟したライブラリだと判断できたのも大きかったです。
flutter_hooksを導入してから約1年が経ったため、本記事で当時の判断を振り返ってみることにします。
結論
以下の理由から、導入する必要がなかったと考えています。
- ロジックの再利用というメリットを上手く活かせなかった
- flutter_hooksの動作の理解が必要なぶん学習コストが増加した
- 複数の実装パターンが存在し、実装・レビューのコストが増加した
flutter_hooks導入で得られるもの
そもそもflutter_hooksを導入する目的はなんでしょうか。flutter_hooksのドキュメントでは「increase the code-sharing between widgets by removing duplicates.(重複を排除することで、ウィジェット間のコード共有を増やすためである。)」と述べられています。
StatefulWidgetにはinitState()
やdispose()
などのライフサイクルメソッド内に記述したロジックを再利用するのが難しいという大きな問題があり、プロジェクトの複数箇所で同じロジックを使用したい場合何度も繰り返し書く必要があるよね、という主張です。
これについてドキュメントのAnimationControllerを使用したコード例がわかりやすいので見てみます。
StatefulWidgetの実装例
こちらはStatefulWidgetで実装した例です。他のウィジェットでもAnimationControllerを使用したい場合、確かにinitState()
・didUpdateWidget()
・dispose()
それぞれにcontrollerの状態を変化させる同じロジックをもう一度書く必要がありそうです。
class Example extends StatefulWidget {
const Example({ super.key, required this.duration });
final Duration duration;
State<Example> createState() => _ExampleState();
}
class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
late AnimationController _controller;
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: widget.duration,
);
}
void didUpdateWidget(Example oldWidget) {
super.didUpdateWidget(oldWidget);
_controller.duration = widget.duration;
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Container();
}
}
flutter_hooksの実装例
こちらはflutter_hooksが提供するuseAnimationController
を使用した例です。
controllerの状態を更新するロジックはフックに分離され、Widgetの見通しが良くなっています。
class Example extends HookWidget {
const Example({super.key, required this.duration});
final Duration duration;
Widget build(BuildContext context) {
final controller = useAnimationController(duration: duration);
return Container();
}
}
useAnimationController()
の実装を見るとHookStateクラスにほぼ同様のロジックが実装されており、StatefulWidgetのcontrollerの状態を更新するロジックが綺麗に分離できていることがわかります。
これはflutter_hooks組み込みのフックを利用してロジックの再利用・自前で実装するコード量を減らせており、flutter_hooksのメリットを享受できているわかりやすい例だと思います。
運用していく上で気になったところ
ロジックの再利用というメリットを上手く活かせなかった
flutter_hooksの主な目的はロジックの共通化・再利用です。この「再利用」にはプロジェクト内での再利用・Flutterコミュニティ内での再利用の2つの側面があると考えています。
汎用的なフック
自分たちのプロジェクトではドメインロジックに依存せず複数のWidgetから呼び出される汎用的なカスタムフックをいくつか実装しました。しかし、これを実装するのであれば先人が作ってすでに十分テストがされている品質の高いカスタムフックを使用したいと感じてしまいました。
フックの仕組みがライブラリに同梱されているReactと比較して、flutter_hooksに依存しないとフックを提供できないFlutterではコミュニティ内での再利用はそこまで活発ではないように思えます。flutter_useなどが有名どころなのかと思いますがこちらもあまり直近ではメンテナンスされてないように見えており、結局汎用フックは自前で実装することが多かったです。
アプリの特定の用途に特化したフック
特定の用途に特化したカスタムフックを作る場合はそのフックを他のウィジェットで再利用することは少なく、1年運用した今でもロジックを切り出して再利用できていると言えるケースは片手に収まるほどでした。自分たちのプロジェクトではWidgetからロジックを切り出した場合、Widgetと隣接したディレクトリに配置することが多かったです。
lib
├─ hooks
│ └─ use_mount.dart <= 汎用的で再利用可能だが、よくあるやつなので自前で実装せずに出来合いのものを使いたい
└─ widgets
└─ movie_player
├─ movie_player.dart
└─ use_movie_player_controller.dart <= 結局movie_playerでしか使用しないため再利用しない
flutter_hooksの動作の理解が必要なぶん学習コストが増加した
flutter_hooksはFlutterの仕組みの上に構築されているため、Flutterの学習コストは下がらず、むしろHookの動作原理を理解する必要がある分コストが増加します。また、React経験者にとっては理解しやすいですが、React未経験者はフック名から大体どんな動作をするフックなのかを判断できるというメリットも享受できないためさらに学習コストが増加する気がします。
Reactとはそもそもの仕組みに違いがありReact同様に書くことはできないため、Reactとの違いを理解する必要があります。自分たちのプロジェクトでも誤った使用方法をしてしまっていたフックの一例としてuseCallback
があります。flutter_hooksではuseCallback
が実装されていますが、React.memo
と対応する概念がFlutterには存在しません。そのため関数を単にuseCallback
でラップしても再描画を防止する効果は得られず、渡し先のWidgetをuseMemoized
でラップする必要があるようです。
StatefulWidgetと比較してコードサンプルが少ない
flutter_hooksはメジャーなライブラリではありますが、コードサンプルはFlutterのデフォルトであるStatefulWidgetで実装されているものが多いです。
それらを自分のプロジェクトに導入するときにはフックで実現する場合どう書き換えるかは自分で考える必要がありますし、フックと組み合わせることで発生したわかりづらい挙動は自分で解決していく必要があります。
(あくまで体感ですが、Agentによる補完も頼りなく「それReactやん」というコード・フックを使用したいのにStatefulWidgetでの実装例を提案されることも多いです)
複数の実装パターンが存在し、実装・レビューのコストが増加した
flutter_hooksでは、同じ機能を実現するために複数の実装方法が存在します。
- StatefulWidget
- HookWidget(関数ベースのフック)
- HookWidget(クラスベースのフック)
私たちのプロジェクトでは開発者がどの方法で実装するか判断する・またそれについて議論するコストとプロジェクト内に異なる実装パターンが混在することを嫌い「ライフサイクルの管理が必要なウィジェットは全てHookWidget + クラス形式のフックで実装する」というルールを決めました。このルールにより実装パターンの統一は実現できましたが、クラス形式のフックではコードの記述量が減らないという新たな課題が生じました。
「破棄されるまでの時間を計測する」という機能を持ったWidgetで実装例を見てみます。
StatefulWidgetの実装例
class TimeAliveWidget extends StatefulWidget {
const TimeAliveWidget({super.key});
State<TimeAliveWidget> createState() => _TimeAliveState();
}
class _TimeAliveState extends State<TimeAlive> {
DateTime? start;
void initState() {
super.initState();
start = DateTime.now();
}
Widget build(BuildContext context) {
return const SizedBox();
}
void dispose() {
if (start != null) {
print(DateTime.now().difference(start!));
}
super.dispose();
}
}
flutter_hooks(クラスベース)の実装例
フック形式で実装した例です。
見てわかる通りStatefulWidgetのStateの実装とHookStateクラスの実装はそっくりになっており、記述量は減っていません。むしろフック利用のための作法の記述でコードの長さ自体は長くなります。
class TimeAliveWidget extends HookWidget {
const TimeAliveWidget({super.key});
Widget build(BuildContext context) {
useTimeAlive();
return const SizedBox();
}
}
void useTimeAlive() {
use(const _TimeAlive());
}
class _TimeAlive extends Hook<void> {
const _TimeAlive();
_TimeAliveState createState() => _TimeAliveState();
}
class _TimeAliveState extends HookState<void, _TimeAlive> {
DateTime start;
void initHook() {
super.initHook();
start = DateTime.now();
}
void build(BuildContext context) {}
void dispose() {
print(DateTime.now().difference(start));
super.dispose();
}
}
まとめ
flutter_hooksはロジックの再利用を促進する優れたライブラリですが、Reactの経験があるからといって考えなしに採用した方が良いライブラリではないというのが現時点での筆者の結論です。
使い方を間違えなければライフサイクルメソッドの自前実装を無くしてコードの記述量を減らしWidgetをスッキリ保つことができますが、この「使い方を間違えなければ」には相応の学習コストが伴います。
Flutterの経験がない場合まずは基本に忠実にStatefulWidgetによる実装から初めて、flutter_hooksのモチベーションであるような苦しみを実際に感じた場合に採用していくのが良かったかなと感じています。
Discussion