🙆

StateNotifireProviderとは?

2022/07/31に公開

なんとなくわかってきた気がする?

今流行りのriverpodの機能であるStateNotifireProviderをドキュメントを見ながら、使ってみた。

公式ドキュメント

https://riverpod.dev/ja/docs/providers/state_notifier_provider

ドキュメントを読んでみると、状態の変化を監視しユーザーの操作により変化する状態を管理するものみたいですね。この辺がビジネスロジックを理解できるIT歴が長い人とそうでない人の差が現れますね😅

こちらが完成品です。参考までにどうぞ
https://github.com/sakurakotubaki/StateNotifierProviderSample

Firebase CLIを使っているので、こちらで環境構築をすませておいてください🙇‍♂️
https://firebase.flutter.dev/docs/cli

iOSはFireStoreへの接続を高速化するためにこちらのコードをPodfileに追加しております。
Flutter3.0.3の場合は、9.3.0を使うみたいです。

Podfile

pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => '9.3.0'

コードを分割しないと見づらいので、機能は少ないですがフォルダ構成をこんな感じにしました。

lib/
├── firebase_options.dart
├── generated_plugin_registrant.dart
├── main.dart
├── provider
│   └── controller.dart
└── state
    └── state.dart

必要なパッケージをインストール

pubspec.yaml

name: hook_tutorial
description: A new Flutter project.

# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1

environment:
  sdk: ">=2.17.3 <3.0.0"

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  flutter_hooks: ^0.18.5+1
  hooks_riverpod: ^1.0.4
  firebase_core: ^1.20.0
  cloud_firestore: ^3.4.2

dev_dependencies:
  flutter_test:
    sdk: flutter

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^2.0.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

状態を監視して管理するプロバイダを作成

state/state.dart

// ref.read()で呼び出すメソッドを書くクラスを作成
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

// データ型は同じにしないと例外処理が発生する
// なので、dynamicで統一した。document.idが使えないため!
final postStateProvider = StateNotifierProvider<PostState, dynamic>((ref) {
  return PostState();
});
// 状態を変更するロジックが書かれたクラス
// 今回だと、追加と削除ですね🎓
// クラスをdynamic型にして、上のproviderの方もdynamicにする。Stringにできなかった!
class PostState extends StateNotifier<dynamic> {
  PostState() : super('');
  // メソッドに_をつけてしまうと読み込めなくなるので、つけない
  // addPostといった感じで描く!
  void addPost(dynamic title, dynamic message) async {
    await FirebaseFirestore.instance
        .collection('posts')
        .add({'title': title, 'message': message});
  }

  void deletePost(dynamic document) async {
    await FirebaseFirestore.instance
        .collection('posts')
        .doc(document.id)
        .delete();
  }
}

どこからでもアクセスできるプロバイダを作成

provider/controller.dart

// グローバルに呼び出すプロバイダーを定義
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

// タイトルを入れるプロバイダー
final titleProvider = StateProvider<String>((ref) {
  return '';
});
// メッセージを入れるプロバイダー
final messageProvider = StateProvider<String>((ref) {
  return '';
});
// FireStoreの'posts'コレクションのすべてのドキュメントを取得するプロバイダー。初回に全件分、あとは変更があるたびStreamに通知される。
final firebasePostsProvider = StreamProvider.autoDispose((_) {
  return FirebaseFirestore.instance.collection('posts').snapshots();
});

アプリの機能を使うファイル

機能が少ないので、main.dartにまとめました🙇‍♂️
TextEditingControllerと同じ機能があるflutter_hooksの機能であるuseTextEditingControllerを使って、フォームの入力されたデータを変数に保存して、FireStoreに渡しています。

APIリファレンス

https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTextEditingController-constant.html

main.dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hook_tutorial/firebase_options.dart';
import 'package:hook_tutorial/provider/controller.dart';
import 'package:hook_tutorial/state/state.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

  runApp(
    const ProviderScope(child: MyApp()),
  );
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const PostPage(),
    );
  }
}

class PostPage extends HookConsumerWidget {
  const PostPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final title = ref.watch(titleProvider);
    final message = ref.watch(messageProvider);
    final AsyncValue<QuerySnapshot> firebasePosts =
        ref.watch(firebasePostsProvider);

    // flutter_hooksのコード
    final titleController = useTextEditingController(text: title);
    final messageController = useTextEditingController(text: message);

    return Scaffold(
      appBar: AppBar(
        title: const Text('POST'),
      ),
      body: SafeArea(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextFormField(
              controller: titleController,
              onChanged: (value) {
                print('🔍titleをデバッグ: $value');
                ref.watch(titleProvider.notifier).state = value;
              },
            ),
            TextFormField(
              controller: messageController,
              onChanged: (value) {
                print('🔍messageをデバッグ: $value');
                ref.watch(messageProvider.notifier).state = value;
              },
            ),
            SizedBox(height: 20),
            ElevatedButton(
                onPressed: () async {
                  // 追加するメソッドを呼び出す
                  ref.read(postStateProvider.notifier).addPost(title, message);
                },
                child: Text('送信')),
            firebasePosts.when(
              // データがあった(データはqueryの中にある)
              data: (QuerySnapshot query) {
                // post内のドキュメントをリストで表示する
                return Expanded(
                  child: ListView(
                    // post内のドキュメント1件ずつをCard枠を付けたListTileのListとしてListViewのchildrenとする
                    children: query.docs.map((document) {
                      return Card(
                        child: ListTile(
                          // postで送った内容を表示する
                          title: Text(document['title']),
                          subtitle: Text(document['message']),
                          // postで保存した内容を削除する
                          trailing: IconButton(
                            onPressed: () async {
                              ref.read(postStateProvider.notifier).deletePost(document);
                            },
                            icon: const Icon(Icons.delete),
                          ),
                        ),
                      );
                    }).toList(),
                  ),
                );
              },

              // データの読み込み中(FireStoreではあまり発生しない)
              loading: () {
                return const Text('Loading');
              },

              // エラー(例外発生)時
              error: (e, stackTrace) {
                return Text('error: $e');
              },
            )
          ],
        ),
      ),
    );
  }
}

スクリーンショット

やって見た感想

今回使ってみたChangeNotifireProviderは、以前は全然わからなかったのですが、試行錯誤して使ってみたら、カウンターアプリ以外で始めてロジックを理解できた気がします。
以前は人のソースコードを見て真似してたのですが、自分で考えるとうまく機能実装ができない悩みがありました。😱
今回はドキュメントを見ながら人のソースコードを参考にしましたが、自分で考えたコードを書いて動いたので、初心者から脱することができたかもしれないです😇
まだまだ、わからないことだらけで勉強中です。

Discussion