Flutterでウィジェットのライフサイクルを管理するFlutter Hooksの概要と基本的なフックの使い方についてまとめました。
- Flutter Hooksとは?
- useState: 状態を持つWidgetを簡単に書ける
- useEffect: 初期化・破棄の処理を簡単に書ける
- useMemoized: 重い処理をキャッシュしてビルド時に走らないようにできる
- useValueChanged: 値が変化した時だけ任意の処理を走らせる
- useRef: 値が変化しても再ビルドしない
- useContext: どこからでもBuildContextを取得できる
- その他のフックについて
- 参考
Flutter Hook 0.20.5
Flutter Hooksとは?
Flutter HooksはFlutterのWidgetのライフサイクルを管理するためのライブラリです。
その名の通り、ライフサイクルを管理するための「フック」がいくつも用意されています。
さて、ライフサイクル管理というと抽象的ですが、具体的には以下のようなメリットが得られるフックが用意されています。
- 状態を持つWidgetを簡単に書ける
useState
フック - Widgetの初期化、破棄の処理を簡単にかける
useEffect
フック - 重い処理をキャッシュしてビルド時に走らないようにできる
useMemoized
フック - 値が変化した時だけ任意の処理を走らせられる
useValueChanged
フック - 値が変化しても再ビルドしない
useRef
フック - buildContextをどこからでも取得できる
buildContext
フック
本記事ではこれら基本的なフックの使い方についてまとめます。
なおインストールについては詳しくは説明しませんが、他のパッケージと同じように以下で行えます。
flutter pub add flutter_hooks
useState: 状態を持つWidgetを簡単に書ける
さてそれではまず、useState フックを使ってみます、
これを使うと、本来 Stateful Widget が必要な状態を持つWidgetを簡潔に書くことができます。
例として、下図のようにタップすると数値が1ずつ増加する簡単なアプリを作ります。
これをStateful Widgetを使って実装すると、以下のようになります。
import 'package:flutter/material.dart'; void main() { runApp(const MaterialApp(home: HomePage())); } // Stateful Widgetを使った場合の実装 class HomePage extends StatefulWidget { const HomePage({super.key}); @override HomePageState createState() => HomePageState(); } class HomePageState extends State<HomePage> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return GestureDetector( onTap: _incrementCounter, child: Text('$_counter'), ); } }
これに対して、useState を使うと以下のように実装できます。
import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; void main() { runApp(const MaterialApp(home: HomePage())); } // Flutter HooksのuseStateを使った実装 class HomePage extends HookWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { // useStateを使ってint型の変数を作成・購読 final counter = useState(0); return GestureDetector( onTap: () => counter.value++, // 値を変えると自動的にHomePageがリビルドされる child: Text(counter.value.toString()), ); } }
Stateを定義することなく、状態を持つWidgetを簡単に作れました。
useEffect: 初期化・破棄の処理を簡単に書ける
次に初期化・破棄の処理を簡単に書ける useEffect フックを使ってみます。
以下は、前節のコードに初期化と破棄の処理を加えたものです。
import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; void main() { runApp(const MaterialApp(home: HomePage())); } class HomePage extends HookWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { print('build'); final counter = useState(0); // useEffectフックで初期化・破棄の処理を書く useEffect(() { // Widgetがビルドされたときの処理をここに書く print('build by useEffect'); return () { // Widgetが破棄されるとき処理を返す(不要な場合はnull) print('dispose by useEffect'); }; }); return GestureDetector( onTap: () => counter.value++, child: Text(counter.value.toString()), ); } }
HomePage Widgetがビルドされた時と破棄される時の処理の処理を記述できました。
実行すると、タップするたびに useState による Widget のビルドが行われ、その結果として破棄と初期化の処理が繰り返し呼ばれることを確認できます。
また、useEffect
の第二引数を渡すと、「useEffectが呼び出された時にこの値のいずれかが変わっていた時のみ処理を行う」という挙動になります。
以下の例では、counter
に加えてcounter2
という状態を定義し、それぞれが変化したらWidget自体は再ビルドされるが、useEffect
の処理がよばれるのはcounter2
の値が変化した時のみになります。
import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; void main() { runApp(const MaterialApp(home: HomePage())); } class HomePage extends HookWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { print('build'); final counter = useState(0); final counter2 = useState(0); // counter2を追加 useEffect(() { print('build by useEffect'); return () { print('dispose by useEffect'); }; }, [counter2.value]); // counter2の値が変わっているときだけ処理 return GestureDetector( onTapDown: (_) => counter.value++, // 押下したらcounterをインクリメント onTapUp: (_) => counter2.value++, // 離上したらcounter2をインクリメント child: Text(counter.value.toString()), ); } }
これを利用して、第二引数に空の配列を渡すと(その値は変化することがないので)Widgetのライフサイクルに関わらず、最初の一回だけ初期化処理を行うということもできます。
import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; void main() { runApp(const MaterialApp(home: HomePage())); } class HomePage extends HookWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { print('build'); final counter = useState(0); useEffect(() { print('build by useEffect'); return () { print('dispose by useEffect'); }; }, []); // 第二引数に空の配列を渡す return GestureDetector( onTap: () => counter.value++, child: Text(counter.value.toString()), ); } }
useMemoized: 重い処理をキャッシュしてビルド時に走らないようにできる
useMemoized
を使うと任意のインスタンスをキャッシュし、Widget が再ビルドされても同じインスタンスを使い回すことができます。
import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; void main() { runApp(const MaterialApp(home: HomePage())); } class HomePage extends HookWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { print('build'); final counter = useState(0); // timeをキャッシュするので再ビルドされても毎回同じ値が表示される final time = useMemoized(() => DateTime.now()); print(time); return GestureDetector( onTap: () => counter.value++, child: Text(counter.value.toString()), ); } }
第二引数を与えると、その値が変わっていたらインスタンスを作り直すことができます。
import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; void main() { runApp(const MaterialApp(home: HomePage())); } class HomePage extends HookWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { print('build'); final counter = useState(0); // 第二引数の値が変わっていたらインスタンスを作り直す // = この例では毎回作り直される final time = useMemoized(() => DateTime.now(), [counter.value]); print(time); return GestureDetector( onTap: () => counter.value++, child: Text(counter.value.toString()), ); } }
useValueChanged: 値が変化した時だけ任意の処理を走らせる
useValueChanged
フックを使うと値が変化した時だけ任意の処理を走らせることができます。
import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; void main() { runApp(const MaterialApp(home: HomePage())); } class HomePage extends HookWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { print('build'); final counter = useState(0); // 値が変化した時の処理 useValueChanged( counter.value, (oldValue, _) => print('time has changed: $oldValue -> ${counter.value}'), ); return GestureDetector( onTap: () => counter.value++, child: Text(counter.value.toString()), ); } }
useRef: 値が変化しても再ビルドしない
値が変化しても再ビルドを走らせたくない場合には useRef
フックを使います。
import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; void main() { runApp(const MaterialApp(home: HomePage())); } class HomePage extends HookWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { print('build'); final counter = useRef(0); // 値が変化しても再ビルドが走らない return GestureDetector( onTap: () { counter.value++; print('counter.value: ${counter.value}'); // 今の値を出力 }, child: Text(counter.value.toString()), ); } }
これを実行して画面をタップすると、値はインクリメントされて結果がコンソールに出力されるのに対して、Widgetの再ビルドは行われないためGUIとして描画されている数値は変化しないことが確認できます。
useContext: どこからでもBuildContextを取得できる
useContext
を使うと、BuildContext
の参照を引き回さなくても取得できるようになります。
import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; void main() { runApp( MaterialApp( theme: ThemeData(primaryColor: Colors.blue), home: const HomePage(), ), ); } class HomePage extends HookWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { print('build'); final counter = useState(0); return GestureDetector( onTap: () { counter.value++; print('counter.value: ${counter.value}'); }, child: _createText(counter.value.toString()), ); } Text _createText(String value) { // useContextを使うといちいちContextを引き回さなくても取得できる final context = useContext(); return Text( value, style: TextStyle(color: Theme.of(context).primaryColor), ); } }
その他のフックについて
上記で紹介したフックの他に、例えば以下のようにAnimationController
の生成と破棄の処理を行うフックなど、特定の用途のためのフックが多数用意されています。
Widget build(BuildContext context) { // AnimationControllerを取得できる(生成と破棄はフックがやってくれる) final controller = useAnimationController(); final controller2 = useAnimationController(); return ... }
本記事ではこれらについては解説しませんので、必要に応じて公式ドキュメントを参照してください。
また、自作のカスタムフックを自由に作ることもできます。
これらを用意することで、異なる Widget 間でフックを使い回して実装の共通化を行うことができます。