Flutter 検索バー作成

#更新履歴

2022/03/20
・書き方を修正し、githubのリポジトリーを追加
Flutter : 2.2.1　→ 2.10.0に変更

#背景

以前書いた記事
[flappy_search_barの使い方](https://qiita.com/mkurom/items/e94b60eea15d4239ac43)
で使用したライブラリーがDISCONTINUEDになったことと、やりたかったことができない部分があったので、自作してみた。

#環境

PC : macOS Monterey（インテルCPU）
エディター : Visual Studio Code
Flutter : 2.10.0

#状態管理
provider + changenotifier

#コード

```main.dart
import 'package:flutter/material.dart';
import 'package:flutter_search_bar_sample/view/app_page.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'search bar sample app',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const AppPage(),
    );
  }
}

```

```app_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_search_bar_sample/view/search_bar_controller.dart';
import 'package:provider/provider.dart';

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

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) {
        final controller = SearchBarController();
        controller.init();
        return controller;
      },
      child: Consumer<SearchBarController>(
        builder: (BuildContext context, SearchBarController searchBarController,
            Widget? child) {
          if (searchBarController.isLoading) {
            return const Scaffold(
              body: Center(
                child: CircularProgressIndicator(),
              ),
            );
          }

          return Scaffold(
            key: searchBarController.globalKey,
            appBar: AppBar(
              centerTitle: true,
              title: _appBarTitle(searchBarController),
              actions: [
                IconButton(
                  icon: _appbarIcon(searchBarController),
                  onPressed: searchBarController.isSearching
                      ? searchBarController.searchEnd
                      : searchBarController.searchStart,
                ),
              ],
            ),
            body: _listWidget(searchBarController),
          );
        },
      ),
    );
  }

  Widget _appbarIcon(SearchBarController searchBarController) {
    return searchBarController.isSearching
        ? const Icon(
            Icons.close,
            color: Colors.white,
          )
        : const Icon(
            Icons.search,
            color: Colors.white,
          );
  }

  Widget _appBarTitle(SearchBarController searchBarController) {
    return searchBarController.isSearching
        ? TextField(
            autofocus: true,
            cursorColor: Colors.white,
            style: const TextStyle(
              color: Colors.white,
            ),
            decoration: const InputDecoration(
              prefixIcon: Icon(
                Icons.search,
                color: Colors.white,
              ),
              hintText: 'Search...',
              hintStyle: TextStyle(
                color: Colors.white,
              ),
            ),
            onChanged: (word) {
              searchBarController.searchOperation(word);
            },
          )
        : const Text(
            '検索リスト',
            style: TextStyle(
              color: Colors.white,
            ),
          );
  }

  Widget _listWidget(SearchBarController searchBarController) {
    final itemList = (searchBarController.isSearching &&
            searchBarController.searchedText.isNotEmpty)
        ? searchBarController.searchedItemList
        : searchBarController.allItemList;

    if (itemList.isEmpty) {
      return Center(
        child: Text('"${searchBarController.searchedText}"を含む文字列は見つかりませんでした。'),
      );
    } else {
      return Container(
        color: Colors.white,
        child: ListView.builder(
          key: const PageStorageKey(0),
          controller: searchBarController.scrollController,
          itemCount: itemList.length,
          itemBuilder: (context, index) {
            return SizedBox(
              height: 60,
              child: Card(
                child: Center(
                  child: Text(itemList[index]),
                ),
              ),
            );
          },
        ),
      );
    }
  }
}
```


```search_bar_controller.dart
import 'package:flutter/material.dart';
import 'package:flutter_search_bar_sample/sample_data.dart';

class SearchBarController extends ChangeNotifier {
  final globalKey = GlobalKey<ScaffoldState>();

  /// state
  bool isLoading = true;
  bool isSearching = false;
  String _searchedText = '';

  var allItemList = <String>[];
  var searchedItemList = <String>[];

  String get searchedText => _searchedText;

  final scrollController = ScrollController();

  void init() {
    fetch();
  }

  Future<void> fetch() async {
    await Future.delayed(const Duration(seconds: 1)).then((_) {
      // fetch data
      // allItemList = await hogehogeRepo.fetch();
      allItemList = exampleList;

      isLoading = false;
      notifyListeners();
    });
  }

  void searchStart() {
    isSearching = true;
    notifyListeners();
  }

  void searchEnd() {
    isSearching = false;
    _searchedText = '';
    searchedItemList.clear();
    notifyListeners();
  }

  void searchOperation(String searchText) {
    _searchedText = searchText;

    searchedItemList.clear();

    searchedItemList.addAll(
      allItemList.where(
        (element) => element.toLowerCase().contains(
              searchText.toLowerCase(),
            ),
      ),
    );
    notifyListeners();
  }
}
```

<details><summary>サンプルデータ</summary><div>

```example_data.dart
List<String> exampleList = [
  'Axvfgfdg',
  'Axvfgfdg3',
  'Bsdadasd',
  'Axvfgfdg2',
  'Bsdadasd3',
  'Cat',
  'Bsdadasd2',
  'Cat2',
  'Cat3',
  'Dog',
  'Dog2',
  'Dog3',
  'Elephant',
  'Elephant2',
  'Elephant3',
  'Fans',
  'Girls',
  'Hiiii',
  'Ilu',
  'Jeans',
  'Kite',
  'Lion',
  'Men',
  'Nephow',
  'Owl',
  'Please',
  'Quat',
  'Rose',
  'Salt',
  'Trolly',
  'Up',
  'View',
  'Window',
  'Xbox',
  'Yellow',
  'Yummy',
  'Zubin',
  'Zara',
  'Fans2',
  'Girls2',
  'Hiiii2',
  'Ilu2',
  'Jeans2',
  'Kite2',
  'Lion2',
  'Men2',
  'Nephow2',
  'Owl2',
  'Please2',
  'Quat2',
  'Rose2',
  'Salt2',
  'Trolly2',
  'Up2',
  'View2',
  'Window2',
  'Xbox2',
  'Yellow2',
  'Yummy2',
  'Zubin2',
  'Zara2',
  'あいうえお',
  'かきくけこ',
  'さしすせそ',
  '12345',
  '678',
  '999'
];
```
</div></details>

#画面
<img width="200" alt="all.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631339/226cc0dc-b827-6c05-ef46-829198882558.png">

<img width="200" alt="search.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631339/56215394-a5f2-4b7b-5637-0e05cc5fc756.png">


#その他

・検索バーでやりたかったこと（後で記事にする予定）
一覧リストと検索結果リストのPrimaryScrollControllerやScrollControllerをそれぞれ用意する。
リスト横にA-Zインデックス？みたいなwidgetを用意する。
ScrollControllerを渡してリストをスクロールできるようにする。

・リポジトリー

https://github.com/mkurom/flutter_search_bar_sample_app