【Flutter】Riverpodを用いてカウンタアプリを書き換える


# はじめに
Providerの基本的な使い方を、備忘録としてまとめようと思います。
基本riverpod公式ドキュメントから、必要と思われる情報を抜き出して記述していますので、さらに詳しく知りたい方はこちらをご覧ください。
https://riverpod.dev/ja/docs/getting_started

また、consumerWidgetの書き換えに関しては、さくしんさんのudemy講座を参考にさせていただきました。
riverpodの書き換えだけでなく、テスト駆動開発や、MVVMの考え方などにも深く言及されているのでおすすめです。
https://www.udemy.com/course/riverpod/


# Providerの種類
まずProviderの種類から。以下の7種類のproviderがあるが、初めのうちはstateProviderを使うことが多い。FutureProvider、StreamProviderに関してはfirebaseを使用する際によく使う。
- Provider…定数
- StateProvider…変数
- ScopedProvider…出力
- StateNotifierProvider…メソッドのついたprovider
- FutureProvider…外部からデータを取ってきて、後で参照する時に使用
- StreamProvider…外部からデータを引っ張ってくる時に使用


# Providerを使うメリット
- 値にアクセスしやすい。グローバルに定義されるので、バケツリレーみたいにページ間で値をやりとりしなくて済む
- 画面更新の最適化ができる。更新範囲を決めることができるので、描画にかかる時間を減らすことができる。
- いくつかの値を組み合わせて、別の値として定義しやすい。



# Providerの始め方
とりあえずグローバル変数として定義して、いろんなファイルから呼び出すのがproviderの主な使い方。
どのproviderも「ref」というオブジェクトを引数にして受け取る。一般的に、refはウィジェットからプロバイダに渡される。

```dart
final myProvider = Provider((ref) => return MyValue());
```

# refの使い道
refには以下の3種類の使い方がある。
- ref.watch
- ref.listen
- ref.read

しかし、そのうちref.readは公式では推奨されていない。

> ref.read はリアクティブではないため、可能な限り使用を避けてください。
watch や listen の使用では問題が生じる場合の回避策として存在しています。 ほとんどの場面では watch や listen の使用、特に watch の使用がベターなはずです。




#### ref.watch
プロバイダの値を取得した上で、その変化を監視する。値が変化すると、その値に依存するウィジェットやプロバイダの更新が行われる。
```dart
final counterProvider = StateProvider((ref) => 0);

class HomeView extends ConsumerWidget {
  const HomeView({Key? key}): super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // `ref` を使ってプロバイダを監視する
    final counter = ref.watch(counterProvider);

    return Text('$counter');
  }
}
```

ただし、onTap、onPressedなど、非同期な場面で使用してはならない。
> watch メソッドは ElevatedButton の onPressed 内など、非同期的な場面で呼び出さないでください。 また initState を始め、State のライフサイクルメソッド内での使用も避けてください。


#### ref.listen
プロバイダの値を監視し、値が変化するたび、関数を呼び出す（主に、画面遷移するとき、ダイアログを表示するときなど）。ref.listenには2つの引数が必要。
- 第1引数にプロバイダ
- 第2引数に状態が変化した際に実行する関数

```dart
final anotherProvider = Provider((ref) {
  ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
    print('The counter changed $newCount');
  });
});
```

ref.watchと同様、非同期な場面で用いてはならない。
> listen メソッドは ElevatedButton の onPressed 内など、非同期的な場面で呼び出さないでください。 また initState を始め、State のライフサイクルメソッド内での使用も避けてください。



#### ref.read
プロバイダの値を取得する（監視はしない）。クリックイベント等の発生時に、値を取得する場合に使用できる。公式では非推奨。


# providerを使う際のウィジェット
通常のウィジェットでは ref を使用することができないため、ウィジェットをprovider用に書き換える必要がある。

#### StatelessWidgetを書き換える
StatelessWidget = ConsumerWidget と考えてもよい。
buildメソッドに第2パラメータ（WidgetRef ref）が存在する以外の違いはない。

#### StatefulWidgetを書き換える
StatefulWidget + State = ConsumerStatefulWidget + ConsumerState と考えてもよい。

#### 実際に書き換えてみる
flutterで初期生成されるカウンタアプリをprovider用に書き換える。

デフォルト
```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  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),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}


```
StatefulWidgetとState<MyHomePage>をまとめてConsumerWidgetに変更
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final countProvider = StateProvider<int>((ref) => 0);

void main() {
  runApp(ProviderScope(child: const MyApp()));
}

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  int _counter = 0;
  void _incrementCounter() {
  }

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    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(
             ref.watch(countProvider).toString(),
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.watch(countProvider.state).state++;
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
```

変更点
- MyHomePageをConsumerWidgetに変更
- Widget buildにWidgetRefの追加
- MyAppをproviderScopeで囲む
- カウンタを保存する変数_counterを、countProviderとして新たに定義
- カウンタの表示をcountProviderから取得
- onPressedでcountProvider.stateの値を書き換える

しかしこれだと、countProviderの値が変わるたびに、ウィジェットが更新されてしまう。
描画効率の面からもこれは避けたい。そこで、ウィジェットをstatelessWidgetに戻し、プロバイダのある箇所をConsumerで囲むことにする。これにより、画面全体ではなく、変更のあったConsumerの部分だけが再描画される。


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

final countProvider = StateProvider<int>((ref) => 0);

void main() {
  runApp(ProviderScope(child: const MyApp()));
}

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  Widget build(BuildContext context) {
    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:',
            ),
            Consumer (
              builder: (BuildContext context, WidgetRef ref ,Widget? child) =>
                  Text(
               ref.watch(countProvider).toString(),
                style: Theme.of(context).textTheme.headline4,
              ),
    ),
          ],
        ),
      ),
      floatingActionButton: Consumer(
        builder: (BuildContext context, WidgetRef ref ,Widget? child) =>
            FloatingActionButton(
          onPressed: () {
            ref.watch(countProvider.state).state++;
          },
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        ),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
```
変更点
- MyHomePageをstatelessWidgetに変更
- Widget buildからWidgetRefの削除
- 各プロバイダをConsumerで囲む

これでも十分だが、プロバイダのある場所をいちいちConsumerをで囲むのは面倒なのでConsumerStatefulWidgetを使用する。これにより、ウィジェット内で、Consumerで囲まずにrefを使用することができる。


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

final countProvider = StateProvider<int>((ref) => 0);

void main() {
  runApp(ProviderScope(child: const MyApp()));
}

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends ConsumerStatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  ConsumerState<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends ConsumerState<MyHomePage> {

  @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(
               ref.watch(countProvider).toString(),
                style: Theme.of(context).textTheme.headline4,
              ),

          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
          onPressed: () {
            ref.watch(countProvider.state).state++;
          },
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        ),
    );
  }
}

```

変更点
- MyHomePageをConsumerStatefulWidgetに変更
- State<MyHomePage>をConsumerState<MyHomePage>に変更
- Consumerの削除

以上、カウンタアプリをConsumerWidget、ConsumerStatefulWidgetの2種類を用いて書き換えることに成功した。

# 終わりに

自分も初心者なので、誤り等あれば指摘していただけるとありがたいです。
最後まで読んでいただきありがとうございました。