📽️

FlutterでDocumentReferenceを使ってみる

2023/02/18に公開

参照型ってどう使う?

RDBの外部キーのようなものだそうですが、僕のイメージだと違うような...
昔勉強したときの感覚では、親テーブルのデータが削除されたり、更新されたら、紐付いている小テーブルのデータも削除されたり、更新されるものだった気がします。

RDBの外部キーについて
https://www.javadrive.jp/mysql/table/index11.html

こちらが公式ドキュメントなのですが、解説がこれだけしかない!
YouTubeを見ていたら、参考になりそうな動画あったので、チュートリアルをやってみました。
https://firebase.google.com/docs/firestore/manage-data/data-types?hl=ja

参照 パス要素別 (コレクション、ドキュメント ID、コレクション、ドキュメント ID...) たとえば、 projects/[PROJECT_ID]/databases/[DATABASE_ID]/documents/[DOCUMENT_PATH]です。

参考にした動画
https://www.youtube.com/watch?v=P5CvA3bvBeU

参考になりそうな情報
https://zenn.dev/sukedon/scraps/356694a8a1cc8d


使ってみてどんなものだったか?

アプリを作った時点では、コレクションは存在していないので、add_blog.dartで作成します。

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

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

  
  State<AddBlog> createState() => _AddBlogState();
}

class _AddBlogState extends State<AddBlog> {
  TextEditingController _titleController = TextEditingController();
  TextEditingController _bodyController = TextEditingController();

  
  void dispose() {
    // TODO: implement dispose
    _titleController.dispose();
    _bodyController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          centerTitle: true,
          title: const Text('ブログを作成'),
        ),
        body: Column(
          children: [
            TextFormField(
              controller: _titleController,
            ),
            TextFormField(
              controller: _bodyController,
            ),
            ElevatedButton(
                onPressed: () async {
                  Map<String, dynamic> addBlog = {
                    'title': _titleController.text,
                    'body': _bodyController.text
                  };

                  FirebaseFirestore.instance.collection('blog').add(addBlog);
                },
                child: const Text('新規作成'))
          ],
        ));
  }
}

Firestoreからブログのデータを取得するページを作成します。こちらのページから、add_blog.dartへ移動します。

reference/blog_page.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:state_tutorial/reference_page/add_blog.dart';
import 'package:state_tutorial/reference_page/detail.dart';

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

  
  Widget build(BuildContext context) {
    // Firestoreのデータを1度だけ全て取得する.
    final _blogdata = FirebaseFirestore.instance.collection('blog').get();
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: const Text('ブログページ'),
      ),
      floatingActionButton: FloatingActionButton(
        // 追加ページへ画面遷移する.
        onPressed: () {
          Navigator.of(context)
              .push(MaterialPageRoute(builder: (context) => const AddBlog()));
        },
        child: const Icon(Icons.add),
      ),
      body: FutureBuilder<QuerySnapshot>(
        future: _blogdata,
        builder: (context, snapshot) {
          if (snapshot.hasError) {
            return Center(child: Text('Error:${snapshot.error}'));
          }

          if (snapshot.hasData) {
            QuerySnapshot data = snapshot.data!;
            List<QueryDocumentSnapshot> documents = data.docs;
            List<Map<String, dynamic>> items = documents
                .map((e) => {
                      'id': e.id,
                      'title': e['title'],
                      'body': e['body'],
                    })
                .toList();

            return ListView.builder(
                itemCount: documents.length,
                itemBuilder: (context, index) {
                  Map<String, dynamic> thisItem =
                      items[index] as Map<String, dynamic>;
                  return ListTile(
                    onTap: () {
                      Navigator.of(context).push(MaterialPageRoute(
                          builder: (context) => Detail(thisItem)));
                    },
                    title: Text(thisItem['title']),
                    subtitle: Text(thisItem['body']),
                  );
                });
          }

          return Center(child: CircularProgressIndicator());
        },
      ),
    );
  }
}

main.dartでこちらのページをimportする。

reference/main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:state_tutorial/reference_page/blog_page.dart';

import 'firebase_options.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const BlogPage(),
    );
  }
}

データを追加した状態

適当にデータを追加...
普段の日常の話題がいいのではないでしょうか???


ここから、問題の分からないことだらけのリファレンス型が出てきます。何をしてもらうかというと、blog_page.dartで取得したデーターをdetail.dartに渡して、そこでMap dataというコンストラクターに渡して、そこから、blogコレクションのidを取得して、今いるページのブログにだけ複数の人が、コメントをする機能を再現します。

reference/detail.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';

class Detail extends StatelessWidget {
  Detail(this.data, {Key? key}) : super(key: key) {
    // blogコレクションのドキュメントの参照を得ることができる.
    // 今いる詳細ページにわたされたdataから、今いるページのデータのidを取得する.
    _documentReference =
        FirebaseFirestore.instance.collection('blog').doc(data['id']);
    // blogコレクションを参照して取得し、commentコレクションを作成する.
    _referenceComments = _documentReference.collection('comment');
    // blogコレクションのcommentサブコレクションを取得する.
    _streamComments = _referenceComments.snapshots();
  }
  Map data; // コンストラクターをMap型で作成して、前のページの値を受け取る.

  // コンストラクターで使うプロパティの型定義をする.
  late DocumentReference _documentReference; // ドキュメントを参照して取得.
  late CollectionReference _referenceComments; // 単一のデータを習得.
  late Stream<QuerySnapshot> _streamComments; // 全てのデータを取得.

  
  Widget build(BuildContext context) {
    // コメントを保存するTextEditingController.
    TextEditingController _comment = TextEditingController();

    return Scaffold(
        appBar: AppBar(
          centerTitle: true,
          title: const Text('Details'),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            showModalBottomSheet(
              context: context,
              builder: (BuildContext context) {
                return Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      TextField(
                        controller: _comment,
                      ),
                      ElevatedButton(
                          onPressed: () async {
                            // 参照したコレクションにサブコレクションを作成する.
                            await _referenceComments.add({
                              'ref': data['title'], // 今いるページのブログのタイトルを保存する.
                              'comment': _comment.text // コメントを保存する.
                            });
                          },
                          child: const Text('ブログにコメントをする'))
                    ],
                  ),
                );
              },
            );
          },
          child: const Icon(Icons.send),
        ),
        body: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Container(
                color: Colors.black12,
                padding: EdgeInsets.all(18),
                child: Text(data['title'])),
            Expanded(
              child: StreamBuilder<QuerySnapshot>(
                stream: _streamComments,
                builder: (BuildContext context,
                    AsyncSnapshot<QuerySnapshot> snapshot) {
                  if (snapshot.hasError) {
                    return Text('Something went wrong');
                  }

                  if (snapshot.connectionState == ConnectionState.waiting) {
                    return Text("Loading");
                  }
                  // ブログへのコメントをリアルタイムに取得する.
                  return ListView(
                    children:
                        snapshot.data!.docs.map((DocumentSnapshot document) {
                      Map<String, dynamic> data =
                          document.data()! as Map<String, dynamic>;
                      return ListTile(
                        // コメントを表示.
                        title: Text(data['comment'].toString()),
                        // 今いるページの保存したブログのタイトルを表示.
                        subtitle: Text('${data['ref'].toString()}にコメントしました'),
                      );
                    }).toList(),
                  );
                },
              ),
            )
          ],
        ));
  }
}

まずは、blogコレクションのidを取得して、commentサブコレクションをblogコレクションの中にデータが追加されたら作成してくれる仕組みになっています。
この辺の情報が少なくて、リファレンス型をどう使えばいいのかが分からなかったりしました!

再buildしてデータをget

虫のボタンかRunボタンを押して、アプリを再起動させてFirestoreからデータを全て取得できたら成功です。
こんな感じになります。
onTapすると詳細ページへ画面遷移できます。

こんな感じになります

コメントがついていて、よく見かけるブログぽくなってる気がしなくもない...




他のフィールドにもコメントする

こんな感じで、別々のコメントがついています。


最後に

DocumentReferenceの情報って少ないから、もっと記事を誰かに書いて欲しいなと思いつつ、誰も書いてくれないので、動くプログラムを作ってみて書いてみました。
他にも知らない機能がFirebaseにはあるので、速く使いこなしたいです。

Discussion