👁️

Firestoreでwhere文を使う

2023/02/01に公開

Flutterの機能で検索機能を実装する

前回書いた記事が、検索をしていると呼べるものではなかったようなので、where文の学習の参考になるチュートリアルを見つけて、学習できたので新たに映画の検索アプリ作りました🙇‍♂️
今回参考にしたチュートリアル
https://www.youtube.com/watch?v=6f1KJTBy3ak
今回作成したDemoアプリ
https://youtu.be/m-YSObk4nYc

完成品のソースコード
完成品はStatefulWidgetからRiverpodにリファクタリングしてます。
https://github.com/sakurakotubaki/MovieWhere


whereとは?

SQLのWHERE文と同じように、条件をつけて検索する機能ですね。

公式の翻訳

https://firebase.flutter.dev/docs/firestore/usage

フィルタリング
コレクション内のドキュメントをフィルタリングするために、whereメソッドをコレクション参照に連結することができます。フィルタリングは、等号チェックと "in "クエリーをサポートしています。例えば、年齢が20歳以上であるユーザーをフィルタリングする。

FirebaseFirestore.instance
  .collection('users')
  .where('age', isGreaterThan: 20)
  .get()
  .then(...);

Firestore は配列クエリもサポートしています。たとえば、英語 (en) あるいはイタリア語 (it) を話すユーザーを絞り込むには arrayContainsAny フィルタを使用します。

今回使用するのは、こちらですね。Firestoreのtitle_arrayは、配列なので、こちらを使うことになります。
DartだとListですけどね。

FirebaseFirestore.instance
  .collection('users')
  .where('language', arrayContainsAny: ['en', 'it'])
  .get()
  .then(...);

アプリを作成

まずは、必要なpackageを追加しておきます。後でRiverpodに変更するので、最初に追加しておいてください。

pubspec.yaml
name: where_query
description: A new Flutter project.

# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1

environment:
  sdk: '>=2.18.0 <3.0.0'

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  firebase_core: ^2.4.1
  cloud_firestore: ^4.3.1
  flutter_riverpod: ^2.1.3

dev_dependencies:
  flutter_test:
    sdk: flutter

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^2.0.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

まずは、StatefulWidgetでやってみます。
その前に、Firestoreにダミーのデータを追加しておいてください。
データの構造は、titleとgenreが、string型で、array型もstringです。

データの構造

深海誠作品が個人的に好きなので、深海誠監督の作品を追加しておきます🤭

{"title": "天気の子", "genre": "恋愛", "title_array": ["深海誠作品", "映像作品", "名作"]}
  • やること
    • searchコレクションを作る
      • titleフィールドに、映画のタイトルを入れる
      • genreフィールドに、映画のジャンルを入れる。(例)ファンタジー
      • title_arrayには、検索キーワードを入れます。(例)名作、深海誠作品

最初のコード

海外の動画を参考に作成しました。Firestoreのデータの構造がおかしいと、エラーが発生するので、気をつけてください😱

main.dart
import 'dart:math';

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(const MyApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Firebase Search',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const FirebaseSearchScreen(),
    );
  }
}

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

  
  State<FirebaseSearchScreen> createState() => _FirebaseSearchScreenState();
}

class _FirebaseSearchScreenState extends State<FirebaseSearchScreen> {
  List searchResult = []; // 空のリストを作る.
  // .whereでstring_id_arrayを検索して、候補を表示する
  void searchFromFirebase(String query) async {
    final result = await FirebaseFirestore.instance
        .collection('search')
        .where('title_array', arrayContains: query)
        .get();
    // リストに、検索して取得したデータを保存する.
    setState(() {
      searchResult = result.docs.map((e) => e.data()).toList();
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Firebase Search"),
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(15.0),
            child: TextField(
              decoration: InputDecoration(
                border: OutlineInputBorder(),
                hintText: "Search Here",
              ),
              onChanged: (query) {
                searchFromFirebase(query);
              },
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: searchResult.length, // リストの数をlengthで数える.
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(searchResult[index]['title']),
                  subtitle: Text(searchResult[index]['genre']),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

検索フォームにキーワードを入力して、候補が表示されれば成功です!
名前が完全に一致していないと検索できないようです🫠


Riverpodで状態管理をする

最近流行りのRiverpodでコードを書き換えてみましょう!
ProviderやGetXをここ数日色々試してみましたが、Riverpodのほうが使いやすいですね。
Riverpodのメリットは、どこからでも呼べることや最近は情報が増えてきて、学習コストが減っているのか流行っているようで、私も最近はRiverpodでしか状態管理はしていないです。

Providerの特徴はコードは書きやすいのですが、コードがネストしていく。GetXはProviderより簡単にかけて便利な機能が多いが、サポートされてないようなので新規の開発では使われないと思われる?

search_state.dartを作成して、検索機能に必要なProviderを定義しましよう。
結構書くの難しくて、型定義のところはいい感じで、書けなかったです。誰かdynamicで書かずに済むいいコードの書き方を知っている方いたら教えて欲しいです🙇‍♂️

search_state.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 空のリストを作る.
// dynamic型にしないと、メソッドの中のresultを代入できなかった!
final searchResultProvider = StateProvider<List<dynamic>>((ref) => []);
// Firestoreを使うProvider.
final firebaseProvider =
    Provider<FirebaseFirestore>((ref) => FirebaseFirestore.instance);
// SearchStateNotifireを呼び出すProvider.
final searchStateNotifireProvider =
    StateNotifierProvider<SearchStateNotifire, dynamic>((ref) {
  return SearchStateNotifire(ref);
});

// キーワードで映画を検索するメソッドが使えるStateNotifier.
class SearchStateNotifire extends StateNotifier<dynamic> {
  Ref _ref;
  SearchStateNotifire(this._ref) : super([]);

  // .whereでstring_id_arrayを検索して、候補を表示する
  Future<void> searchWhere(String query) async {
    final result = await FirebaseFirestore.instance
        .collection('search')
        .where('title_array', arrayContains: query)
        .get();
    // リストに、検索して取得したデータを保存する.
    _ref.watch(searchResultProvider.notifier).state =
        result.docs.map((e) => e.data()).toList();
  }
}

検索機能と画面に取得したデータを表示するコード

main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:where_query/search_state.dart';

import 'firebase_options.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(
    const ProviderScope(child: MyApp()),
  );
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FirestoreSearch',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const FirebaseSearchScreen(),
    );
  }
}

class FirebaseSearchScreen extends ConsumerWidget {
  const FirebaseSearchScreen({
    Key? key,
  }) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    // ListView.builderのitemCountで使用するListのProviderを呼び出す.
    final result = ref.watch(searchResultProvider);
    // Firestoreの映画情報を検索するProviderを呼び出す.
    final searchState = ref.read(searchStateNotifireProvider.notifier);
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.redAccent,
        title: const Text("FirestoreSearch"),
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(15.0),
            child: TextField(
              decoration: const InputDecoration(
                border: OutlineInputBorder(),
                hintText: "Search Here",
              ),
              onChanged: (query) {
                searchState
                    .searchWhere(query); // onChangedを使用して、メソッドの引数にFormの値を保存する.
              },
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: result.length, // リストの数をlengthで数える.
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(result[index]['title']), // 映画のタイトルを表示する.
                  subtitle: Text(result[index]['genre']), // 映画のジャンルを表示する.
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

こんな感じで、条件が一致すれば映画の情報を習得できます。

まとめ

いつもは、データを画面に表示するときは、StreamBuilderやFurureBuilder使ったりしてるので、メソッド使って、Listのデータを画面に表示するのは、いい勉強になりました。
文法への理解があまりなかったのか、以前も似たようなコード書いてたのですが、できませんでした😅
Dartの文法の学習をしてからは、Listを最近よく使うことが多かったので、動く生のソースコードを見れば、こんなロジックでやればできるのだなと、すぐに理解できました。

この文法は、あまり見かけないので、とても勉強になりました。
日本語の情報だと、こんな情報出てこないので苦労しました。いい動画を撮ってくれた海外の人には、感謝しないといけないですね。
YouTubeバカに出来ないですよ!

List searchResult = []; // 空のリストを作る.
  // .whereでstring_id_arrayを検索して、候補を表示する
  void searchFromFirebase(String query) async {
    final result = await FirebaseFirestore.instance
        .collection('search')
        .where('string_id_array', arrayContains: query)
        .get();
    // リストに、検索して取得したデータを保存する.
    setState(() {
      searchResult = result.docs.map((e) => e.data()).toList();
    });
  }

Discussion