【Flutter/Dart】Firebase/Firestoreを使ったBlogアプリを作ろう

# 1.はじめに
新たに6つのAndroidStudio&Flutter&Firebase記事を書いている。以下は内容、
- 【Flutter/Dart】Firebaseを使おう
https://qiita.com/my_programming/private/7b3251701183ff59b97c
- 【Flutter/Dart】Firebase Authenticationを使って認証アプリを作ろう
https://qiita.com/my_programming/private/2d81229942bc0d61da6b
- 【Flutter/Dart】Firebase/Firestoreを使ったチャットアプリを作ろう
https://qiita.com/my_programming/private/ad0f7a5f7637a1a37842
- 【Flutter/Dart】Firebase/Firestoreを使ったToDoアプリを作ろう
https://qiita.com/my_programming/private/2da77f3a36bf120761c5
- 【Flutter/Dart】Firebase/HostingでWebアプリを外部公開しよう
https://qiita.com/my_programming/private/5950b7e27161dc9ec838
- **【Flutter/Dart】Firebase/Firestoreを使ったBlogアプリを作ろう**

BlogアプリをFireStoreを使って構築していく。これまでにToDoアプリというスマホ向けアプリとWebアプリを作成してきた。今回は二つのアプリ機能を合わせて一つのアプリをを合わせていく作業になる。スマホアプリの機能を使って情報の登録・編集・削除できる様にしてFireStore上のデータベースを編集できる様にする。Webアプリの機能を使ってFireStore上のデータを第三者に表示できる様にするとともにHostingによりWeb上でアプリを公開する。

# 2.Blogアプリを作ろう
## 2-1.Blogアプリの概要
Blogアプリについて以下に概要を記す。
### 2-1-1.Blogアプリ内の情報のつながり
スマホアプリでBlog記事を作成でき、Webアプリでは記事を表示することができる。記事はFirestoreに保存される。
<img width="250" alt="step2_1_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/7ba5dc08-88dc-5a64-30e8-2faee405247b.png">

### 2-1-2.Blogアプリのページのつながり
スマホアプリで作る記事は、タイトルページをリストで表示する。タイトルページはサブタイトルをリストで表示する。
<img width="250" alt="step2_1_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/a3b6e783-b9e3-b380-f53a-711a148fb3ae.png">

### 2-1-3.情報追加部分の作成
スマホで作成できる記事は、タイトルページを個別に登録・編集・削除できる。タイトルページではサブタイトルページを個別に登録・編集・削除できる。タイトルページとサブタイトルページから戻る機能を有する。タイトルページとサブタイトルページは概要を記入できる。
画面に表示されいている記事をクリックすると子となる記事を表示できる。
<img width="250" alt="step2_1_3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/2e50f20e-c1c8-ade0-d666-014ed8e08530.png"> 
親と子の記事では記事を新たに追加するためのサブウィンドウが表示される。
<img width="250" alt="step2_1_4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/9af63a82-314e-470c-e343-cab2baeb678f.png">

### 2-1-4.表示部分の作成
Webアプリではスマホで作成した記事を表示できる。 またhtmlに変換でき、hostingにdeployできる。
<img width="250" alt="step2_1_5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/3b5e81ad-8e70-5d9b-9173-60ec8b2cc15e.png">

## 2-2.Blogアプリへの情報登録・編集・削除機能を作ろう（スマホアプリ編）
以下２つを今回もそのまま利用する。

- 認証アプリのために作ったFirebaseのプロジェクト
- チャットアプリのために作ったCloud Firestoreのデータベース

そのため、classpathをandroid/build.gradleの中にコピーするところから、applicationIdを"com.example.my_auth_app"に書き換えるところまでの手続きはチャットアプリと同じなので説明を省く。

lib/main.dartのコードの全体を以下に示す。
``` .dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'ToDo App',
      home: ToDoPage(),
    );
  }
}

class ToDoPage extends StatefulWidget {
  const ToDoPage({Key? key}) : super(key: key);
  @override
  _ToDoPageState createState() => _ToDoPageState();
}

class _ToDoPageState extends State<ToDoPage> {
  final TextEditingController _todoController = TextEditingController();
  final TextEditingController _contentController = TextEditingController();

  final CollectionReference _todos =
  FirebaseFirestore.instance.collection('todo');
  // Functions used for adding and editing
  Future<void> _add_or_update([DocumentSnapshot? documentSnapshot]) async {
    String mode = 'addition';
    if (documentSnapshot != null) {
      mode = 'update';
      _todoController.text = documentSnapshot['todo'];
      _contentController.text = documentSnapshot['content'].toString();
    }

    await showModalBottomSheet(
        isScrollControlled: true,
        context: context,
        builder: (BuildContext ctx) {
          return Padding(
            padding: EdgeInsets.only(
                top: 20,
                left: 20,
                right: 20,
                // prevent the soft keyboard from covering text fields
                bottom: MediaQuery.of(ctx).viewInsets.bottom + 20),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                TextField(
                  controller: _todoController,
                  decoration: const InputDecoration(labelText: 'ToDo'),
                ),
                TextField(
                  keyboardType:
                  const TextInputType.numberWithOptions(decimal: true),
                  controller: _contentController,
                  decoration: const InputDecoration(labelText: 'Content'),
                ),
                const SizedBox(
                  height: 20,
                ),
                ElevatedButton(
                  child: Text(mode== 'addition' ? 'Add' : 'Update'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.red, // エラー時は"primary: Colors.red,"に変更
                  ),
                  onPressed: () async {
                    final String? todo = _todoController.text;
                    final String? content = _contentController.text;
                    // Addiing process
                    if (todo != null && content != null) {
                      if (mode == 'addition') {
                        // Persist a new product to Firestore
                        await _todos.add({"todo": todo, "content": content, "time":DateTime.now()});

                        // Show the snack bar for the add
                        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                            backgroundColor: Colors.red,
                            content: Text(
                              'Added ${todo}!',
                              style: const TextStyle(
                                  fontWeight: FontWeight.bold, fontSize: 15, color: Colors.white),
                            )));
                      }
                      // Editing process
                      if (mode == 'update') {
                        // Update the product
                        await _todos
                            .doc(documentSnapshot!.id)
                            .update({"todo": todo, "content": content});

                        // Show the snack bar for the edit
                        ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                            backgroundColor: Colors.red,
                            content: Text(
                              'Updated Content!',
                              style: TextStyle(
                                  fontWeight: FontWeight.bold, fontSize: 15, color: Colors.white, backgroundColor: Colors.red),
                            )));

                      }
                      // Clear the text fields
                      _todoController.text = '';
                      _contentController.text = '';

                      // Hide the bottom sheet
                      Navigator.of(context).pop();
                    }
                  },
                )
              ],
            ),
          );
        });
  }

  // Deletion Processing Functions
  Future<void> _deleteProduct(String productId) async {
    await _todos.doc(productId).delete();

    // Show the snack bar for the exclusion
    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
        backgroundColor: Colors.red,
        content: Text(
          'Deleted Content!',
          style: TextStyle(
              fontWeight: FontWeight.bold, fontSize: 15, color: Colors.white, backgroundColor: Colors.red),
        )));
  }

  passData(DocumentSnapshot snap){
    Navigator.of(context).push(MaterialPageRoute(builder: (context)=>PostDetails(snapshot: snap,)));
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        appBar: AppBar(
          title: const Text('ToDo App'),
          backgroundColor: Colors.red,
        ),
        // StreamBuilder to pass Firestore values to ListView.builder
        body: StreamBuilder(
          stream: _todos.snapshots(),
          builder: (context, AsyncSnapshot<QuerySnapshot> streamSnapshot) {
            if (streamSnapshot.hasData) {
              // Display FireStore values in list format
              return ListView.builder(
                itemCount: streamSnapshot.data!.docs.length,
                itemBuilder: (context, index) {
                  final DocumentSnapshot documentSnapshot =
                  streamSnapshot.data!.docs[index];
                  // View documents in the card widget
                  return Card(
                    margin: const EdgeInsets.all(10),
                    child: ListTile(
                      title: InkWell(
                        child:  Text(documentSnapshot['todo'],
                          style: TextStyle(fontSize: 22.0, color: Colors.red),
                          maxLines: 1,
                        ),
                        onTap: (){
                          passData(documentSnapshot);
                        },
                      ), // ToDo
                      subtitle: Text(documentSnapshot['content'].toString(),
                        maxLines: 4,
                      ), // Content
                      trailing: SizedBox(
                        width: 100,

                        child: Row(
                            mainAxisAlignment: MainAxisAlignment.start,
                            children: <Widget>[

                              IconButton(
                                  color: Colors.red,
                                  icon: const Icon(Icons.edit),
                                  onPressed: () =>
                                      _add_or_update(documentSnapshot)),
                              // Delete Button
                              IconButton(
                                  color: Colors.red,
                                  icon: const Icon(Icons.delete),
                                  onPressed: () =>
                                      _deleteProduct(documentSnapshot.id)),

                            ]
                        ),
                      ),
                    ),
                  );
                },
              );
            }

            return const Center(
              child: CircularProgressIndicator(),
            );
          },
        ),
        // Add Button
        floatingActionButton: FloatingActionButton(
          backgroundColor: Colors.red,
          onPressed: () => _add_or_update(),
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

///////////////////////////////
class PostDetails extends StatefulWidget {

  DocumentSnapshot snapshot;

  PostDetails({required this.snapshot});

  @override
  State<PostDetails> createState() => _PostDetailsState();
}

class _PostDetailsState extends State<PostDetails> {
  final CollectionReference _todos = FirebaseFirestore.instance.collection('todo');

  passData(DocumentSnapshot snap){
    Navigator.of(context).push(MaterialPageRoute(builder: (context)=>SubPostDetails(snapshot: snap,)));
  }

  // Deletion Processing Functions
  Future<void> _deleteProduct(String productId) async {
    await _todos.doc(widget.snapshot.id).collection('SubCollection').doc(productId).delete();

    // Show the snack bar for the exclusion
    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
        backgroundColor: Colors.red,
        content: Text(
          'Deleted Content!',
          style: TextStyle(
              fontWeight: FontWeight.bold, fontSize: 15, color: Colors.white, backgroundColor: Colors.red),
        )));
  }

  @override
  Widget build(BuildContext context) {
    var _screenSize = MediaQuery.of(context).size;

    return Scaffold(
      appBar: AppBar(
        title: Text("Details"),
        backgroundColor: Colors.red,
      ),

      body: Card(
          elevation: 10.0,
          margin: EdgeInsets.all(10.0),
          child: ListView(
            children: <Widget>[

              Container(
                  padding: EdgeInsets.all(10.0),
                  child: Row(
                    children: <Widget>[
                      Flexible(
                          child: Text(widget.snapshot.get('todo'),
                              style: TextStyle(fontSize: 18.0, color: Colors.red,)

                          )
                      ),
                    ],
                  )
              ),

              SizedBox(height: 10.0, ),
              Container(
                margin: EdgeInsets.all(10.0),
                child: Text(widget.snapshot.get('content'),
                  style: TextStyle(fontSize: 15.0),),
              ),

              SizedBox(height: 10.0, ),

              Container(
                width: _screenSize.width,
                height: _screenSize.height / 2,
                child:StreamBuilder(
                  stream: _todos.doc(widget.snapshot.id).collection('SubCollection').snapshots(),
                  builder: (context, AsyncSnapshot<QuerySnapshot> streamSnapshot) {
                    if (streamSnapshot.hasData) {
                      // Display FireStore values in list format
                      return ListView.builder(
                        itemCount: streamSnapshot.data!.docs.length,
                        itemBuilder: (context, index) {
                          final DocumentSnapshot documentSnapshot =
                          streamSnapshot.data!.docs[index];
                          // View documents in the card widget
                          return Card(
                            margin: const EdgeInsets.all(10),
                            child: ListTile(
                              title: InkWell(
                                child:  Text(documentSnapshot['todo2'],
                                  style: TextStyle(fontSize: 22.0, color: Colors.red),
                                  maxLines: 1,
                                ),
                                onTap: (){
                                  passData(documentSnapshot);
                                },
                              ),
                              subtitle: Text(documentSnapshot['content2'].toString(),
                                maxLines: 4,
                              ), // Content
                              trailing: SizedBox(
                                width: 100,

                                child: Row(
                                    mainAxisAlignment: MainAxisAlignment.start,
                                    children: <Widget>[

                                      IconButton(
                                          color: Colors.red,
                                          icon: const Icon(Icons.edit),
                                          onPressed: () =>
                                              _add_or_update(documentSnapshot)),
                                      // Delete Button
                                      IconButton(
                                          color: Colors.red,
                                          icon: const Icon(Icons.delete),
                                          onPressed: () =>
                                              _deleteProduct(documentSnapshot.id)),

                                    ]
                                ),
                              ),
                            ),
                          );
                        },
                      );
                    }

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

      floatingActionButton: FloatingActionButton(
        backgroundColor: Colors.red,
        onPressed: () => _add_or_update(),
        child: const Icon(Icons.add),
      ),
    );
  }

  final TextEditingController _todoController = TextEditingController();
  final TextEditingController _contentController = TextEditingController();

  Future<void> _add_or_update([DocumentSnapshot? documentSnapshot]) async {
    String mode = 'addition';
    if (documentSnapshot != null) {
      mode = 'update';
      _todoController.text = documentSnapshot['todo2'];
      _contentController.text = documentSnapshot['content2'].toString();
    }

    await showModalBottomSheet(
        isScrollControlled: true,
        context: context,
        builder: (BuildContext ctx) {
          return Padding(
            padding: EdgeInsets.only(
                top: 20,
                left: 20,
                right: 20,
                // prevent the soft keyboard from covering text fields
                bottom: MediaQuery.of(ctx).viewInsets.bottom + 20),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                TextField(
                  controller: _todoController,
                  decoration: const InputDecoration(labelText: 'ToDo2'),
                ),
                TextField(
                  keyboardType:
                  const TextInputType.numberWithOptions(decimal: true),
                  controller: _contentController,
                  decoration: const InputDecoration(
                    labelText: 'Content2',
                  ),
                ),
                const SizedBox(
                  height: 20,
                ),
                ElevatedButton(
                  child: Text(mode== 'addition' ? 'Add' : 'Update'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.red, // エラー時は"primary: Colors.red,"に変更
                  ),
                  onPressed: () async {
                    final String? todo = _todoController.text;
                    final String? content = _contentController.text;
                    // Addiing process
                    if (todo != null && content != null) {
                      if (mode == 'addition') {
                        // Persist a new product to Firestore
                        await _todos.doc(widget.snapshot.id).collection('SubCollection').add({"todo2": todo, "content2": content, "time": DateTime.now()});

                        // Show the snack bar for the add
                        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                            backgroundColor: Colors.red,
                            content: Text(
                              'Added ${todo}!',
                              style: const TextStyle(
                                  fontWeight: FontWeight.bold, fontSize: 15, color: Colors.white),
                            )));
                      }
                      // Editing process
                      if (mode == 'update') {
                        // Update the product
                        await _todos.doc(widget.snapshot.id).collection('SubCollection').doc(documentSnapshot!.id).update({"todo2": todo, "content2": content});

                        // Show the snack bar for the edit
                        ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                            backgroundColor: Colors.red,
                            content: Text(
                              'Updated Content!',
                              style: TextStyle(
                                  fontWeight: FontWeight.bold, fontSize: 15, color: Colors.white, backgroundColor: Colors.red),
                            )));

                      }
                      // Clear the text fields
                      _todoController.text = '';
                      _contentController.text = '';

                      // Hide the bottom sheet
                      Navigator.of(context).pop();
                    }
                  },
                )
              ],
            ),
          );
        });
  }
}


///////////////////////////////
class SubPostDetails extends StatefulWidget {

  DocumentSnapshot snapshot;

  SubPostDetails({required this.snapshot});

  @override
  State<SubPostDetails> createState() => _SubPostDetailsState();
}

class _SubPostDetailsState extends State<SubPostDetails> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("SubDetails"),
        backgroundColor: Colors.red,
      ),

      body: Card(
          elevation: 10.0,
          margin: EdgeInsets.all(10.0),
          child: ListView(
            children: <Widget>[

              Container(
                  padding: EdgeInsets.all(10.0),
                  child: Row(
                    children: <Widget>[
                      Flexible(
                          child: Text(widget.snapshot.get('todo2'),
                              style: TextStyle(fontSize: 18.0, color: Colors.red,)

                          )
                      ),
                    ],
                  )
              ),

              SizedBox(height: 10.0, ),
              Container(
                margin: EdgeInsets.all(10.0),
                child: Text(widget.snapshot.get('content2'),
                  style: TextStyle(fontSize: 15.0),),
              ),
            ],
          )
      ),
    );
  }
}
```
プロジェクトを作成する。AndroidにだけチェックがはいっていればOK。
<img width="250" alt="step2_2_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/89f13fca-e4c4-c365-b6fe-4a99c56ccd28.png">

Firebase側は前回作成したプロジェクトを利用し続ける。

<img width="250" alt="step2_2_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/8355fccc-4bf0-c217-e8d9-ac325b73ebd5.png">

classpathを`android/build.gradle`の中にコピーする。

<img width="250" alt="step2_2_3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/7ceed2c9-d4dd-69f0-c952-78a80e0da6d7.png">

`android/app/build.gradle`にidをペーストして`apply plugin: 'com.google.gms.google-services`の通りに書き換える。

<img width="250" alt="step2_2_4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/30d5ad04-e39a-1337-10fc-7eedd1c190fd.png">

同様に`dependencies`の中の`implementation`の2行をコピーして`android/app/build.gradle`にペーストする。

<img width="250" alt="step2_2_5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/a395ab97-783d-4269-f0e2-7879c33763dc.png">

2つのパッケージを`dart package`で検索、コピーしたバージョン情報を`pubspec.yaml`にペースト、右上の`Pub get`を押す。

<img width="250" alt="step2_2_6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/0c8d4370-cd1c-bc1c-26c8-cc490d94a2e3.png">

android/app/build.gradleのminSdkVersionを23に、targetSdkVersionを30に変更する。アプリを作成する時期に応じてこれらの数字は増えていく。最適な数字についてはアプリをビルドするときにヒントとして推奨値をAndroid Studioが教えてくれる。

<img width="250" alt="step2_2_7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/74a137b7-0752-b9c0-827c-c182ce2b2e12.png">

`lib`フォルダ内のmain.dartに上記のコードを貼り付けて実行すると、以図左側の通りの無地のアプリが起動する。右下の＋ボタンを押すと小さいウィンドウが下図真ん中の様なにスクリーン下部に現れる。記事のタイトルと説明文章を書いてAddを押すと、下図右側の様なリスト表記で結果が表示される。

<img width="250" alt="step2_2_8.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/1b8a324d-5a72-111a-e8e9-1a983936c568.png">

記事のタイトルを押すと説明文章の全体が下図右側の通りに表示される。

<img width="250" alt="step2_2_9.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/dddcefb3-fdf3-a0b9-a1b5-81ccb071f28e.png">

記事は、子となる記事情報を管理できる。例えば下図のようにtodo_task1とtodo_task2の二つの記事あがるとき、それぞれの詳細ページから＋ボタンを押して更に記事を登録することで子となる記事情報を管理できる。
<img width="250" alt="step2_2_10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/3c970412-8d7f-a220-3132-835a632de3da.png">

子の記事情報はサブコレクションとして親の記事の特定のドキュメントに関連付けられる形でFireStore上に情報蓄積される。

<img width="250" alt="step2_2_11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/ce3436ea-2b5f-cf93-a2cd-d2f208e91a5b.png">

## 2-3.Blogアプリでの情報表示機能を作ろう（Webアプリ編）
lib/main.dartのコードの全体を以下に示す。
``` .dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: FirebaseOptions(
      apiKey: "...",
      authDomain: "...",
      projectId: "...",
      storageBucket: "...",
      messagingSenderId: "...",
      appId: "...",
      measurementId: "..."));
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'ToDo App',
      home: ToDoPage(),
    );
  }
}

class ToDoPage extends StatefulWidget {
  const ToDoPage({Key? key}) : super(key: key);
  @override
  _ToDoPageState createState() => _ToDoPageState();
}

class _ToDoPageState extends State<ToDoPage> {
  final TextEditingController _todoController = TextEditingController();
  final TextEditingController _contentController = TextEditingController();

  final CollectionReference _todos =
  FirebaseFirestore.instance.collection('todo');
  
  passData(DocumentSnapshot snap){
    Navigator.of(context).push(MaterialPageRoute(builder: (context)=>PostDetails(snapshot: snap,)));
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        appBar: AppBar(
          title: const Text('ToDo App'),
          backgroundColor: Colors.red,
        ),
        // StreamBuilder to pass Firestore values to ListView.builder
        body: StreamBuilder(
          stream: _todos.snapshots(),
          builder: (context, AsyncSnapshot<QuerySnapshot> streamSnapshot) {
            if (streamSnapshot.hasData) {
              // Display FireStore values in list format
              return ListView.builder(
                itemCount: streamSnapshot.data!.docs.length,
                itemBuilder: (context, index) {
                  final DocumentSnapshot documentSnapshot =
                  streamSnapshot.data!.docs[index];
                  // View documents in the card widget
                  return Card(
                    margin: const EdgeInsets.all(10),
                    child: ListTile(
                      title: InkWell(
                        child:  Text(documentSnapshot['todo'],
                          style: TextStyle(fontSize: 22.0, color: Colors.red),
                          maxLines: 1,
                        ),
                        onTap: (){
                          passData(documentSnapshot);
                        },
                      ),
                      subtitle: Text(documentSnapshot['content'].toString(),
                        maxLines: 4,
                      ),
                    ),
                  );
                },
              );
            }

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

///////////////////////////////
class PostDetails extends StatefulWidget {

  DocumentSnapshot snapshot;

  PostDetails({required this.snapshot});

  @override
  State<PostDetails> createState() => _PostDetailsState();
}

class _PostDetailsState extends State<PostDetails> {
  final CollectionReference _todos = FirebaseFirestore.instance.collection('todo');

  passData(DocumentSnapshot snap){
    Navigator.of(context).push(MaterialPageRoute(builder: (context)=>SubPostDetails(snapshot: snap,)));
  }


  @override
  Widget build(BuildContext context) {
    var _screenSize = MediaQuery.of(context).size;

    return Scaffold(
      appBar: AppBar(
        title: Text("Details"),
        backgroundColor: Colors.red,
      ),

      body: Card(
          elevation: 10.0,
          margin: EdgeInsets.all(10.0),
          child: ListView(
            children: <Widget>[

              Container(
                  padding: EdgeInsets.all(10.0),
                  child: Row(
                    children: <Widget>[
                      Flexible(
                          child: Text(widget.snapshot.get('todo'),
                              style: TextStyle(fontSize: 18.0, color: Colors.red,)
                          )
                      ),
                    ],
                  )
              ),

              SizedBox(height: 10.0, ),
              Container(
                margin: EdgeInsets.all(10.0),
                child: Text(widget.snapshot.get('content'),
                  style: TextStyle(fontSize: 15.0),),
              ),

              SizedBox(height: 10.0, ),

              Container(
                width: _screenSize.width,
                height: _screenSize.height / 2,
                child:StreamBuilder(
                  stream: _todos.doc(widget.snapshot.id).collection('SubCollection').snapshots(),
                  builder: (context, AsyncSnapshot<QuerySnapshot> streamSnapshot) {
                    if (streamSnapshot.hasData) {
                      // Display FireStore values in list format
                      return ListView.builder(
                        itemCount: streamSnapshot.data!.docs.length,
                        itemBuilder: (context, index) {
                          final DocumentSnapshot documentSnapshot =
                          streamSnapshot.data!.docs[index];
                          // View documents in the card widget
                          return Card(
                            margin: const EdgeInsets.all(10),
                            child: ListTile(
                              title: InkWell(
                                child:  Text(documentSnapshot['todo2'],
                                  style: TextStyle(fontSize: 22.0, color: Colors.red),
                                  maxLines: 1,
                                ),
                                onTap: (){
                                  passData(documentSnapshot);
                                },
                              ),
                              subtitle: Text(documentSnapshot['content2'].toString(),
                                maxLines: 4,
                              ),
                            ),
                          );
                        },
                      );
                    }

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

  final TextEditingController _todoController = TextEditingController();
  final TextEditingController _contentController = TextEditingController();

}


///////////////////////////////
class SubPostDetails extends StatefulWidget {

  DocumentSnapshot snapshot;

  SubPostDetails({required this.snapshot});

  @override
  State<SubPostDetails> createState() => _SubPostDetailsState();
}

class _SubPostDetailsState extends State<SubPostDetails> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("SubDetails"),
        backgroundColor: Colors.red,
      ),

      body: Card(
          elevation: 10.0,
          margin: EdgeInsets.all(10.0),
          child: ListView(
            children: <Widget>[

              Container(
                  padding: EdgeInsets.all(10.0),
                  child: Row(
                    children: <Widget>[
                      Flexible(
                          child: Text(widget.snapshot.get('todo2'),
                              style: TextStyle(fontSize: 18.0, color: Colors.red,)

                          )
                      ),
                    ],
                  )
              ),

              SizedBox(height: 10.0, ),
              Container(
                margin: EdgeInsets.all(10.0),
                child: Text(widget.snapshot.get('content2'),
                  style: TextStyle(fontSize: 15.0),),
              ),
            ],
          )
      ),
    );
  }
}
```
ここでは新たにプロジェクトを作成する。今回はWebだけを選択してからFinishボタンを押す。`lib/main.dart`に上記のコードを貼り付ける。
<img width="250" alt="step2_3_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/04e2b41c-f918-a809-8337-f790be3f1cf9.png">

Firebaseのスマホアプリと同一のプロジェクトにて、Webアプリ用のボタンを押す。

<img width="250" alt="step2_3_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/ecdf1d46-3a67-3b19-04d7-31464cfc25a6.png">

すると、ウェブアプリにFirebaseを追加するという画面が表示される。アプリのニックネームを決めたら`アプリを登録`ボタンを押す。

<img width="250" alt="step2_3_3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/0f63c7de-d977-5ea8-054e-914a440f7595.png">

`<script>タグを使用する`のラジオボタンを選択してから赤枠部分をコピーしてから`次へ`を押す。

<img width="250" alt="step2_3_4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/5f000e6a-bfd8-7ea2-cc90-1a033acdc55d.png">

コピーしたscriptを`web/index.html`内に貼り付ける。

<img width="250" alt="step2_3_5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/b832bf1a-2c23-e1d1-96e6-3403fa06713b.png">

`lib/main.dart`内の`Firebase.initializeApp`の引数として`options: FirebaseOptions`と一緒に書き込む。

<img width="250" alt="step2_3_6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/40f10344-49f9-eca5-2615-3e34a440f304.png">

スマホアプリの時と同様に2つのパッケージを`dart package`で検索、コピーしたバージョン情報を`pubspec.yaml`にペースト、右上の`Pub get`を押す。

<img width="250" alt="step2_3_7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/298c8e93-3648-9413-4b32-336b7f46f6be.png">

実行ボタンを押すとWebブラウザが起動する。ブラウザ上にはスマホアプリの時に登録した記事情報が表示されている。現時点ではまだhostingを利用していないため、ローカルのコンピュータ環境内で結果表示されているだけで、第三者のコンピュータでは閲覧できない。

<img width="250" alt="step2_3_8.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/3ea1b57c-b481-f9a1-5a39-e9a6f6682949.png">

スマホアプリで登録した子の記事情報も正しく表示できるかを確認する。

<img width="250" alt="step2_3_9.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/0fdf550b-7b61-dbf1-fa01-485e4ef3c8b1.png">

## 2-4.Blogアプリでの情報表示機能をhostingを使って外部公開しよう
コマンドラインツールを使ってプロジェクトの初期設定を行う。`firebase login`をターミナルで実行するとブラウザに認証画面が表示されるので、アカウントを選択しログイン。
<img width="250" alt="step2_4_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/8ff7a906-0bf8-acbb-28d4-641c3863b7d8.png">

ブラウザ上で利用するアカウント情報を登録してログインする。
<img width="250" alt="step2_4_3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/1c8b2ee8-f2d1-9e1b-49cd-7a1cf4eda2df.png">

正しくログインできるとターミナル上にアカウント情報が`Success!Logged in as a ...`の後に表示される。
<img width="250" alt="step2_4_4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/1788c78a-1df8-79a3-78a7-a958712645d4.png">

ターミナルで`flutter build web`を実行。`build/web`フォルダ内に`index.html`が作成されていることを確認する。

<img width="250" alt="step2_4_5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/1fdb17ae-d822-f8ed-3e9d-d8ee0d79a825.png">

`index.html`に前回コピーしたのと同じscriptを`build/web/index.html`にも書き込む。
<img width="250" alt="step2_4_6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/2bb15eb7-3f03-5bf3-ffb6-f417fa69d64a.png">

ターミナルのカレントフォルダが`flutter_hosting`のルートであることを確認したのち、ターミナルでfirebase initを実行。 `Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys`を矢印ボタンとスペースボタンで選択してエンターキーを押す。
<img width="500" alt="step2_4_7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/2ccbad03-7e44-9446-3dc7-f700429a9471.png">

他の質問については下図の緑文字の通りに回答していく。
<img width="250" alt="step2_4_8.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/3d0874a3-6581-2698-af22-02074b0ed3c4.png">

`firebase deploy`を実行。以下が表示されていれば成功。
<img width="250" alt="step2_4_9.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/c9d254f5-874f-6fa6-1db4-e7f0374d5f31.png">

ターミナルに表示されるアドレスをWebブラウザに貼り付けると下図の様にローカル環境で表示されていたものと同じ内容をブラウザに表示されるはずである。記事をクリックして子のページが表示できるかも確認する。
<img width="250" alt="step2_4_10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/58f59cc9-72ea-ab10-cc3b-14ca71653d92.png">

# 3.まとめ
スマホアプリで記事のCRUD機能を作成、その結果を表示できるWebアプリを作成するとともにアプリをFirebase上にデプロイする方法を学んだ。