🔍

【Flutter】flutter_typeaheadで検索補完してみた

に公開

経緯

飲んだ日本酒を記録する個人開発アプリを使用していると、毎回入力が面倒な箇所がありました。
具体的には飲んだ場所の入力です。同じ店で続けて飲むことが多いので、同じ場所を入力したかったので、検索候補を出して手間を省こうと思います。

検索候補のイメージは以下です。
ユーザの入力に合わせて候補が動的に変わる仕組みです。

ライブラリの選定

候補として以下が挙がりました。

  • Autocomplete
  • flutter_typeahead

デザインにはCupertinoを使用しているため、flutter_typeaheadを選びました。
(AutocompleteはMaterialクラスだったので)

https://pub.dev/packages/flutter_typeahead
ライブラリの説明にきちんとCupertinoでも使用できると書いてあること確認

The package comes in both Material and Cupertino widget flavors. All parameters identical, the only changes are the visual defaults.

実装

公式のSampleを参考に幾つか手を加えました
プラスして使いまわせるようにコンポーネント化してます

import 'package:flutter/cupertino.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';

import '../const/color.dart';

class CustomTypeAheadField extends StatelessWidget {
  final TextEditingController controller;
  final List<String> suggestions;
  final String placeholder;
  final ValueChanged<String> onSelected;

  const CustomTypeAheadField({
    super.key,
    required this.controller,
    required this.suggestions,
    required this.placeholder,
    required this.onSelected,
  });

  
  Widget build(BuildContext context) {
    return TypeAheadField<String>(
      suggestionsCallback: (pattern) {
        return suggestions.where((option) => option.contains(pattern)).toList();
      },
      builder: (context, textController, focusNode) {
        return CupertinoTextField(
          controller: controller,
          placeholder: placeholder,
          focusNode: focusNode,
          onChanged: (value) {
            // これがないとsuggestionsCallbackが呼ばれない
            textController.text = value;
            textController.selection = TextSelection.fromPosition(
              TextPosition(offset: textController.text.length),
            );
          },
        );
      },
      itemBuilder: (context, suggestion) {
        return CupertinoListTile(
          title: Text(suggestion),
        );
      },
      emptyBuilder: (context) {
        return Container(
          height: 50,
          decoration: BoxDecoration(
            color: CupertinoColors.white,
            borderRadius: BorderRadius.circular(8),
          ),
          child: const Center(
            child: Text(
              '候補がありません',
              style: TextStyle(
                color: ConstColor.gray,
                fontWeight: FontWeight.w700,
              ),
            ),
          ),
        );
      },
      onSelected: onSelected,
      listBuilder: (context, children) {
        return Container(
          height: 200,
          decoration: BoxDecoration(
            color: CupertinoColors.white,
            borderRadius: BorderRadius.circular(8),
          ),
          child: ListView.separated(
            shrinkWrap: true,
            itemCount: children.length,
            separatorBuilder: (context, index) => Container(
              color: CupertinoColors.white,
            ),
            itemBuilder: (context, index) => children[index],
          ),
        );
      },
      hideOnEmpty: false,
    );
  }
}

使い方の例

CustomTypeAheadField(
    controller: sakamaiController,
    suggestions: sakamaiOptions,
    placeholder: '例:山田錦',
    onSelected: (value) {
    sakamaiController.text = value;
    },
),

最低限必要

  • suggestionsCallback:入力の変更を受け取り検索ロジックを走らせる
  • builder:入力フォームのメインです。
  • itemBuilder:検索候補を出力

追加した

  • emptyBuilder:候補がない場合の表示用
  • listBuilder:元のデザインがMaterial過ぎたので馴染むようにカスタマイズしました
  • hideOnEmpty:入力値が空の場合の表示有無、falseにすると表示してくれます
  • onSelected:入力の変更を受け取って参照元に返す用

これで検索候補が出るようになりました。

ただ、表示した検索候補をクリックするまで閉じない仕様だったので、画面のどこかしらをタップすれば閉じるようにしました。TypeAheadFieldのunfocusになると閉じる特性を利用して、画面全体をGestureDetectorで覆いました。

child: GestureDetector(
    behavior: HitTestBehavior.opaque, // SizedBoxをタップ可能にする
    onTap: () {
        FocusScope.of(context).unfocus(); // これで候補が閉じる
    },
    // 省略
    CustomTypeAheadField(),
)

Discussion