FormFieldとFormFieldStateを継承して独自入力フィールドを作る方法

# 3行で

- ここでの「独自入力フィールド」とは、入力フィールドをタップすると、ピッカーやモーダルなど**単純なキーボード文字入力ではない方法で値を入力できるフィールド**のことを指します
- 今回はモーダル表示した領域にTextFieldを置いてiOS風の入力ができるサンプルを例に説明します
- 継承元の親クラス（`FormField`, `FormFieldState`）のメソッドを活用すると、フォームの実装が楽になるので良かったです

# 独自入力フィールドの例

- https://github.com/masashi-sutou/custom_form_field

:one: 表示 | :two: タップ時 | :three: 値を入力 | :four: 入力が完了
:-: | :-: | :-: | :-: | 
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37385/cc839a63-2016-05f2-4460-d63c23230c3a.png width=210/> |<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37385/9499e9fb-b0aa-b121-a1a0-07c3095dd476.png width=210/> | <img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37385/6424fd77-f83d-9689-6b13-cf3cb3a065c7.png width=210/> | <img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37385/10593588-d9c3-710c-06d3-6bb61b39fa0b.png width=210/> |


# 実装ポイント

## 1. `FormField`の`builder`で入力フィールドを組み立てる

- `FormField`の`builder`で入力された値をどう表示するか組み立てます
- サンプルでは`InputDecorator`で表示しています

```dart:custom_form_field.dart#L27-L46
builder: (FormFieldState<String> field) {
  final effectiveDecoration = decoration.applyDefaults(
    Theme.of(field.context).inputDecorationTheme,
  );

  return InputDecorator(
    decoration: effectiveDecoration.copyWith(
      errorText: field.errorText,
    ),
    isEmpty: value?.isEmpty ?? true,
    child: Padding(
      padding: const EdgeInsets.only(right: 6),
      child: Text(
        value ?? '',
        maxLines: 1,
        overflow: TextOverflow.ellipsis,
      ),
    ),
  );
},
```

## 2. `FormFieldState`の`build`で親クラスの`super.build`を呼び、`GestureDetector`でタップを検知する

- 入力専用の独自のWidgetはタップのイベントで表示したいので、便宜的に親クラスの`super.build(context)`を渡します

```dart:custom_form_field.dart#L67-L74
@override
Widget build(BuildContext context) {
  return GestureDetector(
    behavior: HitTestBehavior.opaque,
    onTap: () => _handleTap(context),
    child: super.build(context),
  );
}
```

## 3. `FormFieldState`でタップした後の入力フィールドを組み立てる

- 2.の`GestureDetector`の`onTap`に反応して、表示させたい独自入力フィールドを組み立てます
- サンプルプロジェクトでは、`showModalBottomSheet`でiOS風の半モーダルの入力領域を表現して、そこに入力専用の`TextField`を配置しました

```dart:custom_form_field.dart#L76-L99
void _handleTap(BuildContext context) {
  FocusScope.of(context).unfocus();
  final initText = widget.initialValue ?? '';
  _textChangedNotifier.value = initText;
  _textController.text = initText;

  showModalBottomSheet<String>(
    context: context,
    isScrollControlled: true,
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.only(
        topLeft: Radius.circular(32),
        topRight: Radius.circular(32),
      ),
    ),
    builder: _textFieldModalSheet,
  ).whenComplete(() {
    // NOTE: 完了するボタンを押さずに選択画面を閉じた場合は選択前に戻す
    if (!_isTapDoneButton) {
      super.reset();
      super.save();
    }
  });
}
```

## 4. `FormFieldState`の状態管理メソッドを利用して、入力値を制御する

- `FormFieldState`には入力値の状態を制御するメソッドがいくつかあります
- 下記のメソッドを、入力値を設定したいとき、リセットしたいとき、保存したいときに呼びましょう
  - [`super.setValue(T value)`](https://api.flutter.dev/flutter/widgets/FormFieldState/setValue.html)
  - [`super.reset()`](https://api.flutter.dev/flutter/widgets/FormFieldState/reset.html)
  - [`super.save()`](https://api.flutter.dev/flutter/widgets/FormFieldState/save.html)
- 下記のコードは完了ボタンを押したときの処理です

```dart:custom_form_field.dart#L123-L147
ValueListenableBuilder<String>(
  valueListenable: _textChangedNotifier,
  builder: (context, editingText, __) {
    final enableDoneButton =
        editingText.characters.length <= widget.maxLength;
    return FlatButton(
      textTheme: ButtonTextTheme.primary,
      child: Text(
        '完了する',
        style: TextStyle(
          fontWeight: enableDoneButton
              ? FontWeight.bold
              : FontWeight.normal,
        ),
      ),
      onPressed: enableDoneButton
          ? () {
              _isTapDoneButton = true;
              super.setValue(editingText);
              super.save();
              FocusScope.of(context).unfocus();
              Navigator.pop(context);
            }
          : null,
    );
  },
```

# 参考

- https://api.flutter.dev/flutter/widgets/FormField-class.html
- https://api.flutter.dev/flutter/widgets/FormFieldState-class.html