reactive_forms
Flutterアプリケーション向けのモデル駆動型リアクティブフォームバリデーションライブラリ
import { Callout } from "@/components/Callout";
概要
reactive_formsは、Flutterアプリケーション向けの強力なモデル駆動型フォームバリデーションライブラリです。Angular Reactive Formsに強く影響を受けており、時間とともに変化するフォーム入力値を効率的に処理するためのリアクティブなアプローチを提供します。
このライブラリは、ウィジェットの形状、色、アニメーションの決定権を開発者に委ねながら、データの収集と検証の責任から解放します。
主な特徴
1. モデル駆動アプローチ
フォームの構造とバリデーションロジックをウィジェットツリーから分離し、テスタブルで再利用可能なコードを実現します。
2. リアクティブプログラミング
RxDartベースのストリームを使用して、フォーム状態の変更をリアルタイムで監視・反応します。
3. 豊富な組み込みバリデータ
必須入力、メール形式、パターンマッチングなど、20種類以上の組み込みバリデータを提供します。
4. 双方向データバインディング
FormControlとUIウィジェット間の自動的な双方向データ同期を実現します。
インストール
dependencies:
reactive_forms: ^17.0.0
基本的な使い方
1. フォームの定義
import 'package:reactive_forms/reactive_forms.dart';
class LoginForm {
final form = FormGroup({
'email': FormControl<String>(
validators: [
Validators.required,
Validators.email,
],
),
'password': FormControl<String>(
validators: [
Validators.required,
Validators.minLength(8),
],
),
});
FormControl<String> get emailControl =>
form.control('email') as FormControl<String>;
FormControl<String> get passwordControl =>
form.control('password') as FormControl<String>;
}
2. UIの実装
class LoginScreen extends StatelessWidget {
final loginForm = LoginForm();
@override
Widget build(BuildContext context) {
return ReactiveForm(
formGroup: loginForm.form,
child: Column(
children: [
ReactiveTextField<String>(
formControlName: 'email',
decoration: InputDecoration(
labelText: 'メールアドレス',
errorText: 'メールアドレスが無効です',
),
validationMessages: {
ValidationMessage.required: (_) => '必須項目です',
ValidationMessage.email: (_) => '有効なメールアドレスを入力してください',
},
),
ReactiveTextField<String>(
formControlName: 'password',
obscureText: true,
decoration: InputDecoration(
labelText: 'パスワード',
),
validationMessages: {
ValidationMessage.required: (_) => '必須項目です',
ValidationMessage.minLength: (_) => '8文字以上入力してください',
},
),
ReactiveFormConsumer(
builder: (context, form, child) {
return ElevatedButton(
onPressed: form.valid ? () => _login(form) : null,
child: Text('ログイン'),
);
},
),
],
),
);
}
void _login(FormGroup form) {
final values = form.value;
print('Email: ${values['email']}');
print('Password: ${values['password']}');
}
}
バリデータ
組み込みバリデータ
// 必須
Validators.required
// メール
Validators.email
// 数値範囲
Validators.min(0)
Validators.max(100)
// 文字列長
Validators.minLength(3)
Validators.maxLength(50)
// パターン
Validators.pattern(r'^[0-9]+$')
// 比較
Validators.equals('value')
// 複合バリデータ
Validators.compose([
Validators.required,
Validators.minLength(8),
Validators.pattern(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)'),
])
カスタムバリデータ
Map<String, dynamic>? strongPassword(AbstractControl<dynamic> control) {
final value = control.value as String?;
if (value == null || value.isEmpty) {
return null; // 他のバリデータで処理
}
final hasUpperCase = value.contains(RegExp(r'[A-Z]'));
final hasLowerCase = value.contains(RegExp(r'[a-z]'));
final hasDigit = value.contains(RegExp(r'[0-9]'));
final hasSpecialChar = value.contains(RegExp(r'[!@#$%^&*]'));
if (!hasUpperCase || !hasLowerCase || !hasDigit || !hasSpecialChar) {
return {'strongPassword': true};
}
return null;
}
// 使用例
final passwordControl = FormControl<String>(
validators: [Validators.required, strongPassword],
);
非同期バリデーション
Future<Map<String, dynamic>?> uniqueEmail(
AbstractControl<dynamic> control,
) async {
final email = control.value as String?;
if (email == null || email.isEmpty) {
return null;
}
// APIでメールアドレスの重複チェック
final response = await http.get(
Uri.parse('https://api.example.com/check-email?email=$email'),
);
if (response.statusCode == 200) {
final exists = json.decode(response.body)['exists'] as bool;
return exists ? {'uniqueEmail': true} : null;
}
return null;
}
// 使用例
final emailControl = FormControl<String>(
validators: [Validators.required, Validators.email],
asyncValidators: [uniqueEmail],
);
複雑なフォームの例
ネストされたフォームグループ
class UserRegistrationForm {
final form = FormGroup({
'personalInfo': FormGroup({
'firstName': FormControl<String>(
validators: [Validators.required],
),
'lastName': FormControl<String>(
validators: [Validators.required],
),
'birthDate': FormControl<DateTime>(
validators: [Validators.required],
),
}),
'contactInfo': FormGroup({
'email': FormControl<String>(
validators: [Validators.required, Validators.email],
),
'phone': FormControl<String>(
validators: [Validators.pattern(r'^\d{10,11}$')],
),
}),
'address': FormGroup({
'street': FormControl<String>(),
'city': FormControl<String>(),
'zipCode': FormControl<String>(
validators: [Validators.pattern(r'^\d{3}-\d{4}$')],
),
}),
'preferences': FormGroup({
'newsletter': FormControl<bool>(value: false),
'notifications': FormControl<bool>(value: true),
}),
});
}
動的フォーム配列
class SurveyForm {
final form = FormGroup({
'questions': FormArray<String>([]),
});
FormArray<String> get questions =>
form.control('questions') as FormArray<String>;
void addQuestion() {
questions.add(
FormControl<String>(validators: [Validators.required]),
);
}
void removeQuestion(int index) {
questions.removeAt(index);
}
}
// UI実装
class SurveyWidget extends StatelessWidget {
final surveyForm = SurveyForm();
@override
Widget build(BuildContext context) {
return ReactiveForm(
formGroup: surveyForm.form,
child: Column(
children: [
ReactiveFormArray<String>(
formArrayName: 'questions',
builder: (context, formArray, child) {
return Column(
children: [
for (int i = 0; i < formArray.controls.length; i++)
Row(
children: [
Expanded(
child: ReactiveTextField<String>(
formControl: formArray.controls[i] as FormControl<String>,
decoration: InputDecoration(
labelText: '質問 ${i + 1}',
),
),
),
IconButton(
icon: Icon(Icons.delete),
onPressed: () => surveyForm.removeQuestion(i),
),
],
),
],
);
},
),
ElevatedButton(
onPressed: surveyForm.addQuestion,
child: Text('質問を追加'),
),
],
),
);
}
}
条件付きバリデーション
class ConditionalForm {
final form = FormGroup({
'hasAccount': FormControl<bool>(value: false),
'accountNumber': FormControl<String>(),
});
ConditionalForm() {
// hasAccountがtrueの場合のみaccountNumberを必須にする
form.control('hasAccount').valueChanges.listen((hasAccount) {
final accountControl = form.control('accountNumber');
if (hasAccount == true) {
accountControl.setValidators([
Validators.required,
Validators.pattern(r'^\d{10}$'),
]);
} else {
accountControl.clearValidators();
}
accountControl.updateValueAndValidity();
});
}
}
グローバル設定
void main() {
runApp(
ReactiveFormConfig(
validationMessages: {
ValidationMessage.required: (error) => '必須項目です',
ValidationMessage.email: (error) => 'メールアドレスが無効です',
ValidationMessage.minLength: (error) =>
'${(error as Map)['requiredLength']}文字以上入力してください',
ValidationMessage.maxLength: (error) =>
'${(error as Map)['requiredLength']}文字以下で入力してください',
},
child: MyApp(),
),
);
}
高度な機能
フォーム状態の監視
// 値の変更を監視
form.valueChanges.listen((value) {
print('Form value changed: $value');
});
// 有効性の変更を監視
form.statusChanged.listen((status) {
print('Form status: $status');
});
// 特定のコントロールを監視
emailControl.valueChanges
.debounceTime(Duration(milliseconds: 300))
.listen((value) {
print('Email changed: $value');
});
フォームのリセットと初期化
// フォームをリセット
form.reset();
// 特定の値でリセット
form.reset(value: {
'email': '[email protected]',
'password': '',
});
// 初期値を設定
form.patchValue({
'email': '[email protected]',
});
パフォーマンス最適化
// 大規模フォームでのパフォーマンス最適化
ReactiveFormField<String, String>(
formControlName: 'largeText',
// リビルドを最小限に抑える
showErrors: (control) => control.dirty && control.touched,
builder: (field) {
return TextField(
onChanged: field.didChange,
decoration: InputDecoration(
errorText: field.errorText,
),
);
},
);
コード生成
reactive_forms_generatorを使用した型安全なフォーム生成:
// モデル定義
@ReactiveFormAnnotation()
class UserProfile {
final String name;
final String email;
final int age;
UserProfile({
@FormControlAnnotation(validators: [RequiredValidator()])
required this.name,
@FormControlAnnotation(validators: [RequiredValidator(), EmailValidator()])
required this.email,
@FormControlAnnotation(validators: [MinValidator(0), MaxValidator(120)])
required this.age,
});
}
// 生成されたフォームを使用
final form = UserProfileForm();
form.nameControl.value = 'John Doe';
まとめ
reactive_formsは、Flutterにおける複雑なフォーム処理を大幅に簡素化する強力なライブラリです。モデル駆動アプローチにより、テスタブルで保守性の高いコードを実現し、豊富な組み込み機能により開発効率を向上させます。