formz
DartとFlutterのための統一されたフォーム表現とバリデーション。状態管理と検証ロジックを分離し、BlocやCubitとシームレスに統合できるフォームバリデーションライブラリ
formz
概要
formzは、Dartにおける統一されたフォーム表現を提供するライブラリです。フォームの表現とバリデーションを汎用的な方法で簡素化することを目的としており、Very Good Venturesによって開発されています。
主な特徴
1. 宣言的なフォームバリデーション
FormzInputを拡張してカスタムバリデーターを作成し、型安全な方法でフォームの状態を管理できます。
2. Pure/Dirtyステート管理
- Pure: 未変更のフォーム入力を表現
- Dirty: 変更されたフォーム入力を表現
3. エラー表示の制御
displayErrorゲッターを使用して、ユーザーが入力を変更した後にのみエラーを表示できます。
4. パフォーマンス最適化
FormzInputErrorCacheMixinを使用して、高コストなバリデーション結果をキャッシュできます。
基本的な使い方
FormzInputの作成
import 'package:formz/formz.dart';
// バリデーションエラーを定義
enum NameInputError { empty }
// FormzInputを拡張して入力型とエラー型を提供
class NameInput extends FormzInput<String, NameInputError> {
// super.pureを呼び出して未変更のフォーム入力を表現
const NameInput.pure() : super.pure('');
// super.dirtyを呼び出して変更されたフォーム入力を表現
const NameInput.dirty({String value = ''}) : super.dirty(value);
// validatorをオーバーライドして入力値の検証を処理
@override
NameInputError? validator(String value) {
return value.isEmpty ? NameInputError.empty : null;
}
}
FormzInputの操作
const name = NameInput.pure();
print(name.value); // ''
print(name.isValid); // false
print(name.error); // NameInputError.empty
print(name.displayError); // null
const joe = NameInput.dirty(value: 'joe');
print(joe.value); // 'joe'
print(joe.isValid); // true
print(joe.error); // null
print(joe.displayError); // null
高度な使用方法
複数のFormzInputの検証
const validInputs = <FormzInput>[
NameInput.dirty(value: 'jan'),
NameInput.dirty(value: 'jen'),
NameInput.dirty(value: 'joe'),
];
print(Formz.validate(validInputs)); // true
const invalidInputs = <FormzInput>[
NameInput.dirty(),
NameInput.dirty(),
NameInput.dirty(),
];
print(Formz.validate(invalidInputs)); // false
FormzMixinを使用した自動検証
class LoginForm with FormzMixin {
LoginForm({
this.username = const Username.pure(),
this.password = const Password.pure(),
});
final Username username;
final Password password;
@override
List<FormzInput> get inputs => [username, password];
}
void main() {
print(LoginForm().isValid); // false
}
パフォーマンス最適化のためのキャッシュ
import 'package:formz/formz.dart';
enum EmailValidationError { invalid }
class Email extends FormzInput<String, EmailValidationError>
with FormzInputErrorCacheMixin {
Email.pure([super.value = '']) : super.pure();
Email.dirty([super.value = '']) : super.dirty();
static final _emailRegExp = RegExp(
r'^[a-zA-Z\d.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z\d-]+(?:\.[a-zA-Z\d-]+)*$',
);
@override
EmailValidationError? validator(String value) {
return _emailRegExp.hasMatch(value) ? null : EmailValidationError.invalid;
}
}
BlocやCubitとの統合
FormzSubmissionStatus
フォームの送信状態を管理するための列挙型:
enum FormzSubmissionStatus {
initial, // フォームがまだ送信されていない
inProgress, // フォームが送信中
success, // フォームが正常に送信された
failure, // フォームの送信に失敗した
canceled // フォームの送信がキャンセルされた
}
Cubitでの実装例
class SignUpState with FormzMixin {
const SignUpState({
this.email = const Email.pure(),
this.password = const Password.pure(),
this.status = FormzSubmissionStatus.initial,
});
final Email email;
final Password password;
final FormzSubmissionStatus status;
@override
List<FormzInput> get inputs => [email, password];
SignUpState copyWith({
Email? email,
Password? password,
FormzSubmissionStatus? status,
}) {
return SignUpState(
email: email ?? this.email,
password: password ?? this.password,
status: status ?? this.status,
);
}
}
class SignUpCubit extends Cubit<SignUpState> {
SignUpCubit(this._authRepository) : super(const SignUpState());
final AuthRepository _authRepository;
void emailChanged(String value) {
final email = Email.dirty(value);
emit(state.copyWith(
email: email,
status: FormzSubmissionStatus.initial,
));
}
void passwordChanged(String value) {
final password = Password.dirty(value);
emit(state.copyWith(
password: password,
status: FormzSubmissionStatus.initial,
));
}
Future<void> signUpFormSubmitted() async {
if (!state.isValid) return;
emit(state.copyWith(status: FormzSubmissionStatus.inProgress));
try {
await _authRepository.signUp(
email: state.email.value,
password: state.password.value,
);
emit(state.copyWith(status: FormzSubmissionStatus.success));
} catch (_) {
emit(state.copyWith(status: FormzSubmissionStatus.failure));
}
}
}
UIでの使用例
class SignUpForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocListener<SignUpCubit, SignUpState>(
listener: (context, state) {
if (state.status.isSuccess) {
Navigator.of(context).pop();
} else if (state.status.isFailure) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
const SnackBar(content: Text('サインアップに失敗しました')),
);
}
},
child: Column(
children: [
_EmailInput(),
const SizedBox(height: 8),
_PasswordInput(),
const SizedBox(height: 8),
_SignUpButton(),
],
),
);
}
}
class _SignUpButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<SignUpCubit, SignUpState>(
builder: (context, state) {
return state.status.isInProgress
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: state.isValid
? () => context.read<SignUpCubit>().signUpFormSubmitted()
: null,
child: const Text('サインアップ'),
);
},
);
}
}
ベストプラクティス
1. カスタムバリデーターの作成
各入力フィールドに対して専用のFormzInputクラスを作成し、再利用可能なバリデーションロジックを実装します。
2. エラーメッセージの国際化
エラーの表示をUIレイヤーで処理し、多言語対応を実現します:
String? getErrorText(NameInputError? error) {
switch (error) {
case NameInputError.empty:
return '名前を入力してください';
case null:
return null;
}
}
3. 複雑なバリデーション
複数のフィールドに依存するバリデーションは、FormzMixinを使用したフォームクラスで実装します:
class PasswordForm with FormzMixin {
PasswordForm({
this.password = const Password.pure(),
this.confirmedPassword = const ConfirmedPassword.pure(),
});
final Password password;
final ConfirmedPassword confirmedPassword;
@override
List<FormzInput> get inputs => [password, confirmedPassword];
// パスワードの一致を確認
bool get passwordsMatch =>
password.value == confirmedPassword.value;
}
4. 非同期バリデーション
APIを使用したバリデーション(例:メールアドレスの重複チェック)は、Cubit/Blocレイヤーで処理します。
まとめ
formzは、Flutterアプリケーションでフォームバリデーションを実装するための強力で柔軟なソリューションを提供します。BlocやCubitとの統合により、複雑なフォームの状態管理も簡潔に実装できます。型安全性と再利用性を重視した設計により、大規模なアプリケーションでも保守しやすいコードを実現できます。