初心者 flutter x firestoreの複合クエリに挑戦する(multiple search)

# ユーザーとしては、あって当然の機能だが

　うっかりつくる側に回ってしまった初心者には、とても大変なことだった。基礎をおろそかにしたまま応用に走るからいけないんだということはわかっているけれど、これは性格なのでしようがない。公式ドキュメントはもちろん、さまざまな記事、世界中のアクセントを網羅した英語YouTube、それどころか、何語かわからないYouTubeも見て、そこで紹介されているGitHubのコードを集めまくり、読みまくること、ほぼ二十日。

## わかっている人たちの情報は細切れ

　初心者を苦しめたのは、細切れの情報。もちろん、わかる人が見ればちゃんとわかるのだろうけれど、初心者にとっては、まるでピースの足りてないジグソーパズルのようなもの。これはこの辺、これはこの辺かな、でもこの間はなにかな、の世界だ。私にとってもっともありがたかったのは、GitHubのコード。具体的に全体像がわかる。でもそれはそれで個別の世界として完結しているから、分解して自分の世界に当てはめるためには、逆に抽象化の作業が要る。statefullだったりRiverpodだったりするものを、providerに読み替えなきゃならないし。かくして、具体と抽象の間を往復して、ようやく到達したコードがこちら。先輩諸兄姉、笑ってやってくださいな。そしてご同輩の皆さん、お役に立てばさいわいです。

注: たくさんのコードから継ぎ接ぎしてますので、意味不明に不要な破片が残ってるかもしれません。

ここから
![スクリーンショット 2022-06-06 20.57.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1646810/ffd11de9-b854-9d73-85c2-908952a8de67.png)

こうしたい。
![スクリーンショット 2022-06-06 20.59.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1646810/8f9e1286-5906-8be0-28f5-d840241a963b.png)


```
//ListPage.dart

Import...

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

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<ListModel>(
      create: (_) => ListModel()..fetcList(),
      child: Scaffold(
        appBar: AppBar(
          backgroundColor: Colors.brown[300],
          title: const Text('search')),
        body: Container(
          padding: const EdgeInsets.all(16),
          constraints: const BoxConstraints.expand(),
          decoration: const BoxDecoration(
            image: DecorationImage(
              image: AssetImage('assets/images/back.png'),
              fit: BoxFit.cover,
            ),
          ),
          child: Row(
              children: [
                Expanded(
                  flex: 1,
                  child: Padding(
                    padding: const EdgeInsets.all(20.0),
                    child: Consumer<ListModel>(builder: (context, model, child) {
                      return Container(
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(15.0),
                        color: Colors.amber[100],
                      ),
                        child: Column(
                          children: <Widget>[
                            const Padding(padding: EdgeInsets.all(10.0),
                              child: Text(
                                'search terms',
                                style: TextStyle(
                                  fontSize: 20,
                                  fontStyle: FontStyle.italic,
                                  color: Colors.brown,
                                ),),
                            ),
                            Padding(
                              padding: const EdgeInsets.fromLTRB(20, 5, 20, 5),
                              child: TextFormField(
                                    decoration: InputDecoration(
                                      hintText: 'calendar(CE, BCE or BP)',
                                      hintStyle: const TextStyle(
                                          fontSize: 12, color: Colors.black54),
                                      fillColor: Colors.grey[200],
                                      filled: true,
                                      focusedBorder: OutlineInputBorder(
                                        borderRadius: BorderRadius.circular(20),
                                        borderSide: const BorderSide(
                                          color: Colors.grey,
                                          width: 2.0,
                                        ),
                                      ),
                                      enabledBorder: OutlineInputBorder(
                                        borderRadius: BorderRadius.circular(16),
                                        borderSide: BorderSide(
                                          color: Colors.grey[100]!,
                                          width: 1.0,
                                        ),
                                      ),
                                    ),
                                    onChanged: (text) {
                                      model.calendarName = text;
                                      //model.searchEvents(text);
                                    },
                                 ),
                              ),

                            Padding(
                              padding: const EdgeInsets.fromLTRB(20, 5, 20, 5),
                              child: TextFormField(
                                  decoration: InputDecoration(
                                    hintText: 'country',
                                    hintStyle: const TextStyle(
                                        fontSize: 12, color: Colors.black54),
                                    fillColor: Colors.grey[200],
                                    filled: true,
                                    focusedBorder: OutlineInputBorder(
                                      borderRadius: BorderRadius.circular(20),
                                      borderSide: const BorderSide(
                                        color: Colors.grey,
                                        width: 2.0,
                                      ),
                                    ),
                                    enabledBorder: OutlineInputBorder(
                                      borderRadius: BorderRadius.circular(16),
                                      borderSide: BorderSide(
                                        color: Colors.grey[100]!,
                                        width: 1.0,
                                      ),
                                    ),
                                  ),
                                  onChanged: (text) {
                                    model.countryName = text;
                                    //model.searchEvents(text);
                                  },
                              )),

                          Padding(
                            padding: const EdgeInsets.all(10.0),
                            child: ElevatedButton(
                              style: ElevatedButton.styleFrom(
                                primary: Colors.amber,
                              ),
                              onPressed: () {
                                model.searchEvents();
                              },
                              child: const Text('Search'),
                            ),
                          )
                         ],
                       ),
                      );
                     },
                    ),
                  ),
                ),
                Expanded(
                  flex: 3,
                  child: Container(
                    child: Consumer<ListModel>(
                        builder: (context, model, child) {
                          final List<Select>? events = model.events;

                          if (events == null) {
                            return const CircularProgressIndicator();
                          }

                          final List<Widget> widgets = events
                              .map(
                                (events) =>
                                Padding(
                                  padding: const EdgeInsets.all(10.0),
                                  child: Card(
                                    child: Row(
                                      children: [
                                        Expanded(
                                            flex: 3,
                                            child: Padding(
                                              padding: const EdgeInsets.all(
                                                  10.0),
                                              child: Column(
                                                children: [
                                                  Text(events.calendar),
                                                  Text(events.year),
                                                ],
                                              ),
                                            )),
                                        Expanded(
                                            flex: 5,
                                            child: Text(events.name)),
                                        Expanded(
                                          flex: 3,
                                          child: Text(events.country),
                                        ),
                                        Expanded(
                                          flex: 2,
                                          child: Padding(
                                            padding: const EdgeInsets.all(8.0),
                                            child: ElevatedButton(
                                              style: ElevatedButton.styleFrom(
                                                primary: Colors.brown[100],
                                              ),
                                              onPressed: () {
                                                Navigator.push(
                                                  context,
                                                  MaterialPageRoute(
                                                    builder: (
                                                        context) => const DetailPage(),
                                                  ),
                                                );
                                              },
                                              child: const Text('detail'),
                                            ),
                                          ),)
                                      ],
                                    ),
                                  ),
                                ),
                          　　)
                              .toList();
                          return ListView(
                            children: widgets,
                          );}),
                　),
                )
              ],
            ),
        ),
　　　中略
  }
}
```

```
//ListModel.dart

import...

class ListModel extends ChangeNotifier {
  List<Select>? events;

  void fetchList() async {
    final QuerySnapshot snapshot =
    await FirebaseFirestore.instance.collection('events')
        .orderBy('calendar', descending: false)
        .get();

    final List<Select> events = snapshot.docs.map((DocumentSnapshot document) {
      Map<String, dynamic> data = document.data() as Map<String, dynamic>;

      final String calendar = data['calendar'];
      final String year = data['year'];
      final String name = data['name'];
      final String country = data['country'];

      return Select(calendar, year, name, country);
    }).toList();

    this.events = events;
    notifyListeners();
  }

  String? calendarName;
  String? countryName;

  Future<void> searchEvents() async {

    final QuerySnapshot snapshot = await FirebaseFirestore.instance
        .collection('events')
        .where('calendar', isEqualTo: calendarName)
        .where('country', isEqualTo: countryName)
        .get();

    final List<Select> search = snapshot.docs.map((DocumentSnapshot document) {
      Map<String, dynamic> data = document.data() as Map<String, dynamic>;

      final String calendar = data['calendar'];
      final String year = data['year'];
      final String name = data['name'];
      final String country = data['country'];

      return Select(calendar, year, name, country);
    }).toList();

    events = search;
    notifyListeners();
  }
}
```
## なにがそんなに大変だったかといえば

　結局、振り返ってみれば、複数の検索語の受け渡しが難しかった。一つなら用例もたくさん見つかった。ところが二つにするととたんに詰まる。その痕跡をListPage.dartにわざとコメントアウトで残しました。textfieldのonChangedの部分です。一つの文字列で検索する場合、この形式でsearchEvents(text)に直接渡してトリガーになる。でも二つ以上になると、ケンカしてうまくいかないわけです。なのでいったん検索語をStringで保留しておいて、入力後にボタンでトリガーを引くように変えました。この用例は残念ながら見つけられなかった。何のことはない、すでにできていたAddList()関数に倣っただけという。
　
　１時間考えてわからなかったら聞け、という世界で、二十日もうろうろできるのは、プロじゃなくて老後を楽しんでる初心者ならではの楽しみ方ではあります。(渦中ではもちろんぜんぜん楽しくなかったけどね)。さあ、これからもうちょっと複雑な検索に挑戦しよう〜！