対象者
- FlutterでAutocompleteウィジェットを深く理解したい方
- カスタマイズされた入力フォームを作成したい開発者
- ユーザー体験を向上させるためのサジェスト機能に興味があるエンジニア
はじめに
以前、FlutterのAutocomplete
ウィジェットについてブログを書きました。改めて使ってみると、つまずく点がありました。特に、サジェストリストの消し方や、送信ボタンを押したときの挙動など、基本的な使い方から一歩進んだ応用的な部分での課題が見つかりました。今回は、その経験を踏まえて、Autocomplete
ウィジェットの応用編として解説していきます。
Autocompleteの基本
デザイン方法などは以下を参照してください。今回は主に、ボタンなど他のWidgetとの連携がメインになります
Autocompleteウィジェットの応用実装
サジェストリストにユーザー入力を含める
サジェストリストに、あらかじめ用意したオプションだけでなく、ユーザーが直前に入力・送信した文字列も含めたい場合があります。そのために、_fetchOptions
関数を以下のように変更しました。
Future<List<String>> _fetchOptions(String value) async {
await Future.delayed(const Duration(milliseconds: 300));
return [if (_submittedText.isNotEmpty) _submittedText, ..._options]
.where((e) => e.toLowerCase().contains(value.toLowerCase()))
.toList();
}
これにより、送信された文字列_submittedText
がサジェストリストの先頭に追加されます。実際のアプリでは、ユーザ履歴をもう少し多めに取り、重複があれば削除しています。
サジェストリストに「履歴」と表示する
送信済みの文字列をサジェストリストに表示する際、それが履歴であることをユーザーに示すために、displayStringForOption
をカスタマイズします。
displayStringForOption: (value) =>
(value == _submittedText ? '[履歴]' : '') + value,
これにより、送信済みの文字列には[履歴]
が付加されて表示されます。ユーザが入力したものかどうかを区別できるようになります。
このメソッドを知ったことで、Autocomplete
はString
だけでなく任意のクラスをサジェストに使用でき、ユーザーに見せる項目を設定できると気づきました。
onSelectedの役割
onSelected
は、ユーザーがサジェストリストからアイテムを選択したときに呼び出されるコールバックです。選択された値を受け取り、必要な処理を行います。
onSelected: (String selection) => _onSelected(selection),
ここでは、選択された値を_selectedText
にセットしています。
fieldViewBuilderの使い方
fieldViewBuilder
は、カスタムの入力フィールドを構築するためのウィジェットを返します。Autocomplete
ウィジェットに独自のテキストフィールドを組み込みたい場合に使用します。
fieldViewBuilder: (
BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted,
) {
return Row(
children: [
Expanded(
child: TextField(
controller: fieldTextEditingController,
focusNode: fieldFocusNode,
decoration: const InputDecoration(
labelText: 'フルーツを入力',
),
onSubmitted: (value) => _onSubmitted(value),
onChanged: (String value) {
setState(() {
_selectedText = value;
_isSubmitted = false;
});
},
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {
final text = fieldTextEditingController.text;
_onSubmitted(text);
fieldTextEditingController.text = text;
// onFieldSubmitted();
},
child: const Text('送信'),
),
],
);
},
このコードでは、TextField
と送信
ボタンを横並びに配置し、ユーザーが入力や送信を行えるようにしています。
サジェストリストを消す方法
送信ボタンを押した後、サジェストリストを消したい場合があります。しかし、一般的な実装ではテキストフィールドをクリアするため、今回は入力した単語をそのまま残す必要があります。そこで、optionsBuilder
で空のリストを返すことでサジェストリストを非表示にします。
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty || _isSubmitted) {
return const Iterable<String>.empty();
}
return _fetchOptions(textEditingValue.text);
},
_isSubmitted
がtrue
の場合、空のリストを返してサジェストリストを消します。最初はフォーカスが外れたら自動的に消えると考えていたため、戸惑いました。そうではなく、空リストをサジェストさせる処理が必要でした。
optionsBuilderを再度実行するための工夫
サジェストワードを消すには、optionsBuilder
が空リストになる条件を作った上で、optionsBuilder
を再度実行してサジェストリストを更新する必要があります。そのためには、以下のいずれかの方法でonChanged
を発火させます。
fieldTextEditingController.text
を更新するfieldTextEditingController.clear()
を呼び出すonFieldSubmitted()
を実行する- ただし、
onPressed: () {
final text = fieldTextEditingController.text;
_onSubmitted(text);
fieldTextEditingController.text = text; // テキストを再設定
onFieldSubmitted(); // または、fieldTextEditingController.clear();
},
これにより、optionsBuilder
が再度実行され、サジェストリストが更新されます。ユースケースに応じて使い分けてください。
コントローラとフォーカスノードの結びつけ
既存のテキストフィールドにAutocomplete
を追加する場合、テキストフィールドのcontroller
とfocusNode
をAutocomplete
から渡されるものと結びつける必要があります。
TextField(
controller: fieldTextEditingController,
focusNode: fieldFocusNode,
// ...
),
これを怠ると、サジェストリストが正しく表示されなかったり、意図しない挙動を示すことがあります。
Riverpodとの連携
状態管理としてRiverpodを使っていました。そのため、Autocompleteを含むWidgetの引数として、外からRiverpodの値を渡していました。そして Autocomplete作成時のRiverpodの値が渡され、状態で差異が出ました。(Autocompleteを含むWuidgetを再利用したいため、内部にRiverpodのProviderを持ちたくない)
最終的にWidgetを作った時点の「値」ではなく、取得する「メソッド」を引数にすることで、optionsBuilder
実行時のRiverpodの値が取得できるようになり、意図した動作になりました。
Q&A
Q1: サジェストリストが消えない場合の対処法は?
A1: optionsBuilder
で空のリストを返すようにします。具体的には、送信後にフラグ(例:_isSubmitted
)を立てて、optionsBuilder
内でそのフラグをチェックし、空のリストを返します。
Q2: optionsBuilder
を再度実行させるにはどうすればいいですか?
A2: 以下のいずれかの方法でoptionsBuilder
を再度実行できます。
fieldTextEditingController.text
を更新する:テキストを再設定することで、onChanged
が発火し、optionsBuilder
が再度実行されます。fieldTextEditingController.clear()
を呼び出す:テキストフィールドをクリアすることで、onChanged
が発火します。onFieldSubmitted()
を実行する:fieldViewBuilder
の第四引数であるonFieldSubmitted
を呼び出すことで、optionsBuilder
を再度実行できます。
Q3: 既存のテキストフィールドにAutocomplete
を追加するときの注意点は?
A3: テキストフィールドのcontroller
とfocusNode
を、Autocomplete
から渡されるfieldTextEditingController
とfieldFocusNode
に設定する必要があります。これをしないと、サジェスト機能が正しく動作しません。
まとめ
今回は、Autocomplete
ウィジェットの応用的な使い方について解説しました。特に、ユーザーの入力履歴をサジェストに含めたり、サジェストリストの表示・非表示を制御する方法について詳しく説明しました。
これらの実装により、ユーザー体験をより向上させることができます。開発中につまずいた点や解決策も共有しましたので、同じような課題に直面した際の参考になれば幸いです。
ソース(main.dartにコピペして動作確認用)
import 'package:flutter/material.dart';
void main() => runApp(AutoCompleteSample());
class AutoCompleteSample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('AutoComplete Sample')),
body: AutoCompleteWidget(),
),
);
}
}
class AutoCompleteWidget extends StatefulWidget {
@override
_AutoCompleteWidgetState createState() => _AutoCompleteWidgetState();
}
class _AutoCompleteWidgetState extends State<AutoCompleteWidget> {
final List<String> _options = [
'Apple',
'Banana',
'Cherry',
'Date',
'Grape',
'Orange',
'Pineapple',
];
var _selectedText = '';
var _submittedText = '';
var _isSubmitted = false;
void _onSelected(String value) {
setState(() {
_selectedText = value;
});
}
void _onSubmitted(String value) {
setState(() {
_submittedText = value;
_isSubmitted = true;
});
}
Future<List<String>> _fetchOptions(String value) async {
await Future.delayed(const Duration(milliseconds: 300));
return [if (_submittedText.isNotEmpty) _submittedText, ..._options]
.where((e) => e.toLowerCase().contains(value.toLowerCase()))
.toList();
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Column(
children: [
Autocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty || _isSubmitted) {
return const Iterable<String>.empty();
}
return _fetchOptions(textEditingValue.text);
},
onSelected: (String selection) => _onSelected(selection),
displayStringForOption: (value) =>
(value == _submittedText ? '[履歴]' : '') + value,
fieldViewBuilder: (
BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted,
) {
return Row(
children: [
Expanded(
child: TextField(
controller: fieldTextEditingController,
focusNode: fieldFocusNode,
decoration: const InputDecoration(
labelText: 'フルーツを入力',
),
onSubmitted: (value) => _onSubmitted(value),
onChanged: (String value) {
setState(() {
_selectedText = value;
_isSubmitted = false;
});
},
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {
final text = fieldTextEditingController.text;
_onSubmitted(text);
fieldTextEditingController.text = text;
// onFieldSubmitted();
},
child: const Text('送信'),
),
],
);
},
),
const Spacer(),
Text('入力された文字列: $_selectedText'),
Text('送信された文字列: $_submittedText'),
],
),
);
}
}