Flutter Form Builder
Flutterアプリケーションで高機能なフォームを簡単に構築するための包括的なフォームビルダーライブラリ
Flutter Form Builderは、Flutterアプリケーションでデータ収集フォームを構築するための強力なパッケージです。フォームの構築、フィールドの検証、変更への対応、最終的なユーザー入力の収集に必要な定型コードを大幅に削減します。
主な特徴
- 豊富なフォームフィールド: テキスト、日付、時刻、ドロップダウン、チェックボックスなど多様な入力フィールド
- 包括的な検証機能: 組み込みバリデータとカスタムバリデータのサポート
- 多言語対応: 50以上の言語でエラーメッセージをローカライズ
- 状態管理: フォームの状態を簡単に管理、アクセス
- カスタマイズ可能: 独自のカスタムフィールドを作成可能
インストール
dependencies:
flutter_form_builder: ^10.1.0
form_builder_validators: ^11.2.0 # バリデーション用
flutter pub add flutter_form_builder form_builder_validators
基本的な使用方法
シンプルなフォームの作成
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
class SimpleFormExample extends StatefulWidget {
const SimpleFormExample({Key? key}) : super(key: key);
@override
State<SimpleFormExample> createState() => _SimpleFormExampleState();
}
class _SimpleFormExampleState extends State<SimpleFormExample> {
final _formKey = GlobalKey<FormBuilderState>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flutter Form Builder')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: FormBuilder(
key: _formKey,
child: Column(
children: [
FormBuilderTextField(
name: 'email',
decoration: const InputDecoration(
labelText: 'メールアドレス',
prefixIcon: Icon(Icons.email),
),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(
errorText: 'メールアドレスは必須です',
),
FormBuilderValidators.email(
errorText: '有効なメールアドレスを入力してください',
),
]),
),
const SizedBox(height: 16),
FormBuilderTextField(
name: 'password',
decoration: const InputDecoration(
labelText: 'パスワード',
prefixIcon: Icon(Icons.lock),
),
obscureText: true,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(
errorText: 'パスワードは必須です',
),
FormBuilderValidators.minLength(
8,
errorText: 'パスワードは8文字以上である必要があります',
),
]),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
if (_formKey.currentState?.saveAndValidate() ?? false) {
debugPrint(_formKey.currentState?.value.toString());
// フォームデータを処理
} else {
debugPrint('バリデーションエラー');
}
},
child: const Text('送信'),
),
],
),
),
),
);
}
}
フォームフィールドの種類
テキスト入力フィールド
// 基本的なテキストフィールド
FormBuilderTextField(
name: 'name',
decoration: const InputDecoration(
labelText: '名前',
hintText: 'お名前を入力してください',
),
textInputAction: TextInputAction.next,
keyboardType: TextInputType.text,
)
// 複数行テキストフィールド
FormBuilderTextField(
name: 'description',
decoration: const InputDecoration(
labelText: '説明',
alignLabelWithHint: true,
),
maxLines: 5,
minLines: 3,
keyboardType: TextInputType.multiline,
)
// 数値入力フィールド
FormBuilderTextField(
name: 'age',
decoration: const InputDecoration(
labelText: '年齢',
suffixText: '歳',
),
keyboardType: TextInputType.number,
valueTransformer: (value) => value != null ? int.tryParse(value) : null,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.numeric(),
FormBuilderValidators.min(0),
FormBuilderValidators.max(120),
]),
)
日付・時刻選択
// 日付選択
FormBuilderDateTimePicker(
name: 'date',
inputType: InputType.date,
decoration: const InputDecoration(
labelText: '日付',
prefixIcon: Icon(Icons.calendar_today),
),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
locale: const Locale('ja', 'JP'),
)
// 時刻選択
FormBuilderDateTimePicker(
name: 'time',
inputType: InputType.time,
decoration: const InputDecoration(
labelText: '時刻',
prefixIcon: Icon(Icons.access_time),
),
initialTime: const TimeOfDay(hour: 8, minute: 0),
)
// 日付範囲選択
FormBuilderDateRangePicker(
name: 'date_range',
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
decoration: const InputDecoration(
labelText: '期間',
hintText: '開始日 - 終了日',
prefixIcon: Icon(Icons.date_range),
),
)
選択系フィールド
// ドロップダウン
FormBuilderDropdown<String>(
name: 'gender',
decoration: const InputDecoration(
labelText: '性別',
hintText: '性別を選択してください',
),
items: ['男性', '女性', 'その他', '回答しない']
.map((gender) => DropdownMenuItem(
value: gender,
child: Text(gender),
))
.toList(),
validator: FormBuilderValidators.required(
errorText: '性別を選択してください',
),
)
// ラジオボタングループ
FormBuilderRadioGroup<String>(
name: 'experience',
decoration: const InputDecoration(
labelText: '経験年数',
),
options: const [
FormBuilderFieldOption(value: '0-1', child: Text('0-1年')),
FormBuilderFieldOption(value: '2-5', child: Text('2-5年')),
FormBuilderFieldOption(value: '6-10', child: Text('6-10年')),
FormBuilderFieldOption(value: '10+', child: Text('10年以上')),
],
orientation: OptionsOrientation.horizontal,
wrapAlignment: WrapAlignment.spaceEvenly,
)
// チェックボックスグループ
FormBuilderCheckboxGroup<String>(
name: 'skills',
decoration: const InputDecoration(
labelText: 'スキル',
),
options: const [
FormBuilderFieldOption(value: 'dart', child: Text('Dart')),
FormBuilderFieldOption(value: 'flutter', child: Text('Flutter')),
FormBuilderFieldOption(value: 'firebase', child: Text('Firebase')),
FormBuilderFieldOption(value: 'api', child: Text('REST API')),
],
validator: FormBuilderValidators.minLength(
2,
errorText: '少なくとも2つ以上選択してください',
),
)
スイッチ・スライダー
// スイッチ
FormBuilderSwitch(
name: 'accept_terms',
title: const Text('利用規約に同意する'),
decoration: const InputDecoration(
border: InputBorder.none,
),
validator: FormBuilderValidators.requiredTrue(
errorText: '利用規約への同意が必要です',
),
)
// スライダー
FormBuilderSlider(
name: 'rating',
min: 0,
max: 10,
initialValue: 5,
divisions: 10,
decoration: const InputDecoration(
labelText: '評価',
),
displayValues: DisplayValues.all,
onChanged: (value) {
debugPrint('Rating: $value');
},
)
// 範囲スライダー
FormBuilderRangeSlider(
name: 'price_range',
min: 0,
max: 10000,
initialValue: const RangeValues(1000, 5000),
divisions: 100,
decoration: const InputDecoration(
labelText: '価格帯',
),
displayValues: DisplayValues.all,
)
チップ選択
// 選択チップ(単一選択)
FormBuilderChoiceChip<String>(
name: 'choice_chip',
decoration: const InputDecoration(
labelText: 'サイズ',
),
options: const [
FormBuilderChipOption(value: 'S', child: Text('S')),
FormBuilderChipOption(value: 'M', child: Text('M')),
FormBuilderChipOption(value: 'L', child: Text('L')),
FormBuilderChipOption(value: 'XL', child: Text('XL')),
],
spacing: 8,
)
// フィルターチップ(複数選択)
FormBuilderFilterChip<String>(
name: 'filter_chip',
decoration: const InputDecoration(
labelText: 'カテゴリー',
),
options: const [
FormBuilderChipOption(
value: 'technology',
child: Text('テクノロジー'),
avatar: Icon(Icons.computer),
),
FormBuilderChipOption(
value: 'design',
child: Text('デザイン'),
avatar: Icon(Icons.palette),
),
FormBuilderChipOption(
value: 'business',
child: Text('ビジネス'),
avatar: Icon(Icons.business),
),
],
spacing: 8,
runSpacing: 8,
)
バリデーション機能
組み込みバリデータ
// 必須バリデータ
FormBuilderValidators.required(errorText: '必須項目です')
// メールバリデータ
FormBuilderValidators.email(errorText: '有効なメールアドレスを入力してください')
// 数値バリデータ
FormBuilderValidators.numeric(errorText: '数値を入力してください')
// 最小・最大長
FormBuilderValidators.minLength(5, errorText: '5文字以上入力してください')
FormBuilderValidators.maxLength(100, errorText: '100文字以内で入力してください')
// 最小・最大値
FormBuilderValidators.min(18, errorText: '18歳以上である必要があります')
FormBuilderValidators.max(65, errorText: '65歳以下である必要があります')
// パターンマッチング
FormBuilderValidators.match(
RegExp(r'^[0-9]{3}-[0-9]{4}$'),
errorText: '郵便番号の形式が正しくありません(例:123-4567)',
)
// URL検証
FormBuilderValidators.url(errorText: '有効なURLを入力してください')
// IP検証
FormBuilderValidators.ip(errorText: '有効なIPアドレスを入力してください')
// クレジットカード検証
FormBuilderValidators.creditCard(errorText: '有効なクレジットカード番号を入力してください')
複数バリデータの組み合わせ
FormBuilderTextField(
name: 'username',
decoration: const InputDecoration(labelText: 'ユーザー名'),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(errorText: 'ユーザー名は必須です'),
FormBuilderValidators.minLength(3, errorText: '3文字以上入力してください'),
FormBuilderValidators.maxLength(20, errorText: '20文字以内で入力してください'),
FormBuilderValidators.match(
RegExp(r'^[a-zA-Z0-9_]+$'),
errorText: '英数字とアンダースコアのみ使用できます',
),
]),
)
カスタムバリデータ
// シンプルなカスタムバリデータ
String? customValidator(String? value) {
if (value == null || value.isEmpty) return null;
if (value.contains('@example.com')) {
return 'example.comドメインは使用できません';
}
return null;
}
// 他のフィールドの値に依存するバリデータ
FormBuilderTextField(
name: 'confirm_password',
decoration: const InputDecoration(labelText: 'パスワード(確認)'),
obscureText: true,
validator: (value) {
final password = _formKey.currentState?.fields['password']?.value;
if (value != password) {
return 'パスワードが一致しません';
}
return null;
},
)
// 非同期バリデータ
FormBuilderTextField(
name: 'email',
decoration: const InputDecoration(labelText: 'メールアドレス'),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.email(),
]),
asyncValidator: (value) async {
// サーバーでメールアドレスの重複をチェック
final isExists = await checkEmailExists(value);
if (isExists) {
return 'このメールアドレスは既に使用されています';
}
return null;
},
asyncValidatorDebounce: const Duration(milliseconds: 500),
)
フォーム状態管理
フォームデータの取得と設定
// フォームの値を取得
void getFormValues() {
final formState = _formKey.currentState;
// すべての値を取得
final values = formState?.value;
debugPrint('Form values: $values');
// 特定のフィールドの値を取得
final email = formState?.fields['email']?.value;
debugPrint('Email: $email');
// 即時値の取得(保存せずに現在の値を取得)
final instantValues = formState?.instantValue;
debugPrint('Instant values: $instantValues');
}
// フォームの値を設定
void setFormValues() {
// 単一フィールドの値を設定
_formKey.currentState?.fields['email']?.didChange('[email protected]');
// 複数フィールドの値を一括設定
_formKey.currentState?.patchValue({
'name': '山田太郎',
'age': 25,
'gender': '男性',
'skills': ['dart', 'flutter'],
});
}
// フォームをリセット
void resetForm() {
_formKey.currentState?.reset();
}
// 特定のフィールドをリセット
void resetField() {
_formKey.currentState?.fields['email']?.reset();
}
バリデーションエラーの制御
// プログラムでエラーを設定
void setFieldError() {
// フォームキーを使用
_formKey.currentState?.fields['email']?.invalidate('このメールアドレスは使用できません');
// フィールドキーを使用(より直接的)
final emailFieldKey = GlobalKey<FormBuilderFieldState>();
emailFieldKey.currentState?.invalidate('エラーメッセージ');
}
// エラーをクリア
void clearFieldError() {
_formKey.currentState?.fields['email']?.validate();
}
// フォーカスとスクロール制御
void invalidateWithOptions() {
_formKey.currentState?.fields['email']?.invalidate(
'エラーメッセージ',
shouldFocus: true, // エラーフィールドにフォーカス
shouldAutoScrollWhenFocused: true, // 自動スクロール
);
}
カスタムフォームフィールドの作成
基本的なカスタムフィールド
class CustomColorPicker extends StatefulWidget {
final String name;
final Color? initialValue;
final InputDecoration decoration;
final ValueChanged<Color?>? onChanged;
final FormFieldValidator<Color>? validator;
const CustomColorPicker({
Key? key,
required this.name,
this.initialValue,
this.decoration = const InputDecoration(),
this.onChanged,
this.validator,
}) : super(key: key);
@override
State<CustomColorPicker> createState() => _CustomColorPickerState();
}
class _CustomColorPickerState extends State<CustomColorPicker> {
final List<Color> colors = [
Colors.red,
Colors.blue,
Colors.green,
Colors.yellow,
Colors.purple,
Colors.orange,
];
@override
Widget build(BuildContext context) {
return FormBuilderField<Color>(
name: widget.name,
initialValue: widget.initialValue,
validator: widget.validator,
builder: (FormFieldState<Color> field) {
return InputDecorator(
decoration: widget.decoration.copyWith(
errorText: field.errorText,
),
child: Wrap(
spacing: 8,
children: colors.map((color) {
final isSelected = field.value == color;
return GestureDetector(
onTap: () {
field.didChange(color);
widget.onChanged?.call(color);
},
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? Colors.black : Colors.grey,
width: isSelected ? 3 : 1,
),
),
),
);
}).toList(),
),
);
},
);
}
}
高度なカスタムフィールド
class StarRatingField extends StatefulWidget {
final String name;
final int? initialValue;
final int maxRating;
final InputDecoration decoration;
final ValueChanged<int?>? onChanged;
final FormFieldValidator<int>? validator;
final bool enabled;
const StarRatingField({
Key? key,
required this.name,
this.initialValue,
this.maxRating = 5,
this.decoration = const InputDecoration(),
this.onChanged,
this.validator,
this.enabled = true,
}) : super(key: key);
@override
State<StarRatingField> createState() => _StarRatingFieldState();
}
class _StarRatingFieldState extends State<StarRatingField> {
@override
Widget build(BuildContext context) {
return FormBuilderField<int>(
name: widget.name,
initialValue: widget.initialValue,
validator: widget.validator,
enabled: widget.enabled,
builder: (FormFieldState<int> field) {
return InputDecorator(
decoration: widget.decoration.copyWith(
errorText: field.errorText,
enabled: widget.enabled,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(widget.maxRating, (index) {
final rating = index + 1;
final isSelected = (field.value ?? 0) >= rating;
return IconButton(
icon: Icon(
isSelected ? Icons.star : Icons.star_border,
color: widget.enabled
? (isSelected ? Colors.amber : Colors.grey)
: Colors.grey.shade300,
),
onPressed: widget.enabled
? () {
field.didChange(rating);
widget.onChanged?.call(rating);
}
: null,
);
}),
),
);
},
);
}
}
実践的な例
複雑なフォームの実装
class RegistrationForm extends StatefulWidget {
const RegistrationForm({Key? key}) : super(key: key);
@override
State<RegistrationForm> createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
final _formKey = GlobalKey<FormBuilderState>();
bool _isLoading = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ユーザー登録'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: FormBuilder(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 基本情報セクション
const Text(
'基本情報',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
FormBuilderTextField(
name: 'last_name',
decoration: const InputDecoration(
labelText: '姓',
prefixIcon: Icon(Icons.person),
),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.maxLength(50),
]),
),
const SizedBox(height: 16),
FormBuilderTextField(
name: 'first_name',
decoration: const InputDecoration(
labelText: '名',
prefixIcon: Icon(Icons.person),
),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.maxLength(50),
]),
),
const SizedBox(height: 16),
FormBuilderDateTimePicker(
name: 'birth_date',
inputType: InputType.date,
decoration: const InputDecoration(
labelText: '生年月日',
prefixIcon: Icon(Icons.calendar_today),
),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
validator: FormBuilderValidators.required(),
),
const SizedBox(height: 24),
// 連絡先情報セクション
const Text(
'連絡先情報',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
FormBuilderTextField(
name: 'email',
decoration: const InputDecoration(
labelText: 'メールアドレス',
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.email(),
]),
),
const SizedBox(height: 16),
FormBuilderTextField(
name: 'phone',
decoration: const InputDecoration(
labelText: '電話番号',
prefixIcon: Icon(Icons.phone),
hintText: '090-1234-5678',
),
keyboardType: TextInputType.phone,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.match(
RegExp(r'^\d{2,4}-\d{2,4}-\d{4}$'),
errorText: '正しい電話番号を入力してください',
),
]),
),
const SizedBox(height: 24),
// アカウント情報セクション
const Text(
'アカウント情報',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
FormBuilderTextField(
name: 'username',
decoration: const InputDecoration(
labelText: 'ユーザー名',
prefixIcon: Icon(Icons.account_circle),
helperText: '英数字とアンダースコアのみ使用可能',
),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.minLength(3),
FormBuilderValidators.maxLength(20),
FormBuilderValidators.match(
RegExp(r'^[a-zA-Z0-9_]+$'),
errorText: '英数字とアンダースコアのみ使用できます',
),
]),
),
const SizedBox(height: 16),
FormBuilderTextField(
name: 'password',
decoration: const InputDecoration(
labelText: 'パスワード',
prefixIcon: Icon(Icons.lock),
helperText: '8文字以上、大文字・小文字・数字を含む',
),
obscureText: true,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.minLength(8),
(value) {
if (value == null) return null;
if (!RegExp(r'[A-Z]').hasMatch(value)) {
return '大文字を1文字以上含めてください';
}
if (!RegExp(r'[a-z]').hasMatch(value)) {
return '小文字を1文字以上含めてください';
}
if (!RegExp(r'[0-9]').hasMatch(value)) {
return '数字を1文字以上含めてください';
}
return null;
},
]),
),
const SizedBox(height: 16),
FormBuilderTextField(
name: 'password_confirm',
decoration: const InputDecoration(
labelText: 'パスワード(確認)',
prefixIcon: Icon(Icons.lock),
),
obscureText: true,
validator: (value) {
final password = _formKey.currentState?.fields['password']?.value;
if (value != password) {
return 'パスワードが一致しません';
}
return null;
},
),
const SizedBox(height: 24),
// 利用規約
FormBuilderCheckbox(
name: 'accept_terms',
title: RichText(
text: const TextSpan(
children: [
TextSpan(
text: '利用規約',
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
),
TextSpan(
text: 'に同意する',
style: TextStyle(color: Colors.black),
),
],
),
),
validator: FormBuilderValidators.requiredTrue(
errorText: '利用規約への同意が必要です',
),
),
const SizedBox(height: 24),
// 送信ボタン
ElevatedButton(
onPressed: _isLoading ? null : _onSubmit,
child: _isLoading
? const CircularProgressIndicator()
: const Text('登録する'),
),
],
),
),
),
);
}
Future<void> _onSubmit() async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
setState(() => _isLoading = true);
try {
final values = _formKey.currentState!.value;
debugPrint(values.toString());
// ここでAPIリクエストを送信
await Future.delayed(const Duration(seconds: 2));
// 成功メッセージを表示
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('登録が完了しました'),
backgroundColor: Colors.green,
),
);
Navigator.of(context).pop();
}
} catch (e) {
// エラーメッセージを表示
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('エラーが発生しました: $e'),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
} else {
// バリデーションエラーがある場合
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('入力内容を確認してください'),
backgroundColor: Colors.orange,
),
);
}
}
}
動的フォームフィールド
class DynamicFormExample extends StatefulWidget {
const DynamicFormExample({Key? key}) : super(key: key);
@override
State<DynamicFormExample> createState() => _DynamicFormExampleState();
}
class _DynamicFormExampleState extends State<DynamicFormExample> {
final _formKey = GlobalKey<FormBuilderState>();
final List<String> _hobbies = [''];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('動的フォーム')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: FormBuilder(
key: _formKey,
child: Column(
children: [
const Text(
'趣味を入力してください',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
..._hobbies.asMap().entries.map((entry) {
final index = entry.key;
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: [
Expanded(
child: FormBuilderTextField(
name: 'hobby_$index',
decoration: InputDecoration(
labelText: '趣味 ${index + 1}',
suffixIcon: index > 0
? IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
setState(() {
_hobbies.removeAt(index);
});
},
)
: null,
),
validator: FormBuilderValidators.required(
errorText: '趣味を入力してください',
),
),
),
],
),
);
}).toList(),
const SizedBox(height: 16),
TextButton.icon(
onPressed: () {
setState(() {
_hobbies.add('');
});
},
icon: const Icon(Icons.add),
label: const Text('趣味を追加'),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
if (_formKey.currentState?.saveAndValidate() ?? false) {
final values = _formKey.currentState!.value;
final hobbies = values.entries
.where((e) => e.key.startsWith('hobby_'))
.map((e) => e.value)
.toList();
debugPrint('趣味: $hobbies');
}
},
child: const Text('送信'),
),
],
),
),
),
);
}
}
条件付きフィールド表示
class ConditionalFormExample extends StatefulWidget {
const ConditionalFormExample({Key? key}) : super(key: key);
@override
State<ConditionalFormExample> createState() => _ConditionalFormExampleState();
}
class _ConditionalFormExampleState extends State<ConditionalFormExample> {
final _formKey = GlobalKey<FormBuilderState>();
bool _showAdditionalFields = false;
String? _selectedCountry;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('条件付きフォーム')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: FormBuilder(
key: _formKey,
onChanged: () {
// フォームの変更を監視
setState(() {
_showAdditionalFields =
_formKey.currentState?.fields['has_experience']?.value ?? false;
_selectedCountry =
_formKey.currentState?.fields['country']?.value;
});
},
child: Column(
children: [
FormBuilderSwitch(
name: 'has_experience',
title: const Text('プログラミング経験がありますか?'),
decoration: const InputDecoration(
border: InputBorder.none,
),
),
if (_showAdditionalFields) ...[
const SizedBox(height: 16),
FormBuilderTextField(
name: 'years_of_experience',
decoration: const InputDecoration(
labelText: '経験年数',
suffixText: '年',
),
keyboardType: TextInputType.number,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.numeric(),
FormBuilderValidators.min(0),
]),
),
const SizedBox(height: 16),
FormBuilderCheckboxGroup<String>(
name: 'languages',
decoration: const InputDecoration(
labelText: '使用経験のある言語',
),
options: const [
FormBuilderFieldOption(value: 'dart', child: Text('Dart')),
FormBuilderFieldOption(value: 'javascript', child: Text('JavaScript')),
FormBuilderFieldOption(value: 'python', child: Text('Python')),
FormBuilderFieldOption(value: 'java', child: Text('Java')),
],
validator: FormBuilderValidators.minLength(
1,
errorText: '少なくとも1つ選択してください',
),
),
],
const SizedBox(height: 24),
FormBuilderDropdown<String>(
name: 'country',
decoration: const InputDecoration(
labelText: '国',
),
items: const [
DropdownMenuItem(value: 'JP', child: Text('日本')),
DropdownMenuItem(value: 'US', child: Text('アメリカ')),
DropdownMenuItem(value: 'OTHER', child: Text('その他')),
],
validator: FormBuilderValidators.required(),
),
if (_selectedCountry == 'OTHER') ...[
const SizedBox(height: 16),
FormBuilderTextField(
name: 'country_other',
decoration: const InputDecoration(
labelText: '国名を入力してください',
),
validator: FormBuilderValidators.required(
errorText: '国名を入力してください',
),
),
],
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
if (_formKey.currentState?.saveAndValidate() ?? false) {
debugPrint(_formKey.currentState?.value.toString());
}
},
child: const Text('送信'),
),
],
),
),
),
);
}
}
ベストプラクティス
パフォーマンスの最適化
// 1. AutovalidateModeの適切な使用
FormBuilder(
// 常に検証(パフォーマンスへの影響あり)
autovalidateMode: AutovalidateMode.always,
// ユーザーが操作した後のみ検証(推奨)
autovalidateMode: AutovalidateMode.onUserInteraction,
// 手動でのみ検証
autovalidateMode: AutovalidateMode.disabled,
)
// 2. 重い処理の最適化
FormBuilderTextField(
name: 'search',
onChanged: (value) {
// デバウンスを使用して過度な処理を防ぐ
_debouncer.run(() {
// 検索処理
});
},
)
// 3. メモリリークの防止
@override
void dispose() {
// フォームキーを適切に破棄
_formKey.currentState?.dispose();
super.dispose();
}
エラーハンドリング
// グローバルエラーハンドリング
class FormErrorHandler {
static void handleError(BuildContext context, dynamic error) {
String message = 'エラーが発生しました';
if (error is NetworkException) {
message = 'ネットワークエラー: 接続を確認してください';
} else if (error is ValidationException) {
message = 'バリデーションエラー: ${error.message}';
} else if (error is TimeoutException) {
message = 'タイムアウト: 時間をおいて再試行してください';
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
action: SnackBarAction(
label: '再試行',
onPressed: () {
// 再試行ロジック
},
),
),
);
}
}
アクセシビリティ対応
// 適切なセマンティクスの提供
FormBuilderTextField(
name: 'email',
decoration: const InputDecoration(
labelText: 'メールアドレス',
hintText: '[email protected]',
),
// スクリーンリーダー用の追加情報
semanticsLabel: 'メールアドレスを入力してください',
)
// フォーカス管理
final _emailFocus = FocusNode();
final _passwordFocus = FocusNode();
FormBuilderTextField(
name: 'email',
focusNode: _emailFocus,
textInputAction: TextInputAction.next,
onSubmitted: (_) {
FocusScope.of(context).requestFocus(_passwordFocus);
},
)
他のFlutterフォームライブラリとの比較
Flutter Form Builder vs 標準Form
// 標準Formの場合
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
validator: (value) {
if (value == null || value.isEmpty) {
return '必須項目です';
}
if (!value.contains('@')) {
return '有効なメールアドレスを入力してください';
}
return null;
},
),
// ...
],
),
)
// Flutter Form Builderの場合
final _formKey = GlobalKey<FormBuilderState>();
FormBuilder(
key: _formKey,
child: Column(
children: [
FormBuilderTextField(
name: 'email',
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.email(),
]),
),
// ...
],
),
)
主な違い
- コード量: Flutter Form Builderは定型コードを大幅に削減
- バリデーション: 豊富な組み込みバリデータと簡単な組み合わせ
- 状態管理: フィールドの値を自動的に管理
- フィールドタイプ: 多様な入力フィールドをすぐに使用可能
- 国際化: 50以上の言語でエラーメッセージをサポート
トラブルシューティング
よくある問題と解決方法
// 問題1: フォームの値が取得できない
// 解決: saveAndValidate()を呼び出す
if (_formKey.currentState?.saveAndValidate() ?? false) {
final values = _formKey.currentState!.value;
}
// 問題2: バリデーションが機能しない
// 解決: validatorを正しく設定
FormBuilderTextField(
name: 'email',
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.email(),
]),
)
// 問題3: フィールドの値が更新されない
// 解決: didChange()を使用
_formKey.currentState?.fields['email']?.didChange('[email protected]');
// 問題4: カスタムバリデーションメッセージが表示されない
// 解決: errorTextパラメータを使用
FormBuilderValidators.required(
errorText: 'このフィールドは必須です',
)
まとめ
Flutter Form Builderは、Flutterアプリケーションでフォームを構築する際の強力なツールです。豊富な入力フィールド、包括的なバリデーション機能、簡潔なAPIにより、開発効率を大幅に向上させることができます。
主な利点
- 開発速度の向上: 定型コードの削減により、開発時間を短縮
- 保守性の向上: 統一されたAPIにより、コードの可読性と保守性が向上
- 豊富な機能: 多様なフィールドタイプとバリデータにより、複雑な要件にも対応
- 国際化対応: 多言語サポートにより、グローバルなアプリケーション開発が容易
Flutter Form Builderを使用することで、より効率的で保守性の高いフォーム実装が可能になります。