flutter_hooksでカスタムhook作ってみる

# flutter_hooksでカスタムhook作ってみる

reactのエンジニアがflutter_hooksを使用してカスタムhookを作成するまでの備忘録

# flutter_hooksの説明
flutter_hooksはReact hooksにインスパイアされ、useStateやuseEffectなどの関数ベースの機能を
flutterで使用するためのライブラリです。

https://pub.dev/packages/flutter_hooks

flutter_hooksを使用すると、Stateを持つ必要があった際に、StatefulWidgetなどを使用せずに済むため
コードを簡潔に記述することができます。
flutter_hooksを使用すると以下のように変換できます。いつものカウンターアプリです。

> before
```dart
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

> after

```dart
class MyHomePage extends HookWidget {
  final String title;
  const MyHomePage({super.key, required this.title});

  @override
  Widget build(BuildContext context) {
    final count = useState(0);
    void incrementCounter() {
      count.value++;
    }

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${count.value}',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

かなり簡潔になったことがわかってもらえると思います。

# カスタムhookの作成

Reactでhookを使用している方ならcustom Hookをよく使用しているのかなと思います。
主な用途としては、UIとロジックの分離をおこなったり、ロジックの共通化を行うために使用したりすると思います。

flutterではこんな感じに使用します。
先ほどのCounterのカスタムhookを作成します。

```dart
import 'package:flutter_hooks/flutter_hooks.dart';

UseCounter useCounter() {
  final counter = useState(0);
  void increment() {
    counter.value++;
  }

  return UseCounter(count: counter.value, increment: increment);
}

class UseCounter {
  final int count;
  final void Function() increment;
  UseCounter({required this.count, required this.increment});
}
```

> useCounterを使います。
```dart
class HomePage extends HookWidget {
  final String title;
  const HomePage({super.key, required this.title});

  @override
  Widget build(BuildContext context) {
    final state = useCounter();
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${state.count}',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: state.increment,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

```

# ついでにuseEffectも使用してみる

useEffectの機能も追加して以下の10カウントずつ通知してくれるcounterを作成します。

![スクリーンショット 2022-11-11 17.40.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/951917/2dc24b33-2460-6dfe-c683-d2e8012c2d97.png)



useEffectを使用して、countを監視し、countが10になった時にダイアログを出します。

```dart
class HomePage extends HookWidget {
  final String title;
  const HomePage({super.key, required this.title});

  @override
  Widget build(BuildContext context) {
    final state = useCounter();

    useEffect(() {
      if (state.count % 10 == 0) {
        Future.microtask(() => showDialog(
            barrierDismissible: false,
            context: context,
            builder: (context) => CupertinoAlertDialog(
                  title: Text("${state.count}になりました"),
                  actions: [
                    CupertinoDialogAction(
                      child: const Text('close'),
                      onPressed: () {
                        Navigator.of(context).pop();
                      },
                    )
                  ],
                )));
      }
    }, [state.count]);
    
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${state.count}',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: state.increment,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

useEffectもロジックの部分なので、これもカスタムhookに入れたいなーと思ったので
以下のような感じでuseHomePageというカスタムhookを作成しました。
useCounterは先ほど上で作成したものです。
また、dialogを出すために、contextを使用するので、hookで用意されているuseContextを使用します。

```dart
UseHomePage useHomePage() {
  final counter = useCounter();
  final context = useContext();
  useEffect(() {
    if (counter.count % 10 == 0) {
      Future.microtask(() => showDialog(
          barrierDismissible: false,
          context: context,
          builder: (context) => CupertinoAlertDialog(
                title: Text("${counter.count}になりました"),
                actions: [
                  CupertinoDialogAction(
                    child: const Text('close'),
                    onPressed: () {
                      Navigator.of(context).pop();
                    },
                  )
                ],
              )));
    }
  }, [counter.count]);

  return UseHomePage(counter: counter);
}

class UseHomePage {
  final UseCounter counter;
  UseHomePage({required this.counter});
}
```

useHomePageを使用します。
useEffectがなくなって、ロジックの部分が1行に圧縮されたことがわかると思います。

```dart
class HomePage extends HookWidget {
  final String title;
  const HomePage({super.key, required this.title});

  @override
  Widget build(BuildContext context) {
    final state = useHomePage();
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${state.counter.count}',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: state.counter.increment,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
```