🐡

[Flutter] freezed / riverpod / firestore で algolia_helper_flutterを使う

2023/01/12に公開約6,100字1件のコメント

firebaseだと全文検索が非常にむつかしいので、いい感じにやってくれるAlgoliaを使ってみました。

最初の設定はこちらを見るとわかりやすいと思います。

https://qiita.com/mogmet/items/943c0450957298f007ac

今回は貸切風呂に入る予約を想定して、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の初期化をブロックできるようにする。

ログインするとコメントできます