flutter_hooks+firestoreで始める、掲示板アプリ

2021/12/21に公開

事前準備

  • 前提条件
    Firebaseプロジェクトがすでに作られている。
    Firestoreのセキュリティモードがテスト、またはAuthができる状態である。
  • 依存関係をインストール
dependencies:
  flutter:
    sdk: flutter
  flutter_hooks: ^0.18.1
  firebase_core: ^1.10.6
  cloud_firestore: ^3.1.5
  • flutterfire_cliのインストール
    以下のコマンドを入力し、インストール
dart pub global activate flutterfire_cli

参照:https://firebase.flutter.dev/docs/overview/#initializing-flutterfire

  • flutterアプリとfirebaseの連携
    flutterアプリとfirebaseを連携します。
    まず、目的のFlutterアプリプロジェクトのルートに移動し、以下のコマンドを入力します。
flutterfire configure

そうするとどのFirebaseプロジェクトを連携するか、どのプラットフォームで利用するかを聞かれます。
全ての質問に答えて上げましょう。
そうするとlibディレクトリに、firebase_options.dartができていると思います。
これは、Firebaseとのプラットフォームごとの連携をよしなにやってくれるdartファイルになります。

  • firebase_coreを初期化
    ここから、コーディングを初めます。
    扱うパッケージを全てインポートします。
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'firebase_options.dart';

次に、firebase_coreの初期化を行います。
main関数を編集します。

main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}
  • MyAppを作成
main.dart

class MyApp extends HookWidget {
  const MyApp({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Text("まだ、なにも"),
    );
  }
}
  • BordPageを作成
main.dart
class BoardPage extends HookWidget {
  const BoardPage({Key? key}) : super(key: key);
  void _bottomSheet(BuildContext context) {
    showModalBottomSheet(
      context: context,
      builder: (_) {
        return HookBuilder(
          builder: (context) {
            final name = useTextEditingController(text: '名無し');
            final content = useTextEditingController();
            return Column(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [
                TextField(
                  decoration: const InputDecoration(
                    labelText: '名前',
                  ),
                  controller: name,
                ),
                TextField(
                  decoration: const InputDecoration(
                    labelText: 'コメント',
                  ),
                  controller: content,
                ),
                ElevatedButton(
                  child: const Text('投稿'),
                  onPressed: () {
                    FirebaseFirestore.instance.collection('posts').add({
                      'name': name.text,
                      'content': content.text,
                      'time': Timestamp.now(),
                    });
                    Navigator.pop(context);
                  },
                ),
              ],
            );
          },
        );
      },
    );
  }

  
  Widget build(BuildContext context) {
    List<Widget> listTile = [];
    final memo = useMemoized(() => FirebaseFirestore.instance
        .collection('posts')
        .orderBy("time")
        .snapshots());
    final snapshot = useStream(memo);

    if (snapshot.hasData) {
      final docs = snapshot.data?.docs;
      docs?.forEach((element) {
        listTile.add(ListTile(
          title: Text(element.data()['content']),
          subtitle: Text(element.data()['name']),
        ));
      });
    }

    return Scaffold(
      appBar: AppBar(
        title: const Text('アントちゃんねるへようこそ'),
      ),
      body: ListView(
        children: listTile,
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _bottomSheet(context);
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}
  • MyAppを編集
main.dart
class MyApp extends HookWidget {
  const MyApp({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const BoardPage(),
    );
  }
}
  • なにがおきているのか?
BordPage->build
final memo = useMemoized(() => FirebaseFirestore.instance
        .collection('posts')
        .orderBy("time")
        .snapshots());
final snapshot = useStream(memo);

useMemoizedでFirestoreのPostコレクションのキャッシュをとっています。これで、Firestoreに更新があったときだけ、firestoreのpostsコレクションを読み込むようになります。
また、orderBy("time")でドキュメントを投稿時間でソートしています。
useStreamでは、キャッシュされたスナップショットをstreamとして受け取ります。これで、StreamBuilderを使わず、Firestoreの保持しているデータを扱えるようになります。

BordPage->build
 if (snapshot.hasData) {
      final docs = snapshot.data?.docs;
      docs?.forEach((element) {
        listTile.add(ListTile(
          title: Text(element.data()['content']),
          subtitle: Text(element.data()['name']),
        ));
      });
    }

これはsnapshotがデータを持って入れば、listTileというリストにListTileを追加していく処理になります。docsにはpostsコレクションのドキュメントが入っています。

BordPage->_bottomSheet
final name = useTextEditingController(text: '名無し');
final content = useTextEditingController();

useTextEdittingControllerをつかうことでTextFieldの値をかんたんに扱うことができます。

BordPage->_bottomSheet
 onPressed: () {
                    FirebaseFirestore.instance.collection('posts').add({
                      'name': name.text,
                      'content': content.text,
                      'time': Timestamp.now(),
                    });
                    Navigator.pop(context);
                  },

ボタンが押されたさい、Firestoreのpostsコレクションにドキュメントを追加します。
timeには現在のTimestampを、nameには投稿者の名前を、contentには投稿内容を記述します。

Discussion