🐡
[Flutter] freezed / riverpod / firestore で algolia_helper_flutterを使う
firebaseだと全文検索が非常にむつかしいので、いい感じにやってくれるAlgoliaを使ってみました。
最初の設定はこちらを見るとわかりやすいと思います。
今回は貸切風呂に入る予約を想定して、Firestoreに入れるクラスは以下のようなクラスを想定しています。
Reservationクラス
Firestoreを型付きで受け取るためにいろいろ生やしています。
reservation.dart
// ignore_for_file: constant_identifier_names
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'reservation.freezed.dart';
part 'reservation.g.dart';
//貸切風呂の名前
enum BathName {
HOGEA("A"),
HOGEB("B"),
HOGEC("C");
const BathName(this.displayName);
final String displayName;
}
class Reservation with _$Reservation {
const Reservation._();
factory Reservation(
{required String uid,
required String userId,
required String date,
required DateTime fromTime,
required DateTime endTime,
required BathName bathName,
String? guestId,
("外来") String guestName,
("番号なし") String phoneNumber,
(false) isOccupied,
String? description}) = _Reservation;
factory Reservation.fromJson(Map<String, dynamic> json) =>
_$ReservationFromJson(json);
factory Reservation.fromFireStore(
DocumentSnapshot<Map<String, dynamic>> snapshot,
SnapshotOptions? options,
) {
DateTime toDateTime(Timestamp value) {
return value.toDate();
}
final data = snapshot.data();
return Reservation(
userId: data?["userId"],
uid: data?["uid"],
date: data?["date"],
fromTime: toDateTime(data?["fromTime"] as Timestamp),
endTime: toDateTime(data?["endTime"] as Timestamp),
bathName:
BathName.values.byName(data?["bathName"]), // data?["bathName"],
guestName: data?["guestName"],
phoneNumber: data?["phoneNumber"],
isOccupied: data?["isOccupied"],
description: data?["description"]);
}
Map<String, dynamic> toFireStore() {
return {
"uid": uid,
"userId":userId,
"date": date,
"fromTime": fromTime,
"endTime": endTime,
"bathName": bathName.name,
"guestName": guestName,
"phoneNumber": phoneNumber,
"isOccupied": isOccupied,
"description": description
};
}
}
必要なライブラリのインストール
pubspec.yaml
pubspec.yaml
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
hooks_riverpod: ^2.1.3
flutter_hooks: ^0.18.5+1
freezed_annotation: ^2.2.0
json_annotation: ^4.7.0
algolia_helper_flutter: ^0.2.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
freezed: ^2.3.2
build_runner: ^2.3.3
json_serializable: ^6.5.4
Modelの定義
AlgoliaのResultをパースするだけなので簡単でいいと思います。
SearchReservationResult.dart
searchreservationresult.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'searchreservationresult.freezed.dart';
part 'searchreservationresult.g.dart';
class SearchReservationResult with _$SearchReservationResult {
factory SearchReservationResult({
required String uid,
required String date,
required String guestName,
required String phoneNumber,
required String path
}) = _SearchReservationResult;
factory SearchReservationResult.fromJson(Map<String, dynamic> json) =>
_$SearchReservationResultFromJson(json);
}
Providerの定義
ここが重要で、StreamProviderを使うことで
UI側で簡単に扱えるようになります。
applicationIDなどは適宜取り換えるとよいと思います。
algolia_search_provider.dart
import 'package:algolia_helper_flutter/algolia_helper_flutter.dart';
import 'package:algoliatest/searchreservationresult.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
final hitsSearcherProvider = Provider<HitsSearcher>((ref) => HitsSearcher(
applicationID: "YOUR_APPLICATION_ID",
apiKey: "YOUR_API_KEY",
indexName: "YOUR_SEARCH_TARGET_INDEX"));
final searchWordProvider = StateProvider<String>(
(ref) => "",
);
final searchMetadataProvider =
StreamProvider<List<SearchReservationResult>>((ref) async* {
final _searcher = ref.watch(hitsSearcherProvider);
final _searchWords = ref.watch(searchWordProvider);
_searcher.query(_searchWords);
await for (final res in _searcher.responses) {
yield res.hits.map((e) => SearchReservationResult.fromJson(e)).toList();
}
}, dependencies: [searchWordProvider, hitsSearcherProvider]);
最後にUIを簡単に作る。
main.dart
main.dart
import 'package:algoliatest/algolia_search_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends HookConsumerWidget {
MyHomePage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final response = ref.watch(searchMetadataProvider);
final _controller = useTextEditingController();
return Scaffold(
appBar: AppBar(
title: const Text("Algolia Search Example"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
controller: _controller,
onEditingComplete: () {
ref.read(searchWordProvider.notifier).update((state) => _controller.text);
},
),
response.when(
data: (data) {
print(data);
return Text(data.toString());},
error: ((error, stackTrace) => Text(error.toString())),
loading: (() => const CircularProgressIndicator())),
],
),
),
);
}
}
Discussion
このままのコードだと最初に全件取得してしまい、ちょっとアレな感じなのでsearchwordproviderの初期化をブロックできるようにする。