画面とロジックを分ける
Riverpodではモデルクラス以外も使えるのあるんだよ!
最近、僕のコードの書き方が、UIとロジックを分けるのが中途半端でかっこよくないと思い、Riverpod2.0で非推奨になったコードを使わずに、推奨されているNotifierを使って、エラーメッセージをUIに表示するだけですが、やってみました。
フォルダ構成
doc_notifierというフォルダは、実験用に作ったコードが配置されています。使っているのは、エラーメッセージを保持するクラスがあるmodelフォルダ、UIを表示するscreenフォルダ、画面に渡す状態が書かれているstoreフォルダ、他の機能と連携してないファイルを配置しているcommonフォルダに分けてファイル管理をしています。
lib/
├── common
│ └── form_provider.dart
├── doc_notifier
│ ├── notifier_class.dart
│ └── state_class.dart
├── main.dart
├── model
│ └── error_msg.dart
├── screen
│ └── form_example_notifier.dart
└── store
└── form_validator.dart
状態を保持するクラス
これだけしか書いてないですが、これがエラーメッセージを表示するのに必要なクラスです。Riverpod使うときは、Freezedでモデルクラスを使ってただけなんですけど、知り合いの人でアーキテクチャの設計にこだわっている人を見て、状態を保持するクラスを使っているのを見て、画面に状態を渡す方法を今回学習してみました。
// ErrorMsgStateは、名前とメールアドレスのエラーメッセージを保持するクラスです。
class ErrorMsgState {
final String? nameError;
final String? emailError;
ErrorMsgState({this.nameError, this.emailError});
}
状態を管理するクラス
Riverpod2.0から非推奨になったStateProviderとStateNotifierの代わりに使われるようになったNotifierを使用して状態管理をします。入力に使うTextEditingControllerもこちらを使用します。
import 'package:flutter_new/model/error_msg.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// FormValidatorNotifierは、ErrorMsgStateを使用して、名前とメールアドレスのエラーメッセージを保持します。
class FormValidatorNotifier extends Notifier<ErrorMsgState> {
build() {
return ErrorMsgState();
}
void validate({String? name, String? email}) {
String? nameError;
String? emailError;
if (name == null || name.isEmpty) {
nameError = "名前が入力されていません";
}
if (email == null || email.isEmpty) {
emailError = "メールアドレスが入力されていません";
}
state = ErrorMsgState(nameError: nameError, emailError: emailError);
}
}
final formValidatorNotifierProvider =
NotifierProvider<FormValidatorNotifier, ErrorMsgState>(
FormValidatorNotifier.new);
TextEditingControllerはこちらのcommonフォルダに、配置して状態管理をしています。今後は、このような書き方が推奨されるみたいですね。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// NameNotifierは、名前を保持します。
class NameNotifier extends Notifier<TextEditingController> {
build() {
return TextEditingController(text: 'NameNotifier');
}
}
// EmailNotifierは、メールアドレスを保持します。
class EmailNotifier extends Notifier<TextEditingController> {
build() {
return TextEditingController(text: 'EmailNotifier');
}
}
// nameNotifierProviderは、NameNotifierを使用して、名前を保持します。
final nameNotifierProvider = NotifierProvider<NameNotifier, TextEditingController>(NameNotifier.new);
// emailNotifierProviderは、EmailNotifierを使用して、メールアドレスを保持します。
final emailNotifierProvider = NotifierProvider<EmailNotifier, TextEditingController>(EmailNotifier.new);
UIでエラーメッセージを表示する
こちらは、状態を渡して画面にスナックバーを表示するファイルです。
// ConsumerWidget
// ignore: must_be_immutable
import 'package:flutter/material.dart';
import 'package:flutter_new/common/form_provider.dart';
import 'package:flutter_new/store/form_validator.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class FormExampleNotifier extends ConsumerWidget {
final _formKey = GlobalKey<FormState>();
FormExampleNotifier({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// nameNotifierProviderとemailNotifierProviderを使用して、名前とメールアドレスを取得します。
final name = ref.watch(nameNotifierProvider);
final email = ref.watch(emailNotifierProvider);
// formValidatorNotifierProviderを使用して、ErrorMsgStateを取得します。
ref.listen(formValidatorNotifierProvider, (previous, next) {
if (next.nameError != null) {
// nameErrorがnullでない場合は、SnackBarを表示します。
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(next.nameError.toString()),
),
);
} else if (next.emailError != null) {
// emailErrorがnullでない場合は、SnackBarを表示します。
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(next.emailError.toString()),
),
);
} else {
// 入力が完了した場合は、SnackBarを表示します。
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Notifierで送信完了'),
),
);
}
});
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepPurple,
title: const Text('Form Example Notifier'),
),
body: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 300,
child: TextFormField(
controller: name,
decoration: InputDecoration(labelText: '名前'),
),
),
const SizedBox(height: 20),
Container(),
SizedBox(
width: 300,
child: TextFormField(
controller: email,
decoration: InputDecoration(labelText: 'メールアドレス'),
),
),
ElevatedButton(
onPressed: () {
ref
.read(formValidatorNotifierProvider.notifier)
.validate(name: name.text, email: email.text);
},
child: Text('送信'),
),
],
),
),
);
}
}
アプリを実行してみる
import 'package:flutter/material.dart';
import 'package:flutter_new/screen/form_example_notifier.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: FormExampleNotifier(),
);
}
}
実行結果
ref.listenを使用して、スナックバーでメッセージを表示します。入力がされていないと、エラーを表示、送信に成功したら、送信に成功したメッセージを表示します。
まとめ
今回作ったサンプルですけど、MVVMと呼べるものではないです😅
新しいプロバイダーを学習するために作った学習用のサンプルです。完成品のコードもこちらに公開しておきます。
Discussion