見出し画像

Flutter: シンプルでStatelessな検索機能を実装してみた話

最近 Flutter大学を参考にさせていただきながらアプリを作っています。
シンプルな検索機能を作ったので共有できればと思います。

画像1

達成したいこと

文字列のリストが与えられている状況を考えます。いわゆる Search Bar にテキストを打ち込んで、部分的に一致する要素があれば候補リストとして表示させたいです。

"りんご" という文字列があったとすると、"り" や "んご" と入力されていれば候補リストに出てきてほしい。

それではやっていきましょう。

用意したサンプルデータ

サンプル用にしりとり的なデータを用意しました。

// しりとりみたいなサンプルデータ
// これは任意の文字列リストで大丈夫です

  List<String> searchList = [
   "りんご",
   "ごりら",
   "ラッパ",
   "狸",
   "きつね",
   "ねこ",
   "こま",
   "まんとひひ",
   "ヒント",
   "トマト",
   "トートロジー"
 ];

実装の方針

1. 入力を検知してその度にリスト内に検索処理をかける
2.リストの検索処理を考える
3. 検索結果に応じてListTileを生成して並べる

この手順で実装していきます。
細かい部分はコード内のコメントに記述するとして、それぞれの手順で肝となる要素を紹介していければと思います。

1. 入力を検知してその度にリスト内に検索処理をかける

これは TextField の onChanged を使えばできます。公式リファレンス

 TextField(
     onChanged: (text) {
       model.text = text; // <= modelのtext変数に値を渡す
       model.search(); // <= model内に定義した関数で検索処理を実行
     },
 ...

2. リストの検索処理を考える

とても力技の実装で申し訳ないのですが List から String の要素を一つずつ取り出して contains で調べています。

contains は文字列の中に一致する部分があれば true を返すメソッドです。

// [検索対象の文字列] .contains ( [検索ワードの文字列] )

'りんご'.contains('りご'); // これは false になる
'りんご'.contains('り'); // これは true になる
// 検索処理の中身
  search() {
   if (this.text.isNotEmpty) { // 何か文字が入力された実行する
     searchResultList.clear(); // .add で増やしているので毎回clearしている
     // ここから検索処理
     this.searchList.forEach(
       (element) {
         if (element.contains(this.text)) { // .contains で文字列の部分一致を判定できる
           searchResultList.add(element); // 一致している要素があれば追加する
         }
       },
     );
     notifyListeners(); // これを実行すると再描画される
   }
 }

3. 検索結果に応じて ListTile を生成して並べる

List.builder を使うと要素に応じて ListTile を並べることができます。

ListView.builder(
 itemCount: model.searchResultList.length, // ここで要素数を指定できる
 itemBuilder: (BuildContext context, int index) {
   return _searchList(context, model, index); // 上で指定した数だけ繰り返す
 },
),

_searchList という Widget は別で定義しています。

 Widget _searchList(BuildContext context, SearchModel model, int index) {
   return ListTile(
     title: Text(model.searchResultList.elementAt(index)), // 候補リストのListTileを生成
   );
 }

これで要素が出揃いました。
最後に全体のコードを掲載しておしまいにしたいとお思います。何かのお役に立てれば幸いです。

// search_page.dart

// このあたりは環境に合わせたpathを指定してください
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:whiskit_app/presentation/search/search_model.dart';

// プロバイダーモデルを使ってシンプルな検索機能を実装してみる
class SearchPage extends StatelessWidget {
 Widget build(BuildContext context) {
   return ChangeNotifierProvider<SearchModel>(
     create: (_) => SearchModel(),
     // Consumerを使えば上で宣言したmodelにアクセスできるみたいです
     child: Consumer<SearchModel>(builder: (context, model, child) {
       return Scaffold(
         appBar: PreferredSize(
           // PreferredSize は appBar の高さを調整するために使ってます
           preferredSize: Size.fromHeight(40.0),
           child: AppBar(
             backgroundColor: Colors.black54,
             title: TextField(
               onChanged: (text) {
                 // onChangedは入力されている文字が変更するたびに呼ばれます
                 model.text = text;
                 model.search();
               },
               decoration: new InputDecoration(
                 prefixIcon: new Icon(Icons.search, color: Colors.white),
                 hintText: "名前で調べる...",
               ),
             ),
           ),
         ),
         body: Column(
           children: [
             Text('検索結果がリストで表示されます'),
             Expanded(
               // Column内でListView.builderを使う場合Expandedなどで描画を引き伸ばしおかないと無理みたいです。
               child: ListView.builder(
                 itemCount:
                     model.searchResultList.length, // ここで要素数を指定できる 検索結果の長さを渡す
                 itemBuilder: (BuildContext context, int index) {
                   return _searchList(context, model, index); // 上で指定した数だけ繰り返す
                 },
               ),
             ),
           ],
         ),
       );
     }),
   );
 }

 Widget _searchList(BuildContext context, SearchModel model, int index) {
   return ListTile(
     title: Text(model.searchResultList.elementAt(index)), // 候補リストのListTileを生成
   );
 }
}
// search_model.dart

import 'package:flutter/cupertino.dart';

class SearchModel extends ChangeNotifier {
 String text = ''; // TextFieldの値を受け取ります

 // 適当なリストを用意
 // Firestoreなどからもってきて使うなど応用してください
 List<String> searchList = [
   "りんご",
   "ごりら",
   "ラッパ",
   "狸",
   "きつね",
   "ねこ",
   "こま",
   "まんとひひ",
   "ヒント",
   "トマト",
   "トートロジー"
 ];

 List<String> searchResultList = []; // 検索結果が渡されます

 // 検索の中身
 search() {
   if (this.text.isNotEmpty) {
     // 何か文字が入力された実行する
     searchResultList.clear(); // .add で増やしているので毎回clearしている
     // ここから検索処理
     this.searchList.forEach(
       (element) {
         if (element.contains(this.text)) {
           // .contains で文字列の部分一致を判定できる
           searchResultList.add(element); // 一致している要素があれば追加する
         }
       },
     );
     notifyListeners(); // これを実行すると再描画される
   }
 }
}

ひらがなカタカナ区別せず検索みたいなことをしたかったのですが、分からなかったので、何か分かればまた共有します!

パフォーマンスがかなり悪いゴリ押し実装なので、他にいい方法があれば教えていただけたら嬉しいです。

最後まで読んでもらえて嬉しいです。よければフォローもお待ちしています。サポートは記事を書くときのコーヒーになります。