form_validation
Dart/Flutter向けの包括的なフォームバリデーションライブラリ。動的なバリデーション、カスタムバリデータ、国際化対応を提供
概要
form_validationは、Dart/Flutter アプリケーション向けの強力で柔軟なフォームバリデーションライブラリです。シンプルなAPIを提供し、文字列、数値、メールアドレスなど、さまざまなデータ型の検証をサポートします。
主な特徴
- 🔍 包括的なバリデータセット - メール、電話番号、URL、クレジットカードなど
- 🎨 Flutterフォームとの統合 - TextFormFieldとシームレスに連携
- 🔧 カスタムバリデータ - 独自のバリデーションロジックを簡単に実装
- 🌍 国際化対応 - 多言語エラーメッセージのサポート
- ⚡ 高パフォーマンス - 軽量で高速な検証処理
- 📱 クロスプラットフォーム - iOS、Android、Web、デスクトップに対応
インストール
pubspec.yamlに追加
dependencies:
form_validation: ^1.0.0
パッケージのインストール
flutter pub get
基本的な使い方
シンプルなフォームバリデーション
import 'package:flutter/material.dart';
import 'package:form_validation/form_validation.dart';
class LoginForm extends StatefulWidget {
@override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
// メールアドレスフィールド
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'メールアドレス',
hintText: '[email protected]',
),
validator: ValidationBuilder()
.required('メールアドレスを入力してください')
.email('有効なメールアドレスを入力してください')
.build(),
),
// パスワードフィールド
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'パスワード',
),
validator: ValidationBuilder()
.required('パスワードを入力してください')
.minLength(8, 'パスワードは8文字以上必要です')
.maxLength(20, 'パスワードは20文字以内にしてください')
.build(),
),
// 送信ボタン
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// バリデーション成功時の処理
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('ログイン処理を開始します')),
);
}
},
child: Text('ログイン'),
),
],
),
);
}
}
高度な使い方
カスタムバリデータの作成
// カスタムバリデータクラス
class CustomValidators {
// 日本の郵便番号バリデータ
static String? japanesePostalCode(String? value) {
if (value == null || value.isEmpty) {
return null;
}
final postalCodeRegex = RegExp(r'^\d{3}-?\d{4}$');
if (!postalCodeRegex.hasMatch(value)) {
return '有効な郵便番号を入力してください(例:123-4567)';
}
return null;
}
// 日本の電話番号バリデータ
static String? japanesePhoneNumber(String? value) {
if (value == null || value.isEmpty) {
return null;
}
final phoneRegex = RegExp(r'^0\d{1,4}-?\d{1,4}-?\d{4}$');
if (!phoneRegex.hasMatch(value)) {
return '有効な電話番号を入力してください';
}
return null;
}
}
// 使用例
TextFormField(
decoration: InputDecoration(labelText: '郵便番号'),
validator: (value) {
// 複数のバリデータを組み合わせる
final required = ValidationBuilder()
.required('郵便番号を入力してください')
.build()(value);
if (required != null) return required;
return CustomValidators.japanesePostalCode(value);
},
)
ValidationBuilderの拡張
// ValidationBuilderの拡張
extension ValidationExtensions on ValidationBuilder {
ValidationBuilder japanesePostalCode([String? message]) {
return add((value) {
if (value == null || value.isEmpty) return null;
final regex = RegExp(r'^\d{3}-?\d{4}$');
if (!regex.hasMatch(value)) {
return message ?? '有効な郵便番号を入力してください';
}
return null;
});
}
ValidationBuilder strongPassword([String? message]) {
return add((value) {
if (value == null || value.isEmpty) return null;
// 大文字、小文字、数字、特殊文字を含む
final hasUpperCase = value.contains(RegExp(r'[A-Z]'));
final hasLowerCase = value.contains(RegExp(r'[a-z]'));
final hasDigits = value.contains(RegExp(r'[0-9]'));
final hasSpecialCharacters = value.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'));
if (!(hasUpperCase && hasLowerCase && hasDigits && hasSpecialCharacters)) {
return message ?? 'パスワードは大文字、小文字、数字、特殊文字を含む必要があります';
}
return null;
});
}
}
// 使用例
TextFormField(
validator: ValidationBuilder()
.required()
.minLength(8)
.strongPassword()
.build(),
)
動的バリデーション
class DynamicValidationForm extends StatefulWidget {
@override
_DynamicValidationFormState createState() => _DynamicValidationFormState();
}
class _DynamicValidationFormState extends State<DynamicValidationForm> {
bool _requirePhoneNumber = false;
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
// チェックボックス
CheckboxListTile(
title: Text('電話番号を入力する'),
value: _requirePhoneNumber,
onChanged: (value) {
setState(() {
_requirePhoneNumber = value ?? false;
// フォームを再検証
_formKey.currentState?.validate();
});
},
),
// 電話番号フィールド(条件付きバリデーション)
TextFormField(
decoration: InputDecoration(
labelText: '電話番号',
enabled: _requirePhoneNumber,
),
validator: (value) {
if (!_requirePhoneNumber) {
return null; // バリデーションをスキップ
}
return ValidationBuilder()
.required('電話番号を入力してください')
.phone('有効な電話番号を入力してください')
.build()(value);
},
),
],
),
);
}
}
非同期バリデーション
class AsyncValidationExample extends StatelessWidget {
// メールアドレスの重複チェック(サーバーAPI呼び出し)
Future<String?> checkEmailAvailability(String email) async {
// 実際のAPIリクエストをシミュレート
await Future.delayed(Duration(seconds: 1));
// デモ用:特定のメールアドレスが既に使用されている場合
final usedEmails = ['[email protected]', '[email protected]'];
if (usedEmails.contains(email)) {
return 'このメールアドレスは既に使用されています';
}
return null;
}
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: InputDecoration(
labelText: 'メールアドレス',
suffixIcon: _isChecking
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: null,
),
validator: (value) {
// 同期的なバリデーション
final syncError = ValidationBuilder()
.required()
.email()
.build()(value);
if (syncError != null) {
return syncError;
}
// 非同期バリデーションはonChangedで処理
return _asyncError;
},
onChanged: (value) async {
if (value.isNotEmpty && EmailValidator.validate(value)) {
setState(() => _isChecking = true);
final error = await checkEmailAvailability(value);
setState(() {
_isChecking = false;
_asyncError = error;
});
}
},
);
}
}
Flutter統合
フォームフィールドのラッパー
class ValidatedTextFormField extends StatelessWidget {
final String label;
final TextEditingController? controller;
final List<Validator> validators;
final TextInputType? keyboardType;
final bool obscureText;
const ValidatedTextFormField({
Key? key,
required this.label,
this.controller,
this.validators = const [],
this.keyboardType,
this.obscureText = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
keyboardType: keyboardType,
obscureText: obscureText,
decoration: InputDecoration(
labelText: label,
border: OutlineInputBorder(),
filled: true,
fillColor: Colors.grey[100],
),
validator: (value) {
for (final validator in validators) {
final error = validator(value);
if (error != null) {
return error;
}
}
return null;
},
);
}
}
// 使用例
ValidatedTextFormField(
label: 'ユーザー名',
validators: [
RequiredValidator(errorText: '必須項目です'),
MinLengthValidator(3, errorText: '3文字以上入力してください'),
PatternValidator(r'^[a-zA-Z0-9]+$', errorText: '英数字のみ使用可能です'),
],
)
フォーム状態の管理
class FormStateManager extends ChangeNotifier {
final Map<String, String?> _errors = {};
final Map<String, dynamic> _values = {};
void setValue(String field, dynamic value) {
_values[field] = value;
notifyListeners();
}
void setError(String field, String? error) {
_errors[field] = error;
notifyListeners();
}
String? getError(String field) => _errors[field];
dynamic getValue(String field) => _values[field];
bool get hasErrors => _errors.values.any((error) => error != null);
void validateField(String field, List<Validator> validators) {
final value = _values[field];
String? error;
for (final validator in validators) {
error = validator(value?.toString());
if (error != null) break;
}
setError(field, error);
}
void validateAll(Map<String, List<Validator>> fieldValidators) {
fieldValidators.forEach((field, validators) {
validateField(field, validators);
});
}
void reset() {
_errors.clear();
_values.clear();
notifyListeners();
}
}
エラーメッセージのカスタマイズ
多言語対応
class ValidationMessages {
static Map<String, Map<String, String>> _messages = {
'ja': {
'required': '必須項目です',
'email': '有効なメールアドレスを入力してください',
'minLength': '最低{min}文字入力してください',
'maxLength': '最大{max}文字までです',
'pattern': '正しい形式で入力してください',
'numeric': '数値を入力してください',
},
'en': {
'required': 'This field is required',
'email': 'Please enter a valid email address',
'minLength': 'Must be at least {min} characters',
'maxLength': 'Must be no more than {max} characters',
'pattern': 'Please enter in the correct format',
'numeric': 'Please enter a numeric value',
},
};
static String get(String key, String locale, [Map<String, dynamic>? params]) {
final message = _messages[locale]?[key] ?? _messages['en']?[key] ?? key;
if (params != null) {
return message.replaceAllMapped(
RegExp(r'{(\w+)}'),
(match) => params[match.group(1)!]?.toString() ?? match.group(0)!,
);
}
return message;
}
}
// カスタムバリデータでの使用
class LocalizedValidators {
final String locale;
LocalizedValidators(this.locale);
String? required(String? value) {
if (value == null || value.isEmpty) {
return ValidationMessages.get('required', locale);
}
return null;
}
String? minLength(String? value, int min) {
if (value != null && value.length < min) {
return ValidationMessages.get('minLength', locale, {'min': min});
}
return null;
}
}
ベストプラクティス
1. バリデーションロジックの分離
// validators/user_validators.dart
class UserValidators {
static final username = ValidationBuilder()
.required('ユーザー名を入力してください')
.minLength(3, 'ユーザー名は3文字以上必要です')
.maxLength(20, 'ユーザー名は20文字以内にしてください')
.regExp(r'^[a-zA-Z0-9_]+$', '英数字とアンダースコアのみ使用可能です')
.build();
static final email = ValidationBuilder()
.required('メールアドレスを入力してください')
.email('有効なメールアドレスを入力してください')
.build();
static final password = ValidationBuilder()
.required('パスワードを入力してください')
.minLength(8, 'パスワードは8文字以上必要です')
.add((value) {
// カスタムバリデーション
if (!RegExp(r'^(?=.*[A-Za-z])(?=.*\d)').hasMatch(value!)) {
return 'パスワードは英字と数字を含む必要があります';
}
return null;
})
.build();
}
2. エラー表示のカスタマイズ
class CustomErrorText extends StatelessWidget {
final String? errorText;
const CustomErrorText({Key? key, this.errorText}) : super(key: key);
@override
Widget build(BuildContext context) {
if (errorText == null || errorText!.isEmpty) {
return SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
Icon(Icons.error_outline, color: Colors.red, size: 16),
SizedBox(width: 4),
Expanded(
child: Text(
errorText!,
style: TextStyle(color: Colors.red, fontSize: 12),
),
),
],
),
);
}
}
3. リアルタイムバリデーション
class RealtimeValidationField extends StatefulWidget {
final String label;
final List<Validator> validators;
final ValueChanged<String>? onValid;
const RealtimeValidationField({
Key? key,
required this.label,
required this.validators,
this.onValid,
}) : super(key: key);
@override
_RealtimeValidationFieldState createState() => _RealtimeValidationFieldState();
}
class _RealtimeValidationFieldState extends State<RealtimeValidationField> {
final _controller = TextEditingController();
String? _error;
bool _touched = false;
void _validate(String value) {
if (!_touched && value.isEmpty) return;
_touched = true;
String? error;
for (final validator in widget.validators) {
error = validator(value);
if (error != null) break;
}
setState(() => _error = error);
if (error == null && widget.onValid != null) {
widget.onValid!(value);
}
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _controller,
onChanged: _validate,
decoration: InputDecoration(
labelText: widget.label,
errorText: _error,
suffixIcon: _touched && _error == null
? Icon(Icons.check_circle, color: Colors.green)
: null,
),
),
],
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
トラブルシューティング
よくある問題と解決方法
-
バリデーションが機能しない
// 問題: FormStateが正しく初期化されていない // 解決方法: final _formKey = GlobalKey<FormState>(); // これを忘れずに Form( key: _formKey, // keyを設定 // ... ) -
カスタムバリデータが呼ばれない
// 問題: nullチェックが適切でない // 解決方法: String? customValidator(String? value) { if (value == null || value.isEmpty) { return null; // 空の場合はnullを返す(requiredバリデータに任せる) } // カスタムロジック } -
非同期バリデーションのタイミング
// 問題: 入力のたびにAPIが呼ばれる // 解決方法: デバウンスを使用 Timer? _debounce; void _onChanged(String value) { if (_debounce?.isActive ?? false) _debounce!.cancel(); _debounce = Timer(const Duration(milliseconds: 500), () { // バリデーション実行 }); }
パフォーマンスの最適化
// メモ化されたバリデータ
class MemoizedValidator {
final Map<String, String?> _cache = {};
final String? Function(String?) validator;
MemoizedValidator(this.validator);
String? call(String? value) {
final key = value ?? '';
if (_cache.containsKey(key)) {
return _cache[key];
}
final result = validator(value);
_cache[key] = result;
return result;
}
void clear() => _cache.clear();
}