🔍
【Flutter】flutter_typeaheadで検索補完してみた
経緯
飲んだ日本酒を記録する個人開発アプリを使用していると、毎回入力が面倒な箇所がありました。
具体的には飲んだ場所の入力です。同じ店で続けて飲むことが多いので、同じ場所を入力したかったので、検索候補を出して手間を省こうと思います。
検索候補のイメージは以下です。
ユーザの入力に合わせて候補が動的に変わる仕組みです。
ライブラリの選定
候補として以下が挙がりました。
- Autocomplete
- flutter_typeahead
デザインにはCupertinoを使用しているため、flutter_typeaheadを選びました。
(AutocompleteはMaterialクラスだったので)
ライブラリの説明にきちんと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