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(),
        ]),
      ),
      // ...
    ],
  ),
)

主な違い

  1. コード量: Flutter Form Builderは定型コードを大幅に削減
  2. バリデーション: 豊富な組み込みバリデータと簡単な組み合わせ
  3. 状態管理: フィールドの値を自動的に管理
  4. フィールドタイプ: 多様な入力フィールドをすぐに使用可能
  5. 国際化: 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を使用することで、より効率的で保守性の高いフォーム実装が可能になります。