hooks_riverpodについて学んでみる

## riverpodのhooksってなにか?
ReackHooksを真似て作ったらしい?

**開発環境**

- flutter2.8.0
- hooks_riverpod: ^1.0.3

## hooks_riverpod と flutter_riverpod の書き方の違い
どちらのRiverpodパッケージをインストールするかによって、Widgetからの利用時に書き方の違いがあります。

**グローバル変数としてのProvider定義の書き方は共通です。**
flutter_riverpodと同じだよ!
```dart
final appNameProvider = Provider((ref) => 'Special App!');
```

**hooks_riverpodの書き方の例**
pub.devのリンク
https://pub.dev/packages/hooks_riverpod

main.dart
```dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

void main() {
  runApp(
    /// [MyApp] is wrapped in a [ProviderScope].
    /// This widget is where the state of most of our providers will be stored.
    /// This replaces `MultiProvider` if you've used `provider` before.
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

/// A provider that creates and listen to a [StateNotifier].
///
/// Providers are declared as global variables.
/// This does not hinder testability, as the state of a provider is instead
/// stored inside a [ProviderScope].
final counterProvider = StateNotifierProvider<Counter, int>((_) => Counter());

/// A simple [StateNotifier] that implements a counter.
///
/// It doesn't have to be a [StateNotifier], and could be anything else such as:
/// - [ChangeNotifier], with [ChangeNotifierProvider]
/// - [Stream], with [StreamProvider]
/// ...
class Counter extends StateNotifier<int> {
  Counter() : super(0);

  void increment() => state++;
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}
// StatelessWidgetが、HookConsumerWidgetに変更されている。
class MyHomePage extends HookConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Riverpod counter example'),
      ),
      body: Center(
        child: Text(
          '$count',
          style: Theme.of(context).textTheme.headline4,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

**flutter_riverpodの書き方の例**

pub.devのリンク
https://pub.dev/packages/flutter_riverpod

main.dart
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// A Counter example implemented with riverpod

void main() {
  runApp(
    // Adding ProviderScope enables Riverpod for the entire project
    const ProviderScope(child: MyApp()),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: Home());
  }
}

/// Providers are declared globally and specifies how to create a state
final counterProvider = StateProvider((ref) => 0);

// StatelessWidgetが、ConsumerWidgetに変更されている。
class Home extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter example')),
      body: Center(
        // Consumer is a widget that allows you reading providers.
        // You could also use the hook "ref.watch(" if you uses flutter_hooks
        child: Consumer(builder: (context, ref, _) {
          final count = ref.watch(counterProvider.state).state;
          return Text('$count');
        }),
      ),
      floatingActionButton: FloatingActionButton(
        // The read method is an utility to read a provider without listening to it
        onPressed: () => ref.read(counterProvider.state).state++,
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

## 今回作ってみた学習サンプル
ディレクトリ構造
```
lib
├── components
│   └── importer.dart
├── main.dart
├── models
│   └── counter_view_model.dart
└── views
    └── home_page.dart
```

今回は、exportという面白いDartの機能を使ってみた。import文を使って各ページで、同じpackageを毎回読み込むのが面倒臭い!!!
で、１つのファイルにexport文を使ってまとめる方法を思い出したので使ってみた。

components/import.dart
```dart
export 'package:flutter/material.dart';
export 'package:hooks_riverpod/hooks_riverpod.dart';
export 'package:hook_sample_app/models/counter_view_model.dart';
export 'package:hook_sample_app/views/home_page.dart';
```

models/counter_view_model.dart
```dart
import 'package:hook_sample_app/components/importer.dart';

//①Riverpodのみ設定

final countViewModel = ChangeNotifierProvider((_) => CountViewModel());

//CountViewModelの中身は一緒
class CountViewModel extends ChangeNotifier {
  final String title = 'hooks_riverpod';

  int _counter = 0;
  get counter => _counter;

  void incrementCounter() {
    _counter++;
    notifyListeners();
  }
}
```

views/home_page.dart
```dart
import 'package:hook_sample_app/components/importer.dart';

// StatelessWidgetをHookConsumerWidgetに変更
class MyHomePage extends HookConsumerWidget {
  MyHomePage({Key? key}) : super(key: key);
  @override
  // 第2引数にWidgetRef refを追加
  Widget build(BuildContext context, WidgetRef ref) {
    //④countViewModelを参照することを定義、viewModelでViewModelを参照する
    final viewModel = ref.watch(countViewModel);
    return Scaffold(
      appBar: AppBar(
        title: Text('${viewModel.title}'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${viewModel.counter}',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: viewModel.incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

main.dart
```dart
import 'components/importer.dart';

void main() {
  runApp(
    // MyAppをProviderScopeで囲む
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hooks riverpod!',
      theme: ThemeData(
        primarySwatch: Colors.indigo,
      ),
      home: MyHomePage(),
    );
  }
}
```

ご覧いただくと各ページで、import文が今のところ１つしかない!
今まで作ってきたアプリより見た目がスッキリして、補修もしやすくなった気がした...
もっと速く使うべきだった!!!

## おまけ
カウンターばっかりだと面白くないから、ボタンを押したら文字が変わる機能を追加してみた!

models/counter_view_model.dart
```dart
import 'package:hook_sample_app/components/importer.dart';

// 定数の中にクラスを格納する
final countViewModel = ChangeNotifierProvider((_) => CountViewModel());

//CountViewModelの中身は一緒
class CountViewModel extends ChangeNotifier {
  final String title = 'hooks_riverpod!';

  int _counter = 0;
  get counter => _counter;

  String _privateText1 = 'これは多分ゲッターだよ???';
  get privateText => _privateText1;

  String _privateText2 = 'これはプライベートなプロパティだよ!';
  get privateText2 => _privateText2;

  String _greeting = 'あいさつ?';
  get greeting => _greeting;

  void incrementCounter() {
    _counter++;
    notifyListeners();
  }

  void sayMethod() {
    // ボタンを押すと変数に文字を代入する
    _greeting = 'Hello Riverpos!!!';
    notifyListeners();
  }
}
```

views/home_page.dart
```dart
import 'package:hook_sample_app/components/importer.dart';

// StatelessWidgetをHookConsumerWidgetに変更
class MyHomePage extends HookConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);
  @override
  // 第2引数にWidgetRef refを追加
  Widget build(BuildContext context, WidgetRef ref) {
    //④countViewModelを参照することを定義、viewModelでViewModelを参照する
    final viewModel = ref.watch(countViewModel);
    return Scaffold(
      appBar: AppBar(
        title: Text('${viewModel.title}'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('${viewModel.privateText}'),
            const SizedBox(height: 10),
            Text('${viewModel.privateText2}'),
            const SizedBox(height: 10),
            Text('${viewModel.greeting}'),
            const SizedBox(height: 10),
            Text(
              '${viewModel.counter}',
              style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold, color: Colors.blueGrey),
            ),
            ElevatedButton(
                onPressed: viewModel.sayMethod,
                child: const Text('あいさつをする')),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: viewModel.incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

```

![hooks_riverpod.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2303722/ee64ed6f-0cd3-2c9f-3e26-3827aefe1ac9.gif)