Open8

flutter_hooksを勉強するスレ

Ryo24Ryo24

https://qiita.com/Mitsuzara/items/98d1bc4a83265a764084

ReactのHooks

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

StatehulWidgetの問題

StatefulWidgetの問題
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();
  }
}
  • コード量が肥大化
  • initStatedisposeなどが再利用しにくい

flutter_hooksの出番

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の破棄や更新をする
  • useAnimationControllerAnimationControllerのロジックを動作させている。
  • フックは、特殊性を備えた新しいオブジェクト
    • build method内でWidgetにMix-inの時、使う
    • 類似したフックでも再利用可能
    • フックたは完全に独立している。
      • フック間に依存関係がない
      • Widgetに依存していない
Ryo24Ryo24

原則

Stateと同様に、フックもWidgetのElementに格納

  • List<Hook>というオブジェクトで管理される
  • Elementに格納されるStateオブジェクトは1つだけ
  • Hookを使うには、Hook.use を使う

Hook.use で返されるHookは、呼び出し回数に基づく

  1. 最初のHookを返す
  2. 2番目のHookを返す
  3. ....
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実装説明
https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e

Ryo24Ryo24

ルール

先頭にuseをつける

Good
Widget build(BuildContext context) {
  // starts with `use`, good name
  useMyHook();
  // ....
}
Bad
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();
  }
  // ....
}
Ryo24Ryo24

ホットリロード

基本的に使える。

注意点

Before
useA();
useB(42);
useC();
After
useA();
useC();

useB()を消した場合、useC()は作り直される。基本的に前に宣言されたものが破棄された場合、後に宣言されたホックは再構築される。

Ryo24Ryo24

https://blog.logrocket.com/how-to-use-flutter-hooks/

Hooksの種類

  • useEffect : APIからフェッチし、ローカルでの状態管理
  • useState : ローカルの状態管理
  • useMemoized : 重たい処理を管理し、パフォーマンスを向上
  • useRef
  • useCallback
  • useContext
  • useValueChanged

useState

Hooks版のカウンターアプリ
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が変更されていたら実行 / 変更がないと実行されない。

Ryo24Ryo24

Primitives

Widgetの異なるライフサイクルと相互作用する低レベルのHooks

useEffect

https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html
副作用やオプションでキャンセルするときに便利です。

概要
void useEffect(
Dispose? effect(),
[List<Object?>? keys]
)

サンプル

Streamを取得し、Widgetが破棄されるタイミングでStreamを停止する。また、Streamが変更されると昔のは削除&新しいのを取得する。

useEffect sample
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

https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useState.html
変数を作成し、それを管理する。
ValueNotifierをラップしており、.valueで値を取得できる。

概要
ValueNotifier<T> useState<T>(
T initialData
)

サンプル

カウンターアプリ

useState sample
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

https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html
複雑なオブジェクトのインスタンスをキャッシュします。

概要
T useMemoized<T>(
T valueBuilder(),
[List<Object?> keys = const <Object>[]]
)

useRef

https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useRef.html
1 つの変更可能なプロパティを含むオブジェクトを作成します。

概要
ObjectRef<T> useRef<T>(
T initialValue
)

useCallback

https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useCallback.html
関数インスタンスをキャッシュします。 
関数しかキャッシュしないため、useMemoizedのキャッシュする範囲が狭いバージョンという立ち位置。

概要
T useCallback<T extends Function>(
T callback,
List<Object?> keys
)

useContext

https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useContext.html
ビルドする HookWidget の BuildContext を取得します。

概要
BuildContext useContext()

useValueChanged

https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useValueChanged.html
値を監視し、その値が変更されるたびにコールバックをトリガーします。

概要
R? useValueChanged<T, R>(
T value,
R? valueChange(
T oldValue,
R? oldResult
)
)

サンプル

colorが変更されるたびにAnimationController.forward()を呼び出す。

useValueChanged sample
AnimationController controller;
Color color;

useValueChanged(color, (_, __) {
  controller.forward();
});