アフィリエイト広告を利用しています

【Flutter】AppBarに簡単な検索機能を実装する方法

AppBarに簡単な検索機能を実装する方法を紹介します。

初心者向けにステップを分けて説明します。

私自身まだまだプログラミング初心者で、用語など間違えているところもあるかもしれませんがご了承ください。

今回のゴール↓

実行環境

  • DartPad
  • Flutter 2.8.0
  • Dart SDK 2.15.0

ベース

今回、ベースとなるUIはこちらです。

Flutterアプリを作成したときに生成されるカウンターアプリを編集したものです。

コードは以下の通りです。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;

  const MyHomePage({
    Key? key,
    required this.title,
  }) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<String> _list = ['English Textbook', 'Japanese Textbook', 'English Vocabulary', 'Japanese Vocabulary'];

  Widget _defaultListView() {
    return ListView.builder(
      itemCount: _list.length,
      itemBuilder: (context, index) {
        return Card(
          child: ListTile(
            title: Text(_list[index])
          )
        );
      }
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: _defaultListView()
    );
  }
}

ListView.builderを使用しています。
ListViewをご存じない方は以下の記事がおすすめです。

AppBarの編集

検索アイコンボタンを追加

AppBarに検索アイコンボタンを追加します。

actionsを追加し、IconButtonを追加します。

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
        actions: [ //追加
          IconButton(
            icon: Icon(Icons.search),
            onPressed: () {}
          )
        ]
      ),
      body: _defaultListView()
    );
  }

検索モードとの切り替え

次に、検索ボタンを押したら検索モードに移行し、バツボタンを押したら検索モードを終了する機能を追加します。

_searchBoolean

まず、検索機能を切り替えるため、_searchBoolean変数を用意します。

デフォルトでは検索モードはオフにするので、_searchBooleanの値はfalseにしておきます。

class _MyHomePageState extends State<MyHomePage> {
  bool _searchBoolean = false; //追加
  
  //
}

onPressed

次に、検索ボタンを押したら検索モードに移行するために、検索ボタンのonPressedを以下のようにします。

UIを切り替えるために、setStateが必要です。

//
        actions: [
          IconButton(
            icon: Icon(Icons.search),
            onPressed: () {
              setState(() { //追加
                _searchBoolean = true;
              });
            }
          )
        ]
//

clear button

そうしたら、検索モードを終了するために、バツボタンを追加します。

つまり、デフォルト(_searchBoolean == false)では検索ボタンを表示させ、検索モード(_searchBoolean == true)ではバツボタンを表示させるようにします。

この切替のために、actionsを以下のようにします。

// 
      actions: !_searchBoolean
        ? [
          IconButton(
          icon: Icon(Icons.search),
          onPressed: () {
            setState(() {
              _searchBoolean = true;
            });
          })
        ] 
        : [
          IconButton(
            icon: Icon(Icons.clear),
            onPressed: () {
              setState(() {
                _searchBoolean = false;
              });
            }
          )
        ]
//

ここでは、三項演算子を使用しています。
※三項演算子とは、(条件)?(条件がtrueの場合):(条件がfalseの場合)という式による記述方法です。
if文でWidgetを分岐させようとすると、即時関数というカッコがたくさんある記述方法が必要になるため、三項演算子を使いました。即時関数の詳細は以下の記事がおすすめです。

また、条件となる_searchBooleanに!をつけることで、_searchBooleanがfalseの場合を先に記述するようにしています。
こうしているのは、デフォルトのほうを先に記述したほうが感覚的にわかりやすいと思ったからです。

検索バーを作成

さて、いよいよ検索バーを作成します。

検索バーは、AppBarのtitleにTextFieldを指定することで実装します。

検索バーWidgetを作成

まず、検索バーWidgetを作成します。

長めのコードになるため、AppBarのところに直接記述するのではなく、_searchTextFieldとして別に記述します。

ここでは、とりあえずTextField()だけにしておきます。

class _MyHomePageState extends State<MyHomePage> {
  bool _searchBoolean = false;
  
  List<String> _list = ['English Textbook', 'Japanese Textbook', 'English Vocabulary', 'Japanese Vocabulary'];
  
  Widget _searchTextField() { //追加
    return TextField();
  }

  Widget _defaultListView() {
    //

検索バーを表示

次に、検索バーが表示されるようにします。

アイコンボタンと同様に、デフォルト(_searchBoolean == false)ではタイトルテキスト(今回はFlutter Demo Home Page)を表示させ、検索モード(_searchBoolean == true)では検索バーを表示させるようにします。

    //
      appBar: AppBar(
        title: !_searchBoolean ? Text(widget.title) : _searchTextField(),
    //

TextFieldの編集

検索アイコンを押し、TextFieldをタップして文字を入力してみると、以下のようになります。

これを見ると、カーソルがテーマーカラーになっているため見えていないことがわかります。
そのため、カーソルの色を白くしてみます。
他にも見た目や機能を変更してみます(コメントで説明しています)。

  Widget _searchTextField() {
    return TextField(
      autofocus: true, //TextFieldが表示されるときにフォーカスする(キーボードを表示する)
      cursorColor: Colors.white, //カーソルの色
      style: TextStyle( //テキストのスタイル
        color: Colors.white,
        fontSize: 20,
      ),
      textInputAction: TextInputAction.search, //キーボードのアクションボタンを指定
      decoration: InputDecoration( //TextFiledのスタイル
        enabledBorder: UnderlineInputBorder( //デフォルトのTextFieldの枠線
          borderSide: BorderSide(color: Colors.white)
        ),
        focusedBorder: UnderlineInputBorder( //TextFieldにフォーカス時の枠線
          borderSide: BorderSide(color: Colors.white)
        ),
        hintText: 'Search', //何も入力してないときに表示されるテキスト
        hintStyle: TextStyle( //hintTextのスタイル
          color: Colors.white60,
          fontSize: 20,
        ),
      ),
    );
  }

以下のようになります。

textInputActionでsearchを指定したので、キーボード青いボタンが検索用のものになっています。

Android
iOS

他にもいろいろと変更できます。詳しくは以下の公式ページをご覧ください。

これで、AppBarの編集はできました。

※Prefer const with constant constructors.という情報が表示されると思います。constを記述すると、そのWidgetの再ビルドを抑制することができるため、その情報が表示されたところにはconstを記述するのがおすすめです。
ただこの記事では、説明の都合上省略しています。
このconstの記述によるパフォーマンス向上については以下の記事をご覧ください。

検索機能の実装

次に、検索機能を実装します。

ListViewの切り替え

現在、bodyに_defaultListViewを指定していますが、検索機能時にはListViewを切り替えて、検索された文字と一致するリストのみ表示されるようにします。

_searchListViewの作成

まずは、_defaultListViewをコピー・ペーストし、_searchListViewを作成します。

//
  Widget _searchListView() { //追加
    return ListView.builder(
      itemCount: _list.length,
      itemBuilder: (context, index) {
        return Card(
          child: ListTile(
            title: Text(_list[index])
          )
        );
      }
    );
  }
  
  Widget _defaultListView() {
  //

ListView切り替え

アイコンボタンや検索バーと同様、デフォルト(_searchBoolean == false)ではbodyに_defaultListViewを表示させ、検索モード(_searchBoolean == true)ではbodyに_searchListViewを表示させるようにします。

    //
      appBar: AppBar(
        //
      ),
      body: !_searchBoolean ? _defaultListView() : _searchListView()
    );
  }
}

検索機能

今回の検索機能では、検索された文字が含まれるリストのindexを_searchIndexListというリストに入れ、それをもとに_searchListViewで表示します。

_searchIndexList作成

まず、_searchIndexListを作成します。

indexはint型なので、List<int>にします。

//
class _MyHomePageState extends State<MyHomePage> {
  bool _searchBoolean = false;
  List<int> _searchIndexList = []; //追加
//

onChanged追加

次に、検索された文字が含まれるリストのindexを_searchIndexListに入れる処理を追加します。

今回は、検索された文字を変えるごとにリストを変える処理にします。

TextField内が代わる際の処理を追加するには、onChangedを追加します。
※キーボードの検索ボタンを押した際に処理をする場合は、onSubmittedを使います。

//
  Widget _searchTextField() {
    return TextField(
      onChanged: (String s) { //追加
        setState(() {
          _searchIndexList = [];
          for (int i = 0; i < _list.length; i++) {
            if (_list[i].contains(s)) {
              _searchIndexList.add(i);
            }
          }
        });
      },
    //

onChanged: にある(String s)で、入力された文字(String)を変数(s)としています。

UIを切り替えるためsetStateを使い、その中でリストのすべての要素を処理するためのfor文を使っています。
※for文:for (初期値; 条件式; 変化式) {条件式がtrueの間の処理}

このfor文の中のif文は、リストの要素(_list[i])に検索された文字(s)が含まれていれば、_searchIndexListにindex(i)を追加するという処理です。

また、このfor文の前には、_searchIndexListを初期化するために_searchIndexList = [];を記述しています。

 

そして、検索ボタンを押したときにも_searchIndexListを初期化するために、検索ボタンのonPressedに_searchIndexList = [];を追加します。

  //
      appBar: AppBar(
        title: !_searchBoolean ? Text(widget.title) : _searchTextField(),
        actions: !_searchBoolean
        ? [
          IconButton(
          icon: Icon(Icons.search),
          onPressed: () {
            setState(() {
              _searchBoolean = true;
              _searchIndexList = []; //追加
            });
          })
        ] 
  //

_searchListViewに反映

では、この_searchIndexListを_searchListViewに反映させます。

まず、itemCountを_searchIndexList.lengthにして、表示されるリストの数を_searchIndexListの要素の数に合わせます。

そして、itembuilder内のindexを、_searchIndexListに入れたindexにするために、index = _searchIndexList[index];を追加します。

//
  Widget _searchListView() {
    return ListView.builder(
      itemCount: _searchIndexList.length, //編集
      itemBuilder: (context, index) {
        index = _searchIndexList[index]; //追加
        return Card(
          child: ListTile(
            title: Text(_list[index])
          )
        );
      }
    );
  }
//

これで、_searchIndexListを_searchListViewに反映させることができ、検索機能が使えるようになりました。

全体コード

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;

  const MyHomePage({
    Key? key,
    required this.title,
  }) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool _searchBoolean = false;
  List<int> _searchIndexList = [];
  
  List<String> _list = ['English Textbook', 'Japanese Textbook', 'English Vocabulary', 'Japanese Vocabulary'];

  Widget _searchTextField() {
    return TextField(
      onChanged: (String s) {
        setState(() {
          _searchIndexList = [];
          for (int i = 0; i < _list.length; i++) {
            if (_list[i].contains(s)) {
              _searchIndexList.add(i);
            }
          }
        });
      },
      autofocus: true,
      cursorColor: Colors.white,
      style: TextStyle(
        color: Colors.white,
        fontSize: 20,
      ),
      textInputAction: TextInputAction.search,
      decoration: InputDecoration(
        enabledBorder: UnderlineInputBorder(
          borderSide: BorderSide(color: Colors.white)
        ),
        focusedBorder: UnderlineInputBorder(
          borderSide: BorderSide(color: Colors.white)
        ),
        hintText: 'Search',
        hintStyle: TextStyle(
          color: Colors.white60,
          fontSize: 20,
        ),
      ),
    );
  }
  
  Widget _searchListView() {
    return ListView.builder(
      itemCount: _searchIndexList.length,
      itemBuilder: (context, index) {
        index = _searchIndexList[index];
        return Card(
          child: ListTile(
            title: Text(_list[index])
          )
        );
      }
    );
  }
  
  Widget _defaultListView() {
    return ListView.builder(
      itemCount: _list.length,
      itemBuilder: (context, index) {
        return Card(
          child: ListTile(
            title: Text(_list[index])
          )
        );
      }
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: !_searchBoolean ? Text(widget.title) : _searchTextField(),
        actions: !_searchBoolean
        ? [
          IconButton(
          icon: Icon(Icons.search),
          onPressed: () {
            setState(() {
              _searchBoolean = true;
              _searchIndexList = [];
            });
          })
        ] 
        : [
          IconButton(
            icon: Icon(Icons.clear),
            onPressed: () {
              setState(() {
                _searchBoolean = false;
              });
            }
          )
        ]
      ),
      body: !_searchBoolean ? _defaultListView() : _searchListView()
    );
  }
}

あとがき

ということで、AppBarに簡単な検索機能を実装する方法を説明しました。

今回は、簡単な検索機能ということで、検索した文字が含まれるかというのを.contains()で実装しました。

より複雑な検索機能にしたい場合は、その部分を工夫してみましょう。

例えば、現在のコードでは大文字と小文字を区別することになっていますが、区別しない場合は、.contains()を以下のようにすることで実装できます。
※toLowerCase()は小文字にするメソッドです。

    //
            if ((_list[i].toLowerCase()).contains(s.toLowerCase())) {
              _searchIndexList.add(i);
            }
    //

次の記事では、検索機能の発展編として、Chromeのページ内検索のような機能を実装する方法をまとめる予定です。

Flutterについてもっと学びたい方はこちら↓