🌊

【Flutter】RiverpodGenerator✖️go_router超入門 〜Riverpod編〜

2024/07/19に公開

はじめに

Riverpod Generator✖️go_router入門として今回はRiverpod Generatorにフォーカスを当てて紹介していきます!go_router要素はほぼないと思います!
今回はRiverpodを使用したことがない、苦手意識がある方向けに本当に基礎的な部分を紹介します!
目標はRiverpod Generatorを使ってコードのて生成することと、基本的なRiverpodの使用方法(値の定義、更新など)を習得できればと思います。

RiverpodGeneratorとは

RiverpodGeneratorは何が良いか

まずこのGeneratorというものはよく耳にするが、どういった意味なのでしょうか?
Generatorを翻訳すると『発生器』というみたいです。では何を発生してくれるのでしょうか?
結論から言いますと、通常のGeneratorを使用しないRiverpodのみでの実装で[書くのがめんどくさい]「定型部分」「よくわからない」「おまじない」みたいな部分を短いコードを書くだけで、あとはコマンド一つで自動生成してくれるというものがGeneratorです!!便利ですね!!
初見で「よくわからない部分」を理解するのは時間もメンタルもすり減ると思うので、今後Riverpod学び始める方は最初からGeneratorを使用した実装にしていくのが個人的にはおすすめです!

軽くRiverpodの基礎知識

そもそもRiverpodを使うメリットは何かという話も少ししておきましょう。
通常の状態管理であるstatefulとstatelessを使う方法とriverpodを使う実装の大きな違いは、グローバルで定義しても良いということです。厳密には少し違うかもしれませんが、、。
このメリットから定義した値をどこからでも呼び出し、更新ができるようになり、プロジェクトが大きくなればなるほどより開発がしやすくなってくると思います!

RiverpodGeneratorで実装

早速RiverpodGeneratorを使って実装をしていきましょう!!
今回はサンプルコードにコメントアウトで解説を書いておりますのでそちらを参考にしてください!

1.サンプル

サンプルコード
https://github.com/sodateya/riverpod_with_go_router_sample?tab=readme-ov-file
サンプルプロジェクト
https://riverpod-with-go-router.web.app/#/page1

2.使用パッケージ

dependencies

flutter_riverpod
https://pub.dev/packages/flutter_riverpod
riverpod_annotation
https://pub.dev/packages/riverpod_annotation

dev_dependencies

riverpod_generator
https://pub.dev/packages/riverpod_generator
build_runner
https://pub.dev/packages/build_runner

3.Riverpodを使う準備

これはGeneratorを使っても、使わなくてもriverpodを使う上で必要な知識ですので少し紹介します。
すでにRiverpodを使ったことがある方は飛ばしても差し支えないと思います

  • main.dartの編集

    • runApp内のweidgetをProviderScopeでラップします。こうすることで、どこからでもriverpodで定義したものを扱えるようになります。逆にこうしないと何もできないので注意です!!
      main.dart
      lib/main.dart
      void main() {
      runApp(const ProviderScope(child: MyApp()));
      }
      
  • StatelessWidget,StatefulWidgetからConsumerWidgetへ変更

    • ConsumerWidgetを使うことでbuild()関数のなかにWidgetRefが追加されます。このrefを使うことでriverpodで定義したものを呼び出す頃ができます。
      main.dart
      lib/main.dart(MyApp)
      class MyApp extends ConsumerWidget {
      const MyApp({super.key});
      
      Widget build(BuildContext context,WidgetRef ref) {
      final title = ref.watch(fooProvider);
      return MaterialApp(
        debugShowCheckedModeBanner: false,
        title: title,
        theme: ThemeData(
          useMaterial3: true,
        ),
        home: const TopScreen(),);}}
      
  • refの基礎知識(ref.read,ref.watch,ref.listen)

    これに関しては以下の方のブログがとても端的でわかりやすいと思いますので、こちらを参考にしてみると良いと思います。
    https://note-tmk.hatenablog.com/entry/2023/09/20/114412

4.Provider基本的な値の定義・更新・呼び出し

provider(値定義、更新メソッド定義部分)
lib/presentation/page2/provider/page_2_count_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'page_2_count_provider.g.dart';

// ジェネレーターを使った基本的なプロバイダーの定義

class Page2Count extends _$Page2Count {
  
  int build() {
    return 0;
  }

// プロバイダの更新したい場合は、クラスでプロバイダを定義しているため、に関数を定義できる
// プロバイダの値はstateに入っているのでstateを更新すると値をを更新できる
  void increment() {
    state = state + 1;
    print(state);
  }

  void decrement() {
    state = state - 1;
    print(state);
  }
}

// これでターミナルにて以下のコマンドを実行すると.gファイルが生成させる
// dart run build_runner build --delete-conflicting-outputs

// またプロバイダー内にメソッドを持たせない場合は以下のようにクラスではなくメソッドでも定義できる

int testPage2Count(TestPage2CountRef ref) {
  return 0;
}
page(呼び出し部分)
lib/presentation/page2/page/page_2.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_with_go_router_sample/presentation/page2/component/count_container.dart';
import 'package:riverpod_with_go_router_sample/presentation/page2/provider/page_2_count_provider.dart';
import 'package:riverpod_with_go_router_sample/presentation/widget/go_router_path_text.dart';
import 'package:riverpod_with_go_router_sample/presentation/widget/page_content_text.dart';

class Page2 extends ConsumerWidget {
  const Page2({super.key, required this.title});
  final String title;

  
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(page2CountProvider);
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: SingleChildScrollView(
        child: Center(
          child: Column(
            children: [
              const PageContentText(
                  riverpodText: 'riverpodGeneratorを使った\n基本的な値の定義・更新',
                  goRouterText: '同じルート内での遷移'),
              const GoRouterPathText(),
              const SizedBox(height: 40),
              CountContainer(
                countNum: count,
                onAddTap: () {
                  //ref.read(fooProvide).notifier.Fooでプロバイダ内のメソッドを呼び出すことができる
                  final notifier = ref.read(page2CountProvider.notifier);
                  notifier.increment();
                },
                onRemoveTap: () {
                  final notifier = ref.read(page2CountProvider.notifier);
                  notifier.decrement();
                },
              ),
              const SizedBox(height: 40),
              ElevatedButton(
                  onPressed: () {
                    //同じルート内のパスに遷移する際は以下のようにフルパスで遷移  ※同じルート内の意味はrouter.dart参照
                    context.push('/page2$title/page3$count');
                  },
                  child: const Text('Page 3へ')),
              const Text(
                  '値をPage3に渡す\ncountが10以上でXpageにリダイレクトされます\n※リダイレクトについてはrouter.dart参照'),
            ],
          ),
        ),
      ),
    );
  }
}

解説
基本的に必要そうな説明はサンプルコードにコメントアウトで記載していますのでそちらを参考にしていただければと思います。
今回のプロジェクトはgo_routerと一緒に使う方法として作成しているため少し無駄な部分もありますがご了承ください。

  • .gファイルの生成
    • provider(値定義、更新メソッド定義部分)を作ることがせきたら以下のコマンドをプロジェクトのディレクトリターミナルで実行します。
    • 実行が成功すると〇〇.g.dartというファイルが生成されているはずです。このファイルこそが書くのがめんどくさい]「定型部分」「よくわからない」「おまじない」みたいな部分になります!!
dart run build_runner build --delete-conflicting-outputs

5.FamiryPrvider定義・更新・呼び出し

provider(値定義、更新メソッド定義部分)
lib/presentation/page3/provider/page_3_count_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'page_3_count_provider.g.dart';


class Page3Count extends _$Page3Count {
  
  //build内に任意の引数を持たせることができる
  //FamilyProviderとして扱える
  int build(int count) {
    return count + 10;
  }

  void increment() {
    state = state + 1;
  }
}
page(呼び出し部分)
lib/presentation/page2/page/page_3.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_with_go_router_sample/presentation/page2/provider/page_2_count_provider.dart';
import 'package:riverpod_with_go_router_sample/presentation/page3/component/count_num_container.dart';
import 'package:riverpod_with_go_router_sample/presentation/page3/component/page2_count_container.dart';
import 'package:riverpod_with_go_router_sample/presentation/page3/provider/page_3_count_provider.dart';
import 'package:riverpod_with_go_router_sample/presentation/widget/go_router_path_text.dart';
import 'package:riverpod_with_go_router_sample/presentation/widget/page_content_text.dart';

class Page3 extends ConsumerWidget {
  const Page3({super.key, required this.countNum});
  final int countNum;

  
  Widget build(BuildContext context, WidgetRef ref) {
    final page2Count = ref.watch(page2CountProvider);

    //プロバイダに引数を持たせることができる(FamilyProvider) ※定義方法はpage_3_count_provider.dartを参照
    //(riverpod.v1で言う FamilyProviderしてあつあえる)
    final page3Count = ref.watch(page3CountProvider(countNum));

//以下riverpodとは関係ない部分が多いため省略
//全コードはソースコード参照

解説
先ほどの基本的な使い方と大差はないのです。
違うところとすればproviderのに引数を持たせているところです。こうすることで、さまざまな値に対応値たproviderにすることができます

6.FutureProvider定義・更新・呼び出し

provider(値定義、更新メソッド定義部分)
lib/presentation/page4/provider/page4_data_provider.dart
import 'dart:math';

import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_with_go_router_sample/presentation/page4/model/human.dart';

part 'page4_data_provider.g.dart';


class Page4Data extends _$Page4Data {
  

  // buildメソッドをasync(非同期処理)にするとFutureProviderとして扱える ※build内に引数を持たせることも可能
  Future<Human> build() async {
    return Human(name: 'noName', age: 0);
  }

  Future getHuman() async {

    // providerの状態をローディングにしたい時の処理
    state = const AsyncValue.loading();

    // humanListProviderからランダムに一人取得
    final humanList = ref.read(humanListProvider);
    final randomHuman = humanList[Random().nextInt(humanList.length)];

    // 今回は2秒間ローディング時間を設けるよう設計
    // 本番実装でのデータをとってきている間のイメージ
    await Future.delayed(const Duration(milliseconds: 2000));
    // 得られてデータに更新する
    state = AsyncData(randomHuman);
    print(state.value!.name);
  }

  Future getErrorHuman() async {
    state = const AsyncValue.loading();
    await Future.delayed(const Duration(milliseconds: 2000));
    try {
      // エラーを無条件に発生させる
      throw Exception('エラーが発生しました');
    } catch (e, stackTrace) {
      // エラー状態を設定
      state = AsyncValue.error(e, stackTrace);
    }
  }
}


List<Human> humanList(HumanListRef ref) {
  return [
    Human(name: 'Funbatter', age: 28),
    Human(name: 'Jon', age: 18),
    Human(name: 'Takashi', age: 68),
  ];
}
page(呼び出し部分)
lib/presentation/page4/page/page_4.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_with_go_router_sample/presentation/page4/provider/page4_data_provider.dart';
import 'package:riverpod_with_go_router_sample/presentation/widget/go_router_path_text.dart';
import 'package:riverpod_with_go_router_sample/presentation/widget/page_content_text.dart';

class Page4 extends ConsumerWidget {
  const Page4({super.key});
  
  Widget build(BuildContext context, WidgetRef ref) {
    // FutureProviderを呼び出し  ※定義方法はpage_4_data_provider.dartを参照
    final human = ref.watch(page4DataProvider);

    // FutureProviderは返り値のステータスにswitchよって返すものを割り当てられる
    // ※以前は公式ドキュメントでhuman.when(data.....)のようにwhenを使った割り当てを推奨していたが現在はswitchに変わりました
    final Widget humanData = switch (human) {
      // dataが入ってきた時の返すwidget
      AsyncData(:final value) => Text(
          'name: ${value.name} \n age: ${value.age}',
          softWrap: true,
          overflow: TextOverflow.ellipsis,
          style: const TextStyle(fontSize: 20),
          maxLines: 3,
        ),
      // errorが入ってきた時の返すwidget
      AsyncError(:final error) => Text(error.toString()),
      // loading中に返すwidget
      AsyncLoading() => const CircularProgressIndicator(),
      _ => const CircularProgressIndicator(),
    };

    return Scaffold(
      appBar: AppBar(
        title: const Text('Page 4'),
      ),
      body: SingleChildScrollView(
        child: Center(
          child: Column(
            children: [
              const PageContentText(
                riverpodText: 'FutureProviderの定義・呼び出し',
                goRouterText: 'extraでの値渡し',
              ),
              const GoRouterPathText(),
              const SizedBox(height: 40),

              // このようにでFutureProviderのwidgetを
              humanData,
              const SizedBox(height: 40),
              ElevatedButton(
                  style: ElevatedButton.styleFrom(
                      backgroundColor:
                          const Color.fromARGB(255, 201, 196, 234)),
                  onPressed: () {
                    final notifier = ref.read(page4DataProvider.notifier);
                    notifier.getHuman();
                  },
                  child: const Text('Data Get')),
              ElevatedButton(
                  style: ElevatedButton.styleFrom(
                      backgroundColor:
                          const Color.fromARGB(255, 234, 196, 196)),
                  onPressed: () {
                    final notifier = ref.read(page4DataProvider.notifier);
                    notifier.getErrorHuman();
                  },
                  child: const Text('Error Data Get')),

              const SizedBox(height: 40),
              ElevatedButton(
                  onPressed: () {
                    // モデルを次のページに渡す際はextraを使う
                    // ※extraの定義はrouter.dart参照
                    context.push('/page5', extra: human.value);
                  },
                  child: const Text('Page 5へ'))
            ],
          ),
        ),
      ),
    );
  }
}

解説
これまでのProviderとは違いbuild()関数の後にasyncがついています。
こうすることでFutureProviderとして定義できます。
FutureProviderの特徴は、value,loading,errorの状態があるということです。
この状態に応じてswitch文で分岐して返すwidgetなどを決めてあげられるようになっています。

少し前までは whenを使う実装が推奨されていましたが、最新ではswitchを使うようになったみたいです

7.RiverpodGeneratorを使ったGo_routerのルート定義

RiverpodGeneratorを使ったGo_routerのルート定義
lib/router/provoder/router.dart
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_with_go_router_sample/presentation/page1/page/page_1.dart';
import 'package:riverpod_with_go_router_sample/presentation/page2/page/page_2.dart';
import 'package:riverpod_with_go_router_sample/presentation/page2/provider/page_2_count_provider.dart';
import 'package:riverpod_with_go_router_sample/presentation/page3/page/page_3.dart';
import 'package:riverpod_with_go_router_sample/presentation/page4/model/human.dart';
import 'package:riverpod_with_go_router_sample/presentation/page4/page/page_4.dart';
import 'package:riverpod_with_go_router_sample/presentation/page5/page/page_5.dart';
import 'package:riverpod_with_go_router_sample/presentation/x_page/page/x_page.dart';

part 'router.g.dart';


class Router extends _$Router {
  
  GoRouter build() {
    return GoRouter(

        //アプリ起動時のパス
        initialLocation: '/page1',

        //リダイレクト内の条件にマッチしたら該当のページに飛ばせられる ※複数条件入れられます
        //ログイン状況やユーザのステータスに応じてリダイレクトするときに使うと良いかも
        redirect: (context, state) {
          //ここの場合はpage2でカウントアップしたときカウントが10を超えたらpage3ではなくXpageに遷移させる
          final count = ref.read(page2CountProvider);
          if (count >= 10) {
            return '/x_page';
          }

          //通常時はnullでOK
          return null;
        },

        //routesの中にページを入れていく
        //top層のパスには'/'を先頭につける
        routes: [
          GoRoute(
            path: '/page1',
            builder: (context, state) => const Page1(),
          ),

          //引数を渡したい場合は
          //'ページパス:パラメータ1:パラメータ2'
          //のように値を渡すことができる(以下では'title'が該当部分)
          //builder部分は'state.pathParameters[パラメータ1]'のように割り当てることができる
          GoRoute(
              path: '/page2:title',
              builder: (context, state) => Page2(
                    title: state.pathParameters['title']!,
                  ),
              routes: [
                //page2の中のroutsに入れると
                //'page2:title/page3:countNum'
                //のようにつながったパスにできる
                GoRoute(
                  path: 'page3:countNum',
                  builder: (context, state) => Page3(
                    countNum: int.parse(state.pathParameters['countNum']!),
                  ),
                )
              ]),
          GoRoute(
            //nameを定義するとnamedPushで使える
            //(多分Analyticsを使用していたら定義しているだけでこのnameでログが残ると思う)
            name: 'page4',
            path: '/page4',
            builder: (context, state) => const Page4(),
          ),
          GoRoute(
            path: '/page5',
            builder: (context, state) {
              //pathParameterは文字列のみ対応しているため、もしモデルなどを引数に持たせたい場合は、extraを使用する。
              //またはモデルを分解して文字列にするか、、、
              // ※extraはpathに表示されないため注意する
              final human = state.extra;
              return Page5(human: human as Human);
            },
          ),
          //リダイレクトされた際のページ
          GoRoute(
            path: '/x_page',
            builder: (context, state) => const XPage(),
          ),
        ]);
  }
}

解説
少し番外編としてRiverpodGeneratorを使ったgo_routerのルート定義を紹介します。
RiverpodGeneratorを使って定義することでリダイレクト処理の中などでもproviderを呼び出すことができ便利に使うことができました!
『go_router編』で詳しく説明していきますので少々お待ちください!

まとめ

今回紹介した3つのProviderを使いこなすことができれば大体のことは実装可能だと思います!
RiverpodGeneratorは短いコードを書くだけで様々なことができるようになります。
ぜひRiverpodGeneratorを使ってアプリケーションを作ってみてください!!

githubのサンプルのソースコードのREADMEにも軽く説明を書いておりますのでぜひ参考にしてみてください!!
https://github.com/sodateya/riverpod_with_go_router_sample

Flutter大学

Discussion