flutter_hooksを勉強するスレ

ReactのHooks
- クラスコンポーネントでは、状態管理やライフサイクルの機能がある
- 関数コンポーネントでは、状態管理やライフサイクルの機能がない
- フック(関数コンポーネント)を活用することにより、状態管理やライフサイクルの機能が使える
- クラスコンポーネントではなく、フックで書くのがベスト(by Reactの公式ドキュメント)
-
フックを用いた関数コンポーネントで書いていき、必要な場合のみクラスで作成
が今後のスタンダードかも?

StatehulWidgetの問題
class Example extends StatefulWidget {
final Duration duration;
const Example({Key key, required this.duration})
: super(key: key);
@override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
AnimationController? _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: widget.duration);
}
@override
void didUpdateWidget(Example oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.duration != oldWidget.duration) {
_controller!.duration = widget.duration;
}
}
@override
void dispose() {
_controller!.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container();
}
}
- コード量が肥大化
-
initState
やdispose
などが再利用しにくい
flutter_hooksの出番
class Example extends HookWidget {
const Example({Key key, required this.duration})
: super(key: key);
final Duration duration;
@override
Widget build(BuildContext context) {
final controller = useAnimationController(duration: duration);
return Container();
}
}
上記のStatefulWidget
と同じ動作をする。
- controllerの破棄や更新をする
-
useAnimationControllerが
AnimationController
のロジックを動作させている。 - フックは、特殊性を備えた新しいオブジェクト
-
build method
内でWidgetにMix-inの時、使う - 類似したフックでも再利用可能
- フックたは完全に独立している。
- フック間に依存関係がない
- Widgetに依存していない
-

原則
State
と同様に、フックもWidgetのElement
に格納
-
List<Hook>
というオブジェクトで管理される - Elementに格納されるStateオブジェクトは1つだけ
- Hookを使うには、
Hook.use
を使う
Hook.use
で返されるHookは、呼び出し回数に基づく
- 最初のHookを返す
- 2番目のHookを返す
- ....
class HookElement extends Element {
List<HookState> _hooks;
int _hookIndex;
T use<T>(Hook<T> hook) => _hooks[_hookIndex++].build(this);
@override
performRebuild() {
_hookIndex = 0;
super.performRebuild();
}
}
React版のHooks実装説明

ルール
use
をつける
先頭にWidget build(BuildContext context) {
// starts with `use`, good name
useMyHook();
// ....
}
Widget build(BuildContext context) {
// doesn't start with `use`, could confuse people into thinking that this isn't a hook
myHook();
// ....
}
ラップしない!
Widget build(BuildContext context) {
if (condition) {
useMyHook();
}
// ....
}

ホットリロード
基本的に使える。
注意点
useA();
useB(42);
useC();
useA();
useC();
useB()
を消した場合、useC()
は作り直される。基本的に前に宣言されたものが破棄された場合、後に宣言されたホックは再構築される。

Hooksの種類
- useEffect : APIからフェッチし、ローカルでの状態管理
- useState : ローカルの状態管理
- useMemoized : 重たい処理を管理し、パフォーマンスを向上
- useRef
- useCallback
- useContext
- useValueChanged
useState
class MyHomePage extends HookWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
Widget build(BuildContext context) {
final _counter = useState(0);
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter.value',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _counter.value++,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
-
useState
は、ValueNotifierで実装されている -
.value
プロパティで値を取得する
useEffect
関数のコールバックをパラメータとして受け取り、Widgetでサイドエフェクト(ローカル環境以外の状態変化した時の処理)を実行する。これにより、『Widgetがツリーから削除=バックエンドの実行も完全終了』の状態を作ることができる。
useEffect( () {
// side effects code here.
//subscription to a stream, opening a WebSocket connection, or performing HTTP requests
});
サイドエフェクトの内容に関して
-
Stream
の取得 -
WebSocket
への接続 - HTTPリクエスト
などが実行可能である。また、Widgetが破棄されたときに処理はキャンセルできる。
関数のコールバックに関して
戻り値は必須であり、Widgetが破棄されたタイミングで絶対に呼び出される。
Wigetがツリーが削除される前に関数内でサブスクリプションや他のクリーンアップをキャンセル可能。また同期的に呼び出されるため、レンダリングされるたびに呼び出される。
useEffect( () {
// side effects code here.
// - Unsubscribing from a stream.
// - Cancelling polling
// - Clearing timeouts
// - Cancelling active HTTP connections.
// - Cancelling WebSockets conncetions.
return () {
// clean up code
}
});
クリーンアップの例
-
Stream
を終了する - poliling(送信要求)のキャンセル
- タイムアウトの解除
- HTTP接続の終了
- WebSocketの終了
keys
第2引数としてkeys
が存在する。こいつにより、こーるばっくを呼び出すか否かを判別する。
keys
の値を比較し、値が異なると実行 / 同じだと実行しない。
useEffect( () {
// side effects code here.
return () {
// clean up code
}
}, [keys]);
useMemoized
ビルダー関数から生成されたオブジェクトを記録orキャッシュ可能。
keys
第2引数としてkeys
が存在する。
const result = useMemoized(() {}, [keys]);
Widgetの再レンダリング時に参照し、useMemorized
に渡される関数の実行の有無を決定する。
再レンダリング時、keys
が変更されていたら実行 / 変更がないと実行されない。

Primitives
Widgetの異なるライフサイクルと相互作用する低レベルのHooks
useEffect
副作用やオプションでキャンセルするときに便利です。
void useEffect(
Dispose? effect(),
[List<Object?>? keys]
)
サンプル
Stream
を取得し、Widgetが破棄されるタイミングでStream
を停止する。また、Stream
が変更されると昔のは削除&新しいのを取得する。
Stream stream;
useEffect(() {
final subscription = stream.listen(print);
// This will cancel the subscription when the widget is disposed
// or if the callback is called again.
return subscription.cancel;
},
// when the stream changes, useEffect will call the callback again.
[stream],
);
useState
ValueNotifier
をラップしており、.value
で値を取得できる。
ValueNotifier<T> useState<T>(
T initialData
)
サンプル
カウンターアプリ
class Counter extends HookWidget {
@override
Widget build(BuildContext context) {
final counter = useState(0);
return GestureDetector(
// automatically triggers a rebuild of the Counter widget
onTap: () => counter.value++,
child: Text(counter.value.toString()),
);
}
}
useMemoized
複雑なオブジェクトのインスタンスをキャッシュします。
T useMemoized<T>(
T valueBuilder(),
[List<Object?> keys = const <Object>[]]
)
useRef
1 つの変更可能なプロパティを含むオブジェクトを作成します。
ObjectRef<T> useRef<T>(
T initialValue
)
useCallback
関数しかキャッシュしないため、useMemoized
のキャッシュする範囲が狭いバージョンという立ち位置。
T useCallback<T extends Function>(
T callback,
List<Object?> keys
)
useContext
ビルドする HookWidget の BuildContext を取得します。
BuildContext useContext()
useValueChanged
値を監視し、その値が変更されるたびにコールバックをトリガーします。
R? useValueChanged<T, R>(
T value,
R? valueChange(
T oldValue,
R? oldResult
)
)
サンプル
color
が変更されるたびにAnimationController.forward()
を呼び出す。
AnimationController controller;
Color color;
useValueChanged(color, (_, __) {
controller.forward();
});