【Flutter】AutocompleteWidgetでフォームの自動補完機能を実装する

Googleの検索窓やECサイトへのログイン時に自動で候補が表示されるオートコンプリート機能って便利ですよね。Flutterにもこれを簡単に実装できるものがないかな〜と思っていた時に丁度Flutter公式YoutubeチャンネルののWidget of the Weekでそれらしきものが紹介されていました。

https://youtu.be/-Nny8kzW380

今回はこちらのWidgetをいじってみたので紹介していきたいと思います。


## 基本的な使い方
基本的な使い方は下記の通りです。コピペで動きます。

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

  static const _sports = <String>[
    'football',
    'baseball',
    'tennis',
    'swimming',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Autocomplete<String>(
          optionsBuilder: (TextEditingValue textEditingValue) {
            if (textEditingValue.text == '') {
              return const Iterable<String>.empty();
            }
            return _sports.where((String option) {
              return option.contains(textEditingValue.text.toLowerCase());
            });
          },
          onSelected: (String value) {
            // 候補選択時に呼び出されます。
            print('$valueを選択しました！');
          },
        ),
      ),
    );
  }
}
```
<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2466417/00d98fd3-4b50-7af3-9787-36240c2c4014.gif' width='250'>


TextFromFieldの代わりにAutocompleteを指定します。`optionsBuilder`は現在入力されている値を確認し、一致する文字を含む候補を表示してくれます。`onSelected`はユーザーが候補を選択した時に呼び出されます。

また、候補一覧はString型以外にも指定することが可能で、例えばUserというクラスのnameで候補一覧を表示したいといったことも可能です。


```dart
class User {
  User({
    required this.name,
    required this.mail,
  });
  String name;
  String mail;
}

class AutocompleteExample extends StatelessWidget {
  AutocompleteExample({Key? key}) : super(key: key);

  final users = <User>[
    User(name: 'Aくん', mail: 'akun@sample.com'),
    User(name: 'Bくん', mail: 'bkun@sample.com'),
    User(name: 'Cくん', mail: 'ckun@sample.com'),
  ];
  // 何のプロパティを候補に表示するか指定
  static String _displayStringForOption(User option) => option.name;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Autocomplete<User>(
          displayStringForOption: _displayStringForOption,
          optionsBuilder: (TextEditingValue textEditingValue) {
            if (textEditingValue.text == '') {
              return const Iterable<User>.empty();
            }
            return users.where((User option) {
              return option.name.toString().contains(textEditingValue.text);
            });
          },
          // 選択した値が入る
          onSelected: (User user) {
            print('${user.name}を選択しました！');
          },
        ),
      ),
    );
  }
}
```
<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2466417/37e2898d-56c7-dec9-3f42-f2d32399f2a0.gif' width='250'>


簡単に使えて良いですね！

## 候補リストをカスタマイズ

`optionsViewBuilder`プロパティで候補一覧のスタイルを変更することができます。今回は`ListView.builder`でリスト形式で表示するように実装してみました。

```dart
class AutocompleteExample extends StatelessWidget {
  AutocompleteExample({Key? key}) : super(key: key);

  final users = <User>[
    User(name: 'Aくん', mail: 'akun@sample.com'),
    User(name: 'Bくん', mail: 'bkun@sample.com'),
    User(name: 'Cくん', mail: 'ckun@sample.com'),
  ];

  static String _displayStringForOption(User option) => option.name;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Autocomplete<User>(
            displayStringForOption: _displayStringForOption,
            optionsBuilder: (TextEditingValue textEditingValue) {
              if (textEditingValue.text == '') {
                return const Iterable<User>.empty();
              }
              return users.where((User option) {
                return option.name.toString().contains(textEditingValue.text);
              });
            },
            onSelected: (User user) {
              print('${user.name}を選択しました');
            },
            optionsViewBuilder: (context, onSelected, users) {
              return ListView.builder(
                itemCount: users.length,
                itemBuilder: ((context, index) {
                  // iterable to list
                  final userList = users.toList();
                  return MyWidget(userList[index]);
                }),
              );
            }),
      ),
    );
  }
}

class MyWidget extends StatelessWidget {
  MyWidget(this.user);
  User user;
  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        title: Text(user.name),
        subtitle: Text(user.mail),
      ),
    );
  }
}
```

<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2466417/7d5cbf76-454e-594b-5bbf-027c648157eb.gif' width='250'>

## フォームをカスタマイズ

`fieldViewBuilder`プロパティでフォームのスタイルを変更することができます（スタイルだけというか、通常のTextFormFiledと同じようなことがここで指定できます）

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

  static const _sports = <String>[
    'football',
    'baseball',
    'tennis',
    'swimming',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Autocomplete<String>(
          optionsBuilder: (TextEditingValue textEditingValue) {
            // 未入力の場合は自動補完を走らせない
            if (textEditingValue.text == '') {
              return const Iterable<String>.empty();
            }
            return _sports.where((String option) {
              return option.contains(textEditingValue.text.toLowerCase());
            });
          },
          fieldViewBuilder:
              (context, textEditingController, focusNode, onFieldSubmitted) {
            return TextFormField(
              controller: textEditingController,
              focusNode: focusNode,
              onFieldSubmitted: (String value) {
                onFieldSubmitted();
              },
              decoration: InputDecoration(
                focusedBorder: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(16),
                  borderSide: const BorderSide(
                    color: Colors.green,
                    width: 2.0,
                  ),
                ),
                labelStyle: TextStyle(
                  fontSize: 12,
                  color: Colors.green[300],
                ),
                labelText: 'フレームあり、ラベルあり',
                floatingLabelStyle: const TextStyle(fontSize: 12),
                enabledBorder: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(16),
                  borderSide: BorderSide(
                    color: Colors.green[100]!,
                    width: 1.0,
                  ),
                ),
              ),
            );
          },
          // 選択した値が入る
          onSelected: (String selection) {
            print('$selectionを選択しました！');
          },
        ),
      ),
    );
  }
}
```
<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2466417/a84c9419-2f4e-7de2-709e-aa65bf5f3b87.gif' width='250'>

## 活用例
SharedPreferenceなどのパッケージを活用してユーザーのメールアドレスを永続化しておけば、次回ユーザーがログインする時の負担を減らすことができます（ECサイトとかでよく見るやつです)

https://pub.dev/packages/shared_preferences

```dart
class AutocompleteSample extends StatefulWidget {
  const AutocompleteSample({Key? key}) : super(key: key);

  @override
  State<AutocompleteSample> createState() => _AutocompleteSampleState();
}

class _AutocompleteSampleState extends State<AutocompleteSample> {
  List<String> mails = [];

  Future<void> getMail() async {
    final prefs = await SharedPreferences.getInstance();
    final mail = prefs.getString('mail');
    if (mail != null) {
      mails.add(mail);
    }
  }

  @override
  void initState() {
    super.initState();
    getMail();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Autocomplete<String>(
          optionsBuilder: (TextEditingValue textEditingValue) {
            if (textEditingValue.text == '') {
              return const Iterable<String>.empty();
            }
            return mails.where((String option) {
              return option.contains(textEditingValue.text.toLowerCase());
            });
          },
          onSelected: (String selection) {
            print('$selectionを選択しました！');
          },
        ),
      ),
    );
  }
}
```
<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2466417/e9ebc40c-85cc-748e-e341-eeff22449990.gif' width='250'>

自分で検索ロジックを書く必要がないので検索フォームの実装の負担がかなり減るなと思いました！みなさんもぜひ使ってみてください！

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

https://youtu.be/-Nny8kzW380