LoginSignup
0
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-03-25

1.はじめに

新たに6つのAndroidStudio&Flutter&Firebase記事を書いている。以下は内容、

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

2.Blogアプリを作ろう

2-1.Blogアプリの概要

Blogアプリについて以下に概要を記す。

2-1-1.Blogアプリ内の情報のつながり

スマホアプリでBlog記事を作成でき、Webアプリでは記事を表示することができる。記事はFirestoreに保存される。
step2_1_1.png

2-1-2.Blogアプリのページのつながり

スマホアプリで作る記事は、タイトルページをリストで表示する。タイトルページはサブタイトルをリストで表示する。
step2_1_2.png

2-1-3.情報追加部分の作成

スマホで作成できる記事は、タイトルページを個別に登録・編集・削除できる。タイトルページではサブタイトルページを個別に登録・編集・削除できる。タイトルページとサブタイトルページから戻る機能を有する。タイトルページとサブタイトルページは概要を記入できる。
画面に表示されいている記事をクリックすると子となる記事を表示できる。
step2_1_3.png
親と子の記事では記事を新たに追加するためのサブウィンドウが表示される。
step2_1_4.png

2-1-4.表示部分の作成

Webアプリではスマホで作成した記事を表示できる。 またhtmlに変換でき、hostingにdeployできる。
step2_1_5.png

2-2.Blogアプリへの情報登録・編集・削除機能を作ろう(スマホアプリ編)

以下2つを今回もそのまま利用する。

  • 認証アプリのために作った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。
step2_2_1.png

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

step2_2_2.png

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

step2_2_3.png

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

step2_2_4.png

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

step2_2_5.png

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

step2_2_6.png

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

step2_2_7.png

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

step2_2_8.png

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

step2_2_9.png

記事は、子となる記事情報を管理できる。例えば下図のようにtodo_task1とtodo_task2の二つの記事あがるとき、それぞれの詳細ページから+ボタンを押して更に記事を登録することで子となる記事情報を管理できる。
step2_2_10.png

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

step2_2_11.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に上記のコードを貼り付ける。
step2_3_1.png

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

step2_3_2.png

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

step2_3_3.png

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

step2_3_4.png

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

step2_3_5.png

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

step2_3_6.png

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

step2_3_7.png

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

step2_3_8.png

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

step2_3_9.png

2-4.Blogアプリでの情報表示機能をhostingを使って外部公開しよう

コマンドラインツールを使ってプロジェクトの初期設定を行う。firebase loginをターミナルで実行するとブラウザに認証画面が表示されるので、アカウントを選択しログイン。
step2_4_2.png

ブラウザ上で利用するアカウント情報を登録してログインする。
step2_4_3.png

正しくログインできるとターミナル上にアカウント情報がSuccess!Logged in as a ...の後に表示される。
step2_4_4.png

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

step2_4_5.png

index.htmlに前回コピーしたのと同じscriptをbuild/web/index.htmlにも書き込む。
step2_4_6.png

ターミナルのカレントフォルダがflutter_hostingのルートであることを確認したのち、ターミナルでfirebase initを実行。 Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploysを矢印ボタンとスペースボタンで選択してエンターキーを押す。
step2_4_7.png

他の質問については下図の緑文字の通りに回答していく。
step2_4_8.png

firebase deployを実行。以下が表示されていれば成功。
step2_4_9.png

ターミナルに表示されるアドレスをWebブラウザに貼り付けると下図の様にローカル環境で表示されていたものと同じ内容をブラウザに表示されるはずである。記事をクリックして子のページが表示できるかも確認する。
step2_4_10.png

3.まとめ

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0