Flutterでテキスト検索機能を実装する方法

image

はじめに

今回は、Flutterで検索機能を実装する方法をご紹介します。
配列の中から、キーワードが含まれる要素だけを表示したいケースがよくありますよね。
そんな時にサクッとコピペで実装できる、テキスト検索の方法をご紹介します。
この記事では、

  • 完全一致での検索
  • 部分一致での検索
  • 検索機能で使えるテクニック
    を説明します。

開発環境

  • Flutter version 2.10.3
  • Dart version 2.16.1

前提

['Bob', 'John', 'Fred', 'Emma','Charlotte']という名前の配列を使って、検索機能を説明していきます。 まずは、名前が入っている配列(データの配列)と検索結果が入る配列の2つを用意します。

  static const nameList = ['Bob', 'John', 'Fred', 'Emma', 'Charlotte'];
  List<String> searchedNames = [];

今回の例では、以下のような実装をしています。
TextFieldonChanged部分のsearchの処理によって、検索結果searchedNamesが変化します。

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('検索'), centerTitle: true),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('検索フォーム', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            TextField(
onChanged: search,
decoration: InputDecoration(contentPadding: EdgeInsets.symmetric(horizontal: 16)), ), const SizedBox(height: 16), Text('検索結果', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 16), Text('Result : ${searchedName.toString()}') ], ), ), ); }

完全一致での検索

all match

  • 検索のキーワードが空の場合は、検索結果を空にする
  • その他は完全一致している名前を抽出する (statefulWidgetを使っているので、setStateが入っています)
  void search(String text) {
    setState(() {
      if (text.trim().isEmpty) {
        searchedNames = [];
      } else {
searchedNames = nameList.where((element) => element == text).toList();
} }); }

部分一致での検索

partial match

  • 検索のキーワードが空の場合は、検索結果を空にする
  • その他はcontainsを使って部分一致している名前を抽出する
  void search(String text) {
    setState(() {
      if (text.trim().isEmpty) {
        searchedNames = [];
      } else {
searchedNames = nameList.where((element) => element.contains(text)).toList();
} }); }

検索機能で使えるテクニック

検索処理にAPIを利用する場合は、TextFieldonChangedのイベントが走るたびに、APIを叩くことになります。
結果的に動作が重くなったり、検索自体がうまくいかない場合があります。
そんな時には、ユーザーが入力し終えたら検索処理をするという実装をすると良いと思います。
「ユーザーが最後の文字を打ってから、1秒経過したら検索をかける」という場合の例をご紹介します。

search delayed

具体例

まずは、遅らせる秒数を定義します。(各自で調整してください)
そして、最後に文字を入力した日時をStatefulWidgetのStateに保持します。

static const searchDelayMillSec = 1000;
DateTime _lastChangedDate = DateTime.now();

検索を遅らせる処理は以下の通りです。

  • 文字を打つたびに_lastChangedDateが更新される
  • その1秒後にFuture.delayedの中の検索処理が走る という流れです。
 void delayedSearch(String text) {
    Future.delayed(const Duration(milliseconds: searchDelayMillSec), () {
      final nowDate = DateTime.now();
if (nowDate.difference(_lastChangedDate).inMilliseconds > searchDelayMillSec) {
_lastChangedDate = nowDate; search(text); } }); //キーワードが入力されるごとに、検索処理を待たずに_lastChangedDateを更新する
_lastChangedDate = DateTime.now();
}

Future.delayedの中の検索処理が走る前に次の文字を入力すると、_lastChangedDateが再度更新され、

if (nowDate.difference(_lastChangedDate).inMilliseconds > searchDelayMillSec)

という条件はfalseになります。
したがって、1秒以内に次の文字が入力された場合には、検索処理は走りません。
以下にコード全文をコピペ用に置いておきます。
少し難しいので、実際に動かしてみるとイメージがつきやすいかもしれません。

コード全文

import 'package:flutter/material.dart';

void main() => runApp(const SearchApp());

class SearchApp extends StatelessWidget {
  const SearchApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: SearchScreen(),
    );
  }
}

class SearchScreen extends StatefulWidget {
  const SearchScreen({Key? key}) : super(key: key);

  
  State<SearchScreen> createState() => _SearchScreenState();
}

class _SearchScreenState extends State<SearchScreen> {
  static const searchDelayMillSec = 1000;
  static const nameList = ['Bob', 'John', 'Fred', 'Emma', 'Charlotte'];
  List<String> searchedNames = [];
  DateTime _lastChangedDate = DateTime.now();

  void search(String text) {
    setState(() {
      if (text.trim().isEmpty) {
        searchedNames = [];
      } else {
        searchedNames = nameList.where((element) => element.contains(text)).toList();
      }
    });
  }

  void delayedSearch(String text) {
    Future.delayed(const Duration(milliseconds: searchDelayMillSec), () {
      final nowDate = DateTime.now();
      if (nowDate.difference(_lastChangedDate).inMilliseconds > searchDelayMillSec) {
        _lastChangedDate = nowDate;
        search(text);
      }
    });
    //キーワードが入力されるごとに、検索処理を待たずに_lastChangedDateを更新する
    _lastChangedDate = DateTime.now();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('テキスト検索'), centerTitle: true),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('検索フォーム', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            TextField(
              onChanged: delayedSearch,
              decoration: InputDecoration(contentPadding: EdgeInsets.symmetric(horizontal: 16)),
            ),
            const SizedBox(height: 16),
            Text('検索結果', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            const SizedBox(height: 16),
            Text('Result : ${searchedNames.toString()}')
          ],
        ),
      ),
    );
  }
}

最後に

いかがでしたか。
StatefulWidgetを用いて、原始的な方法でFlutterでのテキスト検索を実装してみました。
こちらの例を参考にカスタマイズしていただけたら幸いです。

関連

テキストフィールドを実装するに当たってのコツは、こちらで紹介しています。 https://www.kamo-it.org/blog/textfield-focus/

お知らせ

5月28日開催のアプリ開発講座の参加者募集中!!

5月28日開催のアプリ開発講座の参加者募集中!!

5月28日にアプリ開発講座を開催します!会場は岐阜県美濃加茂市のコワーキングスペース「こやぁね」です。興味のある方は是非ご参加ください!

Read More
可茂IT塾ではFlutterインターンを募集しています!

可茂IT塾ではFlutterインターンを募集しています!

可茂IT塾ではFlutterインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。

Read More

お知らせ

5月28日開催のアプリ開発講座の参加者募集中!!

5月28日開催のアプリ開発講座の参加者募集中!!

5月28日にアプリ開発講座を開催します!会場は岐阜県美濃加茂市のコワーキングスペース「こやぁね」です。興味のある方は是非ご参加ください!

Read More
可茂IT塾ではFlutterインターンを募集しています!

可茂IT塾ではFlutterインターンを募集しています!

可茂IT塾ではFlutterインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。

Read More