はじめに
formz はFlutterにおけるフォーム入力+バリデーションの管理を簡略化する為のライブラリです。
この記事では、formzとriverpodを使って簡単なフォームを実装してみます。
まずはフォーム用の画面を用意します。
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Padding(
padding: const EdgeInsets.all(32.0),
child: Center(
child: Column(
children: <Widget>[
TextField(
decoration: const InputDecoration(labelText: 'ユーザー名'),
keyboardType: TextInputType.emailAddress,
onChanged: (value) {
},
),
TextField(
decoration: const InputDecoration(labelText: 'パスワード'),
onChanged: (value) {
},
),
const SizedBox(
height: 20,
),
ElevatedButton(
onPressed: () {
},
child: const Text('Submit'),
),
],
),
),
),
),
);
}
}
FormzInputの作成
formzを使ったフォームの実装を行っていきます。
formzではFormzInputというクラスを継承してフォームを管理するモデルを作成します。
まずはREADMEに従って、FormzInputを用意します。
// ユーザー名入力用
class UserNameInput extends FormzInput<String, UserNameInputError> {
const UserNameInput.pure() : super.pure('');
const UserNameInput.dirty({String value = ''}) : super.dirty(value);
@override
UserNameInputError? validator(String value) {
return value.isEmpty ? UserNameInputError.empty : null;
}
}
// ユーザー名入力エラー用
enum UserNameInputError {
empty(errorText: '未入力です');
const UserNameInputError({required this.errorText});
final String errorText;
}
// パスワード入力用
class PasswordInput extends FormzInput<String, PasswordInputError> {
const PasswordInput.pure() : super.pure('');
const PasswordInput.dirty({String value = ''}) : super.dirty(value);
@override
PasswordInputError? validator(String value) {
if (value.length < 8) return PasswordInputError.tooShort;
if (!value.contains(RegExp(r'[0-9]'))) {
return PasswordInputError.noDigits;
}
if (!value.contains(RegExp(r'[A-Z]'))) {
return PasswordInputError.noUppercase;
}
if (!value.contains(RegExp(r'[a-z]'))) {
return PasswordInputError.noLowercase;
}
return null;
}
}
// パスワード入力エラー用
enum PasswordInputError {
tooShort(errorText: '8文字以上入力して下さい'),
noDigits(errorText: "数字が含まれていません"),
noUppercase(errorText: "アルファベットの大文字が含まれていません"),
noLowercase(errorText: "アルファベットの小文字が含まれていません");
const PasswordInputError({required this.errorText});
final String errorText;
}
FormzInput継承時の型指定で、そのフォームの入力値の型とエラーの型が決まります。
FormzInput<String, NameInputError>
一番目の型引数がフォーム入力値の型です。二番目がエラーの型で、これをバリデーションのエラー時に返却します。
コンストラクタには pure
と dirty
という2つのfactoryコンストラクタを用意します。
pure
はフォームが未入力状態を表すコンストラクタです。 super.pure('')
と親クラスのコンストラクタに値を渡すことでフォームの初期値を設定します。
dirty
はフォームの入力後を表すコンストラクタになります。
バリデーションのロジックは validator
メソッドをoverrideして記述します。
このメソッドの返却値の型は、継承時に指定したエラーの型をNull可にしたものになります。入力値が正常の場合にはnullを返却します。
RiverpodのNotifierProviderの作成
RiverpodのNotiferProviderを使ってフォームの状態管理を行います。
※Riverpodはv2のコード生成を利用しています。またフォーム全体をまとめるモデルクラスはfreezedを使っています
@freezed
class SampleForm with _$SampleForm {
factory SampleForm({
required UserNameInput userName,
required PasswordInput password,
required bool isValid,
}) = _SampleForm;
}
@riverpod
class SampleFormController extends _$SampleFormController {
@override
SampleForm build() {
return SampleForm(
userName: const UserNameInput.pure(),
password: const PasswordInput.pure(),
isValid: false);
}
void onChangeUserName(String value) {
final userName = UserNameInput.dirty(value: value);
state = state.copyWith(
userName: userName,
isValid: Formz.validate([
userName,
state.password,
]));
}
void onChangePassword(String value) {
final password = PasswordInput.dirty(value: value);
state = state.copyWith(
password: password,
isValid: Formz.validate([
password,
state.userName,
]));
}
Future<void> submit() async {
if (!state.isValid) return;
// submit処理...
}
NotiferProviderで、stateの初期状態を定義する build
メソッドでは、先程用意したFormzInputの pure
を使って、初期状態を未入力として返却しています。
return SampleForm(
userName: const UserNameInput.pure(),
password: const PasswordInput.pure(),
isValid: false);
また、入力を受け付けるメソッド、 onChangeUserName
と onChangePassword
では、 dirty
で入力済みのFormzInputを生成し、その後バリデーションを実行して結果を isValid
に代入しています。
final userName = UserNameInput.dirty(value: value);
state = state.copyWith(
userName: userName,
isValid: Formz.validate([
userName,
state.password,
]));
UIにバリデーションの結果を反映する
最後にフォームから入力を監視して、バリデーションの結果をUIに反映したいと思います。
MainAppは以下のようになりました。
class MainApp extends ConsumerWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final sampleForm = ref.watch(sampleFormControllerProvider);
return MaterialApp(
home: Scaffold(
body: Padding(
padding: const EdgeInsets.all(32.0),
child: Center(
child: Column(
children: <Widget>[
TextField(
decoration: const InputDecoration(labelText: 'ユーザー名'),
keyboardType: TextInputType.emailAddress,
onChanged: ref
.read(sampleFormControllerProvider.notifier)
.onChangeUserName,
),
if (sampleForm.userName.displayError != null)
Text(sampleForm.userName.displayError!.errorText,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
)),
TextField(
decoration: const InputDecoration(labelText: 'パスワード'),
onChanged: ref
.read(sampleFormControllerProvider.notifier)
.onChangePassword,
),
if (sampleForm.password.displayError != null)
Text(sampleForm.password.displayError?.errorText ?? '',
style: TextStyle(
color: Theme.of(context).colorScheme.error,
)),
const SizedBox(
height: 20,
),
ElevatedButton(
onPressed: sampleForm.isValid
? ref.read(sampleFormControllerProvider.notifier).submit
: null,
child: const Text('送信'),
),
],
),
),
),
),
);
}
}
- riverpodで入力を監視できるよう、
StatelessWidget
->ConsumerWidget
に変更しました。 - バリデーションエラーがあった場合だけエラーテキストを表示するようにしています。
if (sampleForm.userName.displayError != null) Text(sampleForm.userName.displayError?.errorText ?? '', style: TextStyle( color: Theme.of(context).colorScheme.error, )),
※displayErrorは dirty
の場合だけエラーを返却するので初期状態でエラーにならない
3. ボタンの活性/非活性状態を、 isValid
で判定するようにしています。
おわり
formz+riverpodを使った簡単なフォーム実装についてまとめてみました!