Flutter HooksのuseXXXの使い方

# はじめに

Flutter Hooksとは[React Hooks](https://medium.com/@dan_abramov/making-sense-of-react-hooks-fdbde8803889)のFlutter版です。
React Hooksについてはこちらの記事がわかりやすいです。
[5分でわかるReact Hooks](https://qiita.com/Mitsuzara/items/98d1bc4a83265a764084)

現在、Hooks自体が注目されているのと同時に、[Riverpod](https://riverpod.dev/)という[Provider](https://pub.dev/packages/provider)の上位版が登場し、このRiverpodがFlutter Hooksとの併用を推奨していることもあり、より注目を浴びているようです。
本記事では[Flutter Hooks](https://github.com/rrousselGit/flutter_hooks)のサンプルコードをベースに、Flutter Hooksの`useXXX`まわりの使い方についてまとめます。


※ Flutter側とHooksについて、いろいろと議論されているので、今後どうなるのかは不透明ですが、ここでは触れないでおきます。
https://github.com/flutter/flutter/issues/51752





# 環境

[flutter_hooks](https://dart-pub.mirrors.sjtug.sjtu.edu.cn/packages/flutter_hooks) 0.13.2
Flutter 1.20.3

※2020/9/4時点の最新版

# PrimitivesなHooks

## [useContext](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useContext.html)

`HookWidget`の`build`内で`BuildContext`を取得できます。

### 仕組み

```dart
BuildContext useContext() {
  assert(
    HookElement._currentHookElement != null,
    '`useContext` can only be called from the build method of HookWidget',
  );
  return HookElement._currentHookElement;
}
```

### 使い方

```dart
class SampleWidget extends HookWidget {
  @override
  Widget build(BuildContext context) {
    return Container(child: _buildBody());
  }

  Widget _buildBody() {
    // [BuildContext]を引数でバケツリレーしなくてもよくなる。ただし、buildの外で`useContext`を実行しても[BuildContext]は取得できません
    final context = useContext();
    // 途中略
    
    return SomeWidget();
  }
}
```

## [useState](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useState.html)

`ValueNotifier`をラップして使やすくしてくれます。
HookWidgetの中で`useState(初期値)`と書くだけでValueNotifierを取得できます。
コードは以下のようになっています。

### 仕組み

```dart:useState
// 引数は初期値
ValueNotifier<T> useState<T>([T initialData]) {
  return use(_StateHook(initialData: initialData));
}
```

<details><summary>詳細コード</summary><div>

```dart
class _StateHook<T> extends Hook<ValueNotifier<T>> {
  const _StateHook({this.initialData});

  final T initialData;

  @override
  _StateHookState<T> createState() => _StateHookState();
}

class _StateHookState<T> extends HookState<ValueNotifier<T>, _StateHook<T>> {
  // 実態としてValueNotifierを持っています
  ValueNotifier<T> _state;

  @override
  void initHook() {
    super.initHook();
    // 初めてcallされたときにinitialDataを使って初期化します
    _state = ValueNotifier(hook.initialData)..addListener(_listener);
  }

  @override
  void dispose() {
    // disposeまで担保してくれます
    _state.dispose();
  }

  @override
  ValueNotifier<T> build(BuildContext context) {
    return _state;
  }

  void _listener() {
    setState(() {});
  }
}
```
</div></details>

引数の`[T initialData]`の部分は、オプショナルな位置的パラメータです。(カッコ[]は配列ではありません）

### 使い方

使い方は`useState(0)`のように書くだけです。
引数の初期値は省略も可能です。省略する場合は`useState<int>()`のように型を指定する必要があります。また、その場合の初期値はnullになるため、扱いに注意してください。
また、初期値による更新通知は発生しません。

```dart
class UseStateExample extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final counter = useState(0);

    return Scaffold(
      appBar: AppBar(title: const Text('useState example')),
      body: Center(
        // Read the current value from the counter
        child: Text('Button tapped ${counter.value} times'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => counter.value++,
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

## [useEffect](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useEffect.html)

初期化処理など一度のみ実行したい場合や、開放処理を書きたい場合に便利な仕組みです。

### 仕組み

```dart
void useEffect(Dispose Function() effect, [List<Object> keys]) {
  use(_EffectHook(effect, keys));
}
```

`keys`が指定されていれば、`effect`は一度のみ呼ばれます。そのため、一度だけ行いたい初期化処理などに有用です。
逆に、`keys`が指定されていない場合は毎回`effect`が同期的に呼ばれます。
また、`keys`に変更があった場合は`effect`でreturnした関数が実行され、もう一度`effect`が呼ばれます。

<details><summary>詳細コード</summary><div>

```dart
typedef Dispose = void Function();

class _EffectHook extends Hook<void> {
  const _EffectHook(this.effect, [List<Object> keys])
      : assert(effect != null, 'effect cannot be null'),
        super(keys: keys);
  
  // Widgetがdisposeのタイミングで呼ばれます
  final Dispose Function() effect;

  @override
  _EffectHookState createState() => _EffectHookState();
}

class _EffectHookState extends HookState<void, _EffectHook> {
  Dispose disposer;

  @override
  void initHook() {
    super.initHook();
    scheduleEffect();
  }

  @override
  void didUpdateHook(_EffectHook oldHook) {
    super.didUpdateHook(oldHook);

    // [keys]がnullだと毎回scheduleEffectが実行されます
    if (hook.keys == null) {
      if (disposer != null) {
        disposer();
      }
      scheduleEffect();
    }
  }

  @override
  void build(BuildContext context) {}

  @override
  void dispose() {
    // [effect]で[Dispose](関数)を返却していれば、自動で開放してくれます
    if (disposer != null) {
      disposer();
    }
  }

  void scheduleEffect() {
    disposer = hook.effect();
  }
}
```

</div></details>

### 使い方

```dart
class UseEffectSample extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final store = useMemoized(() => MyStore());

    useEffect(() {
      // 初期表示時にデータのロードを実行
      store.loadData();
      // 関数(Function())を返却しておくと、Widgetのライフサイクルに合わせてWidgetのdisposeのタイミングで関数を実行してくれます（不要であればnullでOK）
      return store.dispose;
    },
      // [keys]は空配列でも問題ない
      const []);

    return SomeWidget();
  }
}
```

ここで注意すべきは、`useEffect`の戻り値は関数のため、`store.dispose()`ではなく、`store.dispose`である必要があります。少し崩した書き方をすると、以下のような形にすると理解しやすいでしょうか。

```dart
useEffect(() {
  // 初期化処理
  
  return () {
    // 開放処理
  };
});
```

## [useMemoized](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useMemoized.html)

値をキャッシュしてくれるため、API通信などで取得したデータの管理に便利です。
具体的にはStreamやFutureの管理を想定しています。

### 仕組み

```dart:useMemoized
T useMemoized<T>(
     T Function() valueBuilder,
     [List<Object> keys = const <dynamic>[]]) {
  return use(_MemoizedHook(
    valueBuilder,
    keys: keys,
  ));
}
```

初めてcallされると一度だけ`valueBuilder`が呼ばれ、その値をキャッシュします。
以降、`HookWidget`がリビルドされてもキャッシュされた値を返却します。この時、`valueBuilder`は呼ばれません。
ただし、keys（オプショナルな第二引数）が変更された場合は`valueBuilder`が再度呼ばれて新しいインスタンスが生成されます。keysはデフォルト初期値が`const <dynamic>[]`となっているため、指定しなくてもそれっぽくは動くようになっています。

<details><summary>詳細コード</summary><div>

```dart
class _MemoizedHook<T> extends Hook<T> {
  const _MemoizedHook(
    this.valueBuilder, {
    List<Object> keys = const <dynamic>[],
  })  : assert(valueBuilder != null, 'valueBuilder cannot be null'),
        assert(keys != null, 'keys cannot be null'),
        // keysは親のHookに渡されます
        super(keys: keys);

  final T Function() valueBuilder;

  @override
  _MemoizedHookState<T> createState() => _MemoizedHookState<T>();
}

class _MemoizedHookState<T> extends HookState<T, _MemoizedHook<T>> {
  T value;

  @override
  void initHook() {
    super.initHook();
    // 初めてcallされたタイミングでvalueBuilderが呼ばれる
    value = hook.valueBuilder();
  }

  @override
  T build(BuildContext context) {
    return value;
  }
}
```
</div></details>


#### keysについて

`useMemorized`で指定する`keys`は、`Object`の配列になっています。`Key`クラスではなく`Object`の配列なので、APIリクエストの場合などはリクエストパラメータなどをセットすると良いかもしれません。
また、これは指定すると親クラスである`Hook`へと渡されます。そこの説明にはこう記載されています。

```dart
  /// A list of objects that specify if a [HookState] should be reused or a new one should be created.
  ///
  /// When a new [Hook] is created, the framework checks if keys matches using [Hook.shouldPreserveState].
  /// If they don't, the previously created [HookState] is disposed, and a new one is created
  /// using [Hook.createState], followed by [HookState.initHook].
  final List<Object> keys;
```

### 使い方

次の`useFuture`/`useStream`と併用することが多いので、そちらでまとめて説明します。

# `dart:async`関連Hooks

## [useFuture](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html) / [useStream](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useStream.html)

FutureやStreamを扱うもので、FutureBuilderやStreamBuilderを簡単にかけるようになります。

### 仕組み

useFutureを例にしますが、useStreamもほぼ同等の内容です。

```dart
// [preserveState]をtrueにすると、Futureのインスタンスが変わった場合でも現在の値を維持することができます。（デフォルトtrue）
AsyncSnapshot<T> useFuture<T>(Future<T> future,
    {T initialData, bool preserveState = true}) {
  return use(_FutureHook(future,
      initialData: initialData, preserveState: preserveState));
}
```

<details><summary>詳細コードおよび`preserveState`パラメータの補足</summary><div>

```dart
class _FutureHook<T> extends Hook<AsyncSnapshot<T>> {
  const _FutureHook(this.future, {this.initialData, this.preserveState = true});

  final Future<T> future;
  final bool preserveState;
  final T initialData;

  @override
  _FutureStateHook<T> createState() => _FutureStateHook<T>();
}

class _FutureStateHook<T> extends HookState<AsyncSnapshot<T>, _FutureHook<T>> {
  Object _activeCallbackIdentity;
  AsyncSnapshot<T> _snapshot;

  @override
  void initHook() {
    super.initHook();
    _snapshot =
        AsyncSnapshot<T>.withData(ConnectionState.none, hook.initialData);
    _subscribe();
  }

  @override
  void didUpdateHook(_FutureHook<T> oldHook) {
    super.didUpdateHook(oldHook);
    if (oldHook.future != hook.future) {
      if (_activeCallbackIdentity != null) {
        _unsubscribe();
        // [preserveState]はここで使われます（後述）
        if (hook.preserveState) {
          _snapshot = _snapshot.inState(ConnectionState.none);
        } else {
          _snapshot =
              AsyncSnapshot<T>.withData(ConnectionState.none, hook.initialData);
        }
      }
      _subscribe();
    }
  }

  @override
  void dispose() {
    _unsubscribe();
  }

  void _subscribe() {
    if (hook.future != null) {
      final callbackIdentity = Object();
      _activeCallbackIdentity = callbackIdentity;
      hook.future.then<void>((data) {
        if (_activeCallbackIdentity == callbackIdentity) {
          setState(() {
            _snapshot = AsyncSnapshot<T>.withData(ConnectionState.done, data);
          });
        }
      }, onError: (dynamic error) {
        if (_activeCallbackIdentity == callbackIdentity) {
          setState(() {
            _snapshot = AsyncSnapshot<T>.withError(ConnectionState.done, error);
          });
        }
      });
      _snapshot = _snapshot.inState(ConnectionState.waiting);
    }
  }

  void _unsubscribe() {
    _activeCallbackIdentity = null;
  }

  @override
  AsyncSnapshot<T> build(BuildContext context) {
    return _snapshot;
  }
}
```

`preserveState`の使用箇所を抜き出すと以下のようになっています。

```dart
if (hook.preserveState) {
  _snapshot = _snapshot.inState(ConnectionState.none);
} else {
  _snapshot = AsyncSnapshot<T>.withData(ConnectionState.none, hook.initialData);
}
```

ここで`AsyncSnapshot`の`inState`と`withData`の実態を使って書き換えると以下のようになります。(ビルドエラーになりますが、雰囲気だけつかんでください…)

```dart
if (hook.preserveState) {
  _snapshot = AsyncSnapshot<T>._(ConnectionState.none, data, error);
} else {
  _snapshot = AsyncSnapshot<T>._(ConnectionState.none, hook.initialData, null);
}
```

AsyncSnapshotを生成する際のデータとエラー値に差分があり、前者は現在の状態を複製し、後者は初期値を使って状態をクリアしていることがわかります。

</div></details>

### 使い方

`package_info`を使ってアプリ情報を取得する場合を例にとってみます。
まず、Hooksを使わずに素直に書くとこうなります。

```dart
class NotUseHooksSample extends HookWidget {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: PackageInfo.fromPlatform(),
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          return Text('appName = ${snapshot.data.appName}');
        } else {
          return Container();
        }
      },
    );
  }
}
```

これをHooksを使うとネストが浅くなり、シンプルになることがわかると思います。

```dart
class UseFutureSample extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final packageInfo = useMemoized(PackageInfo.fromPlatform);
    final snapshot = useFuture(packageInfo);
    if (snapshot.hasData) {
      return Text('appName = ${snapshot.data.appName}');
    } else {
      return Container();
    }
  }
}
```

また、[useStreamController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html)というものもあり、これは自動でdisposeしてくれる`StreamController`を取得できるものです。

# Hooks Riverpod

おまけです。
Flutter Hooksではありませんが、Hooks Riverpodでよく使われる`useProvider`も紹介します。

## useProvider

```dart
T useProvider<T>(ProviderListenable<T> provider) {
  final container = ProviderScope.containerOf(useContext());
  return use(_ProviderHook<T>(container, provider));
}
```

<details><summary>詳細コード</summary><div>

```dart
class _ProviderHook<T> extends Hook<T> {
  const _ProviderHook(this._container, this._providerListenable);

  final ProviderContainer _container;
  final ProviderListenable<T> _providerListenable;

  @override
  _ProviderHookState<T> createState() => _ProviderHookState();
}

class _ProviderHookState<T> extends HookState<T, _ProviderHook<T>> {
  ProviderSubscription<T> _link;

  @override
  void initHook() {
    super.initHook();
    _listen();
  }

  void _listen() {
    _link?.close();
    // De-reference the providerListenable so that `is` promotes the type
    final providerListenable = hook._providerListenable;
    _link = hook._container.listen<T>(
      providerListenable,
      mayHaveChanged: (_) => markMayNeedRebuild(),
    );
  }

  @override
  bool shouldRebuild() => _link.flush();

  @override
  T build(BuildContext context) {
    return _link.read();
  }

  @override
  void didUpdateHook(_ProviderHook<T> oldHook) {
    super.didUpdateHook(oldHook);
    assert(
      oldHook._providerListenable.runtimeType ==
          hook._providerListenable.runtimeType,
      'The provider listened cannot change',
    );
    if (oldHook._container != hook._container) {
      _listen();
    } else if (_link is SelectorSubscription<dynamic, T>) {
      final link = _link as SelectorSubscription<dynamic, T>;
      assert(
        hook._providerListenable is ProviderSelector<dynamic, T>,
        'useProvider was updated from `useProvider(provider.select(...)) '
        'to useProvider(provider), which is unsupported',
      );
      if ((hook._providerListenable as ProviderSelector<dynamic, T>).provider !=
          (oldHook._providerListenable as ProviderSelector<dynamic, T>)
              .provider) {
        _listen();
      } else {
        // this will update _state
        link.updateSelector(hook._providerListenable);
      }
    } else if (oldHook._providerListenable != hook._providerListenable) {
      _listen();
    }
  }

  @override
  void dispose() {
    _link.close();
    super.dispose();
  }
}
```

</div></details>

### 使い方

RiverpodのSampleから拝借しました。

```dart
final helloWorldProvider = Provider((_) => 'Hello world');

class Example extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final value = useProvider(helloWorldProvider);
    return Text(value); // Hello world
  }
}
```

詳しい解説はここでは割愛させていただきますが、ここでいう`Provider`はRiverpodで定義されたもので、他に`ChangeNotifierProvider`や`StateNotifierProvider`、`StateProvider`、`StreamProvider`などが用意されています。`BuildContext`を使わずにアクセスができるので便利ですね。

---

他にも便利なものがいくつか用意されていますので、随時追記していきたいと思います。

# 参考記事

[Flutter Hooks, say goodbye to StatefulWidget and reduce boilerplate code.](https://medium.com/flutter-community/flutter-hooks-say-goodbye-to-statefulwidget-and-reduce-boilerplate-code-8573d4720f9a)

[Flutter を MVVM で実装する](https://medium.com/@wasabeef/flutter-%E3%82%92-mvvm-%E3%81%A7%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B-861c5dbcc565)

[Flutterの状態管理手法の選定(2020年8月版)](https://medium.com/flutter-jp/state-1daa7fd66b94)