StateNotifireProviderとは?
なんとなくわかってきた気がする?
今流行りのriverpodの機能であるStateNotifireProviderをドキュメントを見ながら、使ってみた。
公式ドキュメント
ドキュメントを読んでみると、状態の変化を監視しユーザーの操作により変化する状態を管理するものみたいですね。この辺がビジネスロジックを理解できるIT歴が長い人とそうでない人の差が現れますね😅
こちらが完成品です。参考までにどうぞ
Firebase 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リファレンス
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