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に保存される。
2-1-2.Blogアプリのページのつながり
スマホアプリで作る記事は、タイトルページをリストで表示する。タイトルページはサブタイトルをリストで表示する。
2-1-3.情報追加部分の作成
スマホで作成できる記事は、タイトルページを個別に登録・編集・削除できる。タイトルページではサブタイトルページを個別に登録・編集・削除できる。タイトルページとサブタイトルページから戻る機能を有する。タイトルページとサブタイトルページは概要を記入できる。
画面に表示されいている記事をクリックすると子となる記事を表示できる。
親と子の記事では記事を新たに追加するためのサブウィンドウが表示される。
2-1-4.表示部分の作成
Webアプリではスマホで作成した記事を表示できる。 またhtmlに変換でき、hostingにdeployできる。
2-2.Blogアプリへの情報登録・編集・削除機能を作ろう(スマホアプリ編)
以下2つを今回もそのまま利用する。
- 認証アプリのために作ったFirebaseのプロジェクト
- チャットアプリのために作ったCloud Firestoreのデータベース
そのため、classpathをandroid/build.gradleの中にコピーするところから、applicationIdを"com.example.my_auth_app"に書き換えるところまでの手続きはチャットアプリと同じなので説明を省く。
lib/main.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。
Firebase側は前回作成したプロジェクトを利用し続ける。
classpathをandroid/build.gradle
の中にコピーする。
android/app/build.gradle
にidをペーストしてapply plugin: 'com.google.gms.google-services
の通りに書き換える。
同様にdependencies
の中のimplementation
の2行をコピーしてandroid/app/build.gradle
にペーストする。
2つのパッケージをdart package
で検索、コピーしたバージョン情報をpubspec.yaml
にペースト、右上のPub get
を押す。
android/app/build.gradleのminSdkVersionを23に、targetSdkVersionを30に変更する。アプリを作成する時期に応じてこれらの数字は増えていく。最適な数字についてはアプリをビルドするときにヒントとして推奨値をAndroid Studioが教えてくれる。
lib
フォルダ内のmain.dartに上記のコードを貼り付けて実行すると、以図左側の通りの無地のアプリが起動する。右下の+ボタンを押すと小さいウィンドウが下図真ん中の様なにスクリーン下部に現れる。記事のタイトルと説明文章を書いてAddを押すと、下図右側の様なリスト表記で結果が表示される。
記事のタイトルを押すと説明文章の全体が下図右側の通りに表示される。
記事は、子となる記事情報を管理できる。例えば下図のようにtodo_task1とtodo_task2の二つの記事あがるとき、それぞれの詳細ページから+ボタンを押して更に記事を登録することで子となる記事情報を管理できる。
子の記事情報はサブコレクションとして親の記事の特定のドキュメントに関連付けられる形でFireStore上に情報蓄積される。
2-3.Blogアプリでの情報表示機能を作ろう(Webアプリ編)
lib/main.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
に上記のコードを貼り付ける。
Firebaseのスマホアプリと同一のプロジェクトにて、Webアプリ用のボタンを押す。
すると、ウェブアプリにFirebaseを追加するという画面が表示される。アプリのニックネームを決めたらアプリを登録
ボタンを押す。
<script>タグを使用する
のラジオボタンを選択してから赤枠部分をコピーしてから次へ
を押す。
コピーしたscriptをweb/index.html
内に貼り付ける。
lib/main.dart
内のFirebase.initializeApp
の引数としてoptions: FirebaseOptions
と一緒に書き込む。
スマホアプリの時と同様に2つのパッケージをdart package
で検索、コピーしたバージョン情報をpubspec.yaml
にペースト、右上のPub get
を押す。
実行ボタンを押すとWebブラウザが起動する。ブラウザ上にはスマホアプリの時に登録した記事情報が表示されている。現時点ではまだhostingを利用していないため、ローカルのコンピュータ環境内で結果表示されているだけで、第三者のコンピュータでは閲覧できない。
スマホアプリで登録した子の記事情報も正しく表示できるかを確認する。
2-4.Blogアプリでの情報表示機能をhostingを使って外部公開しよう
コマンドラインツールを使ってプロジェクトの初期設定を行う。firebase login
をターミナルで実行するとブラウザに認証画面が表示されるので、アカウントを選択しログイン。
正しくログインできるとターミナル上にアカウント情報がSuccess!Logged in as a ...
の後に表示される。
ターミナルでflutter build web
を実行。build/web
フォルダ内にindex.html
が作成されていることを確認する。
index.html
に前回コピーしたのと同じscriptをbuild/web/index.html
にも書き込む。
ターミナルのカレントフォルダがflutter_hosting
のルートであることを確認したのち、ターミナルでfirebase initを実行。 Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys
を矢印ボタンとスペースボタンで選択してエンターキーを押す。
firebase deploy
を実行。以下が表示されていれば成功。
ターミナルに表示されるアドレスをWebブラウザに貼り付けると下図の様にローカル環境で表示されていたものと同じ内容をブラウザに表示されるはずである。記事をクリックして子のページが表示できるかも確認する。
3.まとめ
スマホアプリで記事のCRUD機能を作成、その結果を表示できるWebアプリを作成するとともにアプリをFirebase上にデプロイする方法を学んだ。