# Flutter Hooksまとめ:StatefulWidgetとの比較
コードを読んでいるとたまにHooksが登場しますね。従来のFlutter的な書き方とは味が違うというか、書き方が異なります。そのはず、Reactに似たものがあるらしく、設計思想や使い方が似ているみたいです。
Flutter Hooksを使うと、状態やリソース管理が圧倒的に楽で、コードもきれいになります。ライバルのStatefulWidgetではinitStateやdispose、setStateなどメソッドやクラスが必要ですが、Hooksなら1クラス・1ファイルで完結し、宣言的に書けます。
※この記事は参考ドキュメントの内容を自分なりにまとめたものです。
参考
- https://zenn.dev/flutteruniv/books/flutter-architecture/viewer/1-1_introduction
- flutter_hooks package
StatefulWidget | Hooks |
---|---|
initState() + dispose()
|
useEffect() 一箇所 |
setState() 必要 |
自動更新 |
2クラス必要 | 1クラスのみ |
メンバ変数で状態管理 |
useState() で状態管理 |
Hooksのメリットと制限
メリット
- 専用のuseState(useTextEditingController、useAnimationControllerなど)が用意されている
- 初期化・廃棄処理が自動化される
- build内で宣言的に書ける
- コードの可読性が向上する
制限
- build内でしか使えない
- Widgetのスコープ内でのみ有効
- 複数画面で共有する状態管理には向いていない
- フックの呼び出し順序が重要(条件分岐内での使用は避ける)
使い分け
- Hooks: 単一Widget内での状態・リソース管理
- Provider/Riverpod: 複数画面で共有する状態管理
Hooksの基本
Hooksには既存フック(useState、useEffect、useTextEditingController、useAnimationControllerなど)とカスタムフックがあります。TextEditingControllerやScrollController、FocusNodeなど初期化やdisposeが必要なものも、既存フックで簡単に管理できます。
final text1 = useTextEditingController();
final text2 = useTextEditingController();
useStateの変数や処理はbuild内でのみ記述でき、hooksの名前はuseではじめるのが推奨です。フックはHookWidgetのスコープごとにListに管理されます。
final a = useState(0); // 1番目のフック
final b = useAnimationController(); // 2番目のフック
final c = useTextEditingController(); // 3番目のフック
// hooks = [HookState<int>, HookState<AnimationController>, HookState<TextEditingController>]
重要: if文やswitch文などの条件処理でフックを呼ぶと、呼び出し順がズレてエラーになるので注意してください。
if (isAdmin) {
useTextEditingController();
}
// trueならhooks[0]はuseTextEditingController、falseなら未定義で参照時にエラー
useEffectとuseMemoizedの違い
useEffect
副作用(API呼び出しやリスナー登録など)を実行するためのフックです。依存リスト(第二引数)に指定した値が変わった時だけコールバックが実行されます。参照型(Listなど)は中身の変更ではなくインスタンス自体の変更でしか検知されません。
useEffect(() {
print('Hello');
return null;
}, [time]);
useMemoized
値のキャッシュやインスタンスの再利用に使います。依存リストの値が変わった時だけ再計算されます。
final controller = useMemoized(() => AnimationController(..., duration: duration), [duration]);
カスタムフックの作り方
関数型で作るのが一般的で、他のフックを内部で使ってもOKです。useから始める命名が推奨されます。
ValueNotifier<T> useLoggedState<T>([T? initialData]) {
final result = useState<T?>(initialData);
useValueChanged(result.value, (_, __) {
print(result.value);
});
return result;
}
フックが複雑になる場合はクラス型で作ることもでき、StateクラスのようにinitHook、dispose、setStateなどのライフサイクルメソッドにアクセスできます。
サンプル:StatefulWidgetとHookWidgetの比較
HookWidget版
class MyHomePage extends HookWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
final textController = useTextEditingController(text: '');
final displayText = useState('');
useEffect(() {
void listener() {
displayText.value = textController.text;
}
textController.addListener(listener);
return () => textController.removeListener(listener);
}, []);
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(displayText.value, style: Theme.of(context).textTheme.headlineMedium),
TextField(controller: textController),
],
),
),
);
}
}
StatefulWidget版
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String _displayText = '';
final _textController = TextEditingController();
@override
void initState() {
super.initState();
_textController.addListener(() {
setState(() {
_displayText = _textController.text;
});
});
}
@override
void dispose() {
_textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(_displayText, style: Theme.of(context).textTheme.headlineMedium),
TextField(controller: _textController),
],
),
),
);
}
}
まとめ
Flutter Hooksを使うことで、StatefulWidgetと比べて以下のメリットが得られます:
- コードの簡潔性: 1クラスで完結し、ボイラープレートコードが削減される
- 宣言的な書き方: build内で状態管理が完結する
- 自動リソース管理: 初期化・廃棄処理が自動化される
- 可読性の向上: 状態の流れが追いやすい
ただし、複数画面での状態共有にはProviderやRiverpodなどの状態管理ライブラリと組み合わせることをお勧めします。