Flutter AutocompleteでTextFieldに入力候補を表示する

![autocomplete_sample_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/9cd4cea2-7990-59ac-23f2-aad19ee884f3.png)

## この記事について

Flutterで入力候補付きのテキストフィールドを表示する`Autocomplete`ウィジェットの実装サンプルを紹介、解説しています。ここでは下記の3つタイプの実装サンプルを紹介しています。

1. Flutter公式サンプル ← 一番簡単だが見た目×
1. Medium(海外ブログサービス)で見つけたサンプル ← Flutter Webと相性×
1. 僕がたどり着いた正解のサンプル ← 上記の欠点を克服！

**答えだけ欲しい方は3つ目のサンプルコードだけコピペで使っていただければOKです◎**

※ FlutterやDartの基礎的な部分についての解説は省いています
※ 可読性や簡単のため参考記事から一部コードを編集しています

## Flutter AutoCompleteについて

Flutterの標準的なWidget`TextField`の変化系で、テキスト入力中に入力候補を表示させることができます。
その他多数の標準的なWidgetと共に`flutter/material.dart`に含まれています。
公式ページ：[Autocomplete class](https://api.flutter.dev/flutter/material/Autocomplete-class.html)

#### `material.dart`のインポート

```sample.dart
import 'package:flutter/material.dart';
```

## 1. Flutter公式サンプル

**特長**
最も簡単な実装サンプル
**デメリット**
装飾の変更なし
![autocomplete_sample_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/3e4b33a3-7b9e-7526-0f0d-851074c2f8ef.png)

### Widget実装部コードサンプル

```sample.dart
class MyAutocomplete extends StatelessWidget {
  const MyAutocomplete({Key? key}) : super(key: key);

  // 入力候補に出す値の配列
  static const _kOptions = ['aardvark', 'bobcat', 'chameleon']; // 1.

  @override
  Widget build(BuildContext context) {
    // AutoComplete Widget本体
    return Autocomplete<String>( // 2.
      // 入力候補リストの要素ビルダー
      optionsBuilder: (TextEditingValue textEditingValue) { // 3.
        if (textEditingValue.text == '') {
          return const Iterable<String>.empty();
        }
        return _kOptions.where((String option) { // 4.
          return option.contains(textEditingValue.text.toLowerCase());
        });
      },
      // 入力候補選択時の処理
      onSelected: (String selection) { // 5.
        debugPrint('You just selected $selection');
      },
    );
  }
}
```

### 解説

1. 入力候補に表示する要素のデータ元になる配列
1. `AutoComplete<String>`のように、リストに出す候補のクラスを宣言
   (`String`が返せるクラスであればなんでも可、今回はそのまま`String`)
1. `optionsBuilder`で、候補に出す値の配列を返します
   (`textEditingValue.text`でフィールドに入力中の値を取得して`where`内で利用)
1. 2.で宣言したクラスの配列を`return`
1. 候補選択時の処理、次フィールドへフォーカスの移動なども実装可

### 参考ページ
https://api.flutter.dev/flutter/material/Autocomplete-class.html

## 2. Mediumで見つけたサンプル
**特長**
装飾を施したパターンの簡単なサンプル
**デメリット**
Webでこのまま実装した場合、キーボードの矢印による候補の選択ができなくなる
(厳密にはハイライトされなくなる、その他いろいろと難あり)
![autocomplete_sample_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/a3e83991-a90b-5438-243b-d9aad51a229b.png)

### Widget実装部コードサンプル

```sample.dart
class MyAutocomplete extends StatelessWidget {
  const MyAutocomplete({Key? key}) : super(key: key);

  static const _kOptions = ['aardvark', 'bobcat', 'chameleon'];

  @override
  Widget build(BuildContext context) {
    return Autocomplete<String>(
      optionsBuilder: (TextEditingValue textEditingValue) {
        if (textEditingValue.text == '') {
          return const Iterable<String>.empty();
        }
        return _kOptions.where((String option) {
          return option.contains(textEditingValue.text.toLowerCase());
        });
      },
      onSelected: (String selection) {
        debugPrint('You just selected $selection');
      },
      // ---------- ここから追加 ----------
      optionsViewBuilder: (
        BuildContext context,
        AutocompleteOnSelected<String> onSelected,
        Iterable<String> options,
      ) {
        // 入力候補リストの表示枠のWidgetを定義
        return Align(
          alignment: Alignment.topLeft,
          child: Material( 
            elevation: 4, // 1.
            child: Container(
              width: MediaQuery.of(context).size.width - 24, // 2.
              color: Colors.cyan,
              // のリストWidget本体
              child: ListView.builder( // 3.
                shrinkWrap: true, // 4.
                itemCount: options.length,
                itemBuilder: (context, index) {
                  final option = options.elementAt(index);
                  return GestureDetector(
                    child: ListTile(
                      title: Text(option, style: const TextStyle(color: Colors.white)),
                    ),
                    onTap: () => onSelected(option), // 5.
                  );
                },
              ),
            ),
          ),
        );
      },
      // ---------- ここまで ----------
    );
  }
}

```

### 解説

1. 影をつけるためのelevation
   `Material`ごと削除すれば影のないフラットなデザインに
1. テキストフィールドの幅に入力候補の幅もあわせる
   デフォルトでは画面の右端まで突き抜けてしまう
1. 入力候補を動的に生成するリストビュー
1. 候補が少ない時にリストが下の伸びっぱなしにならないよう`false`に
1. 候補選択時にフィールドに値が入力させる処理`onSelected`

### 参考ページ
https://medium.flutterdevs.com/explore-autocomplete-widget-in-flutter-70d3478bacc4

## 3. 僕がたどり着いた正解のサンプル
![autocomplete_sample_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/9cd4cea2-7990-59ac-23f2-aad19ee884f3.png)

**特長**
入力候補部分のWidgetの装飾を変更しつつ、キーボード矢印による候補の選択にも対応
執筆時点では他サイトに類似のサンプルなどありませんでした！
(Autocompleteのソースコードを見てアレンジしました)
**デメリット**
特にありません、実際にアプリ制作する場合はこちらをお使いください

### Widget実装部コードサンプル

```sample.dart
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; // 1.
```

```sample.dart
class MyAutocomplete extends StatelessWidget {
  const MyAutocomplete({Key? key}) : super(key: key);

  static const _kOptions = ['aardvark', 'bobcat', 'chameleon', 'dragonfly', 'eagle', 'fossa'];

  @override
  Widget build(BuildContext context) {
    return Autocomplete<String>(
      optionsBuilder: (TextEditingValue textEditingValue) {
        if (textEditingValue.text == '') {
          return const Iterable<String>.empty();
        }
        return _kOptions.where((String option) {
          return option.contains(textEditingValue.text.toLowerCase());
        });
      },
      onSelected: (String selection) {
        debugPrint('You just selected $selection');
      },
      optionsViewBuilder: (
        BuildContext context,
        AutocompleteOnSelected<String> onSelected,
        Iterable<String> options,
      ) {
        // 入力候補リストの表示枠のWidgetを定義
        return Align(
          alignment: Alignment.topLeft,
          child: Material(
            elevation: 4,
            child: SizedBox( // 2.
              width: MediaQuery.of(context).size.width - 24,
              child: ConstrainedBox( // 3.
                constraints: const BoxConstraints(maxHeight: 175),
                child: ListView.builder(
                  shrinkWrap: true,
                  itemCount: options.length,
                  itemBuilder: (context, index) {
                    final option = options.elementAt(index);
                    return Builder(
                      builder: (BuildContext context) {
                        // 4.
                        final bool highlight = AutocompleteHighlightedOption.of(context) == index;
                        // 5.
                        if (highlight) {
                          SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
                            Scrollable.ensureVisible(context, alignment: 0.5);
                          });
                        }
                        return GestureDetector(
                          child: ListTile(
                            tileColor: highlight ? Colors.cyan.withAlpha(172) : Colors.cyan, // 6.
                            title: Text(option, style: const TextStyle(color: Colors.white)),
                          ),
                          onTap: () => onSelected(option),
                        );
                      },
                    );
                  },
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}
```

### 解説

1. 5.で使用するコールバック関数のライブラリをインポート
1. 色付きの`Container`から色なしの`SizedBox`に変更
1. 長いリストが画面外に出ないように`maxHeight`を設定
1. 選択中の候補かどうかを`bool`で取得
1. 選択中の候補がリスト表示枠の中心に来るように`callback`
1. 選択中の候補の色をハイライト表示

## あとがき

参考になりましたらLGTMポチッとお願いします！
まだまだFlutter修行中ですが、ご質問などいただければ分かる範囲でお答え(もしかすると記事化)するので、ぜひコメントも残してください:)