Firestoreでwhere文を使う
Flutterの機能で検索機能を実装する
前回書いた記事が、検索をしていると呼べるものではなかったようなので、where文の学習の参考になるチュートリアルを見つけて、学習できたので新たに映画の検索アプリ作りました🙇♂️
今回参考にしたチュートリアル
今回作成したDemoアプリ
完成品のソースコード
完成品はStatefulWidgetからRiverpodにリファクタリングしてます。
whereとは?
SQLのWHERE文と同じように、条件をつけて検索する機能ですね。
公式の翻訳
フィルタリング
コレクション内のドキュメントをフィルタリングするために、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に変更するので、最初に追加しておいてください。
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には、検索キーワードを入れます。(例)名作、深海誠作品
- searchコレクションを作る
最初のコード
海外の動画を参考に作成しました。Firestoreのデータの構造がおかしいと、エラーが発生するので、気をつけてください😱
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で書かずに済むいいコードの書き方を知っている方いたら教えて欲しいです🙇♂️
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();
}
}
検索機能と画面に取得したデータを表示するコード
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