11月24日 進捗日記[自分用]
作業内容
・参加者の気分を共有できるようにコード改変
・ボタンの作成
・Cloud Firestoreにデータを追加
・Cloud Firestoreからデータを取得
作業目的
・Cloud Firestoreを使いこなせるようにする。
改変前の画面
動作は11月23日の進捗日記を参照
改変後の画面
Cloud FireStoreの画面
ボタンの作成
まずは現在を表現できるボタンとして「良い」「まあまあ」「悪い」という文字の入ったボタンを生成。特に変わったことはなし。
enum Feel { good, soso, bad, unknown }
class HowAreYou extends StatelessWidget {
//コンストラクタ
const HowAreYou({required this.state, required this.onSelection});
final Feel state;
final void Function(Feel selection) onSelection;
@override
Widget build(BuildContext context) {
switch (state) {
case Feel.good:
return Padding(
padding: EdgeInsets.all(8.0),
child: Row(
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(elevation: 0),
onPressed: () => onSelection(Feel.good),
child: Text('良い'),
),
SizedBox(width: 8),
TextButton(
onPressed: () => onSelection(Feel.soso),
child: Text('まあまあ'),
),
SizedBox(width: 8),
TextButton(
onPressed: () => onSelection(Feel.bad),
child: Text('悪い'),
),
],
),
);
case Feel.soso:
return Padding(
padding: EdgeInsets.all(8.0),
child: Row(
children: [
TextButton(
onPressed: () => onSelection(Feel.good),
child: Text('良い'),
),
SizedBox(width: 8),
ElevatedButton(
style: ElevatedButton.styleFrom(elevation: 0),
onPressed: () => onSelection(Feel.soso),
child: Text('まあまあ'),
),
SizedBox(width: 8),
TextButton(
onPressed: () => onSelection(Feel.bad),
child: Text('悪い'),
),
],
),
);
case Feel.bad:
return Padding(
padding: EdgeInsets.all(8.0),
child: Row(
children: [
TextButton(
onPressed: () => onSelection(Feel.good),
child: Text('良い'),
),
SizedBox(width: 8),
TextButton(
onPressed: () => onSelection(Feel.soso),
child: Text('まあまあ'),
),
SizedBox(width: 8),
ElevatedButton(
style: ElevatedButton.styleFrom(elevation: 0),
onPressed: () => onSelection(Feel.bad),
child: Text('悪い'),
),
],
),
);
default:
return Padding(
padding: EdgeInsets.all(8.0),
child: Row(
children: [
StyledButton(
onPressed: () => onSelection(Feel.good),
child: Text('良い'),
),
SizedBox(width: 8),
StyledButton(
onPressed: () => onSelection(Feel.soso),
child: Text('まあまあ'),
),
SizedBox(width: 8),
StyledButton(
onPressed: () => onSelection(Feel.bad),
child: Text('悪い'),
),
],
),
);
}
}
}
Cloud Firestoreにデータを追加
次に押されたボタンに応じてデータベースにデータを追加
//ここで変数を定義
//プライベート変数
int _feelgood = 0;
int _feelsoso = 0;
int _feelbad = 0;
//プライベート変数を領域外から呼び出すためのget宣言
int get feelgood => _feelgood;
int get feelsoso => _feelsoso;
int get feelbad => _feelbad;
//初期状態を感情なしと定義
Feel _feel = Feel.unknown;
//StreamSubscriptionを理解していない(Provider関連??)
StreamSubscription<DocumentSnapshot>? _feelSubscription;
Feel get feel => _feel;
//ここでデータベースにデータを挿入
//.collection('feelbook')でコレクションを指定(コレクションはCloudfirebase用語)
//.doc(FirebaseAuth.instance.currentUser!.uid)はドキュメントをユーザIDにする。
set feel(Feel feel) {
final userDoc = FirebaseFirestore.instance
.collection('feelbook')
.doc(FirebaseAuth.instance.currentUser!.uid);
if (feel == Feel.good) {
userDoc.set(<String,String>{'feel': 'good'});
} else if(feel == Feel.soso){
userDoc.set(<String,String>{'feel': 'soso'});
} else{
userDoc.set(<String,String>{'feel': 'bad'});
}
}
Cloud Firestoreからデータを取得
次にデータベースのデータが更新された時にデータを取得してそれぞれの気分の人数を更新
//データベースでのそれぞれの気分の人数をカウント(効率悪そう)
FirebaseFirestore.instance
.collection('feelbook')
.where('feel', isEqualTo: 'good')
.snapshots()
.listen((snapshot) {
_feelgood = snapshot.docs.length;
notifyListeners();
});
FirebaseFirestore.instance
.collection('feelbook')
.where('feel', isEqualTo: 'soso')
.snapshots()
.listen((snapshot) {
_feelsoso = snapshot.docs.length;
notifyListeners();
});
FirebaseFirestore.instance
.collection('feelbook')
.where('feel', isEqualTo: 'bad')
.snapshots()
.listen((snapshot) {
_feelbad = snapshot.docs.length;
notifyListeners();
});
//データベースから自分の気分を取得
_feelSubscription = FirebaseFirestore.instance
.collection('feelbook')
.doc(user.uid)
.snapshots()
.listen((snapshot) {
if (snapshot.data() != null) {
if (snapshot.data()!['feel'] as String == 'good') {
_feel = Feel.good;
} else if(snapshot.data()!['feel'] as String == 'soso'){
_feel = Feel.soso;
} else{
_feel = Feel.bad;
}
} else {
_feel = Feel.unknown;
}
notifyListeners();
});
//取得した人数を表示
Consumer<ApplicationState>(
builder: (context, appState, _) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children:[
Header('今日の気分は?'),
Icon(
Icons.outlet,
color: Colors.amber,
),
],
),
Paragraph('良い:${appState.feelgood}人 まあまあ:${appState.feelsoso}人 悪い:${appState.feelbad}人'),
if (appState.loginState == ApplicationLoginState.loggedIn) ...[
HowAreYou(
state: appState.feel,
//よくわからんん
onSelection: (feel) => appState.feel = feel,
),
],
],
),
),
Flutterでアイコンを使う
以下のページから探せる。
Flutterであれば画像のダウンロードなども必要なく、ただ使いたいところにコードを入りつければいい
https://fonts.google.com/icons?selected=Material+Icons
以下の例はoutletというアイコンを使う場合。
色もコンテナなどと同様につけることができる
Icon(
Icons.outlet,
),
感想
改変をすることでだいぶCloudFirestoreの使い方がわかってきた。ただ、Providerがよくわかっていないこともあり掴めない部分があるのでそれらを勉強したい。ただ、最近はProviderよりRiverPodsぽいのでそっちを勉強した方がいいのかな?
また今後としてはAuthとFirestoreを使ったアプリを作る練習をしたい。今のところLINEのようなアプリの一部機能を模倣したいと考えている。
コードの全体像
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:firebase_core/firebase_core.dart'; // new
import 'package:firebase_auth/firebase_auth.dart'; // new
import 'package:provider/provider.dart'; //new
import 'src/authentication.dart'; //new
import 'dart:async'; // new
import 'package:cloud_firestore/cloud_firestore.dart';
import 'src/widgets.dart';
void main() {
runApp(
//
ChangeNotifierProvider(
create: (context) => ApplicationState(),
builder: (context, _) => App()
),
);
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Firebase Meetup',
theme: ThemeData(
buttonTheme: Theme.of(context).buttonTheme.copyWith(
highlightColor: Colors.deepPurple,
),
primarySwatch: Colors.deepPurple,
textTheme: GoogleFonts.robotoTextTheme(
Theme.of(context).textTheme,
),
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Firebase Meetup'),
),
body: ListView(
children: <Widget>[
// Image.asset('assets/codelab.png'),
const SizedBox(height: 8),
const IconAndDetail(Icons.calendar_today, 'October 30'),
const IconAndDetail(Icons.location_city, 'San Francisco'),
Consumer<ApplicationState>(
builder: (context, appState, _) => Authentication(
email: appState.email,
loginState: appState.loginState,
startLoginFlow: appState.startLoginFlow,
verifyEmail: appState.verifyEmail,
signInWithEmailAndPassword: appState.signInWithEmailAndPassword,
cancelRegistration: appState.cancelRegistration,
registerAccount: appState.registerAccount,
signOut: appState.signOut,
),
),
const Divider(
height: 8,
thickness: 1,
indent: 8,
endIndent: 8,
color: Colors.grey,
),
const Header("What we'll be doing"),
const Paragraph(
'Join us for a day full of Firebase Workshops and Sushi!',
),
// Add the following two lines.
// Modify from here
Consumer<ApplicationState>(
builder: (context, appState, _) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (appState.attendees >= 2)
Paragraph('${appState.attendees} people going')
else if (appState.attendees == 1)
Paragraph('1 person going')
else
Paragraph('No one going'),
if (appState.loginState == ApplicationLoginState.loggedIn) ...[
YesNoSelection(
state: appState.attending,
onSelection: (attending) => appState.attending = attending,
),
],
Row(
children:[
Header('今日の気分は?'),
Icon(
Icons.outlet,
color: Colors.amber,
),
],
),
Paragraph('良い:${appState.feelgood}人 まあまあ:${appState.feelsoso}人 悪い:${appState.feelbad}人'),
if (appState.loginState == ApplicationLoginState.loggedIn) ...[
HowAreYou(
state: appState.feel,
onSelection: (feel) => appState.feel = feel,
),
],
if (appState.loginState == ApplicationLoginState.loggedIn) ...[
Header('Discussion'),
GuestBook(
addMessage: (String message) =>
appState.addMessageToGuestBook(message),
messages: appState.guestBookMessages, // new
),
],
],
),
),
// To here.
],
),
);
}
}
class ApplicationState extends ChangeNotifier {
ApplicationState() {
init();
}
int _attendees = 0;
int get attendees => _attendees;
Attending _attending = Attending.unknown;
StreamSubscription<DocumentSnapshot>? _attendingSubscription;
Attending get attending => _attending;
//ここで変数を定義
int _feelgood = 0;
int _feelsoso = 0;
int _feelbad = 0;
int get feelgood => _feelgood;
int get feelsoso => _feelsoso;
int get feelbad => _feelbad;
Feel _feel = Feel.unknown;
//StreamSubscriptionを理解していない。
StreamSubscription<DocumentSnapshot>? _feelSubscription;
Feel get feel => _feel;
//出席関連
set attending(Attending attending) {
final userDoc = FirebaseFirestore.instance
.collection('attendees')
.doc(FirebaseAuth.instance.currentUser!.uid);
if (attending == Attending.yes) {
userDoc.set(<String,bool>{'attending': true});
} else {
userDoc.set(<String,bool>{'attending': false});
}
}
//今日の気分関連
set feel(Feel feel) {
final userDoc = FirebaseFirestore.instance
.collection('feelbook')
.doc(FirebaseAuth.instance.currentUser!.uid);
if (feel == Feel.good) {
userDoc.set(<String,String>{'feel': 'good'});
} else if(feel == Feel.soso){
userDoc.set(<String,String>{'feel': 'soso'});
} else{
userDoc.set(<String,String>{'feel': 'bad'});
}
}
// Add from here
Future<DocumentReference> addMessageToGuestBook(String message) {
if (_loginState != ApplicationLoginState.loggedIn) {
throw Exception('Must be logged in');
}
return FirebaseFirestore.instance.collection('guestbook').add(<String, dynamic>{
'text': message,
'timestamp': DateTime.now().millisecondsSinceEpoch,
'name': FirebaseAuth.instance.currentUser!.displayName,
'userId': FirebaseAuth.instance.currentUser!.uid,
});
}
// To here
Future<void> init() async {
await Firebase.initializeApp();
//人数
FirebaseFirestore.instance
.collection('attendees')
.where('attending', isEqualTo: true)
.snapshots()
.listen((snapshot) {
_attendees = snapshot.docs.length;
notifyListeners();
});
//気分
FirebaseFirestore.instance
.collection('feelbook')
.where('feel', isEqualTo: 'good')
.snapshots()
.listen((snapshot) {
_feelgood = snapshot.docs.length;
notifyListeners();
});
FirebaseFirestore.instance
.collection('feelbook')
.where('feel', isEqualTo: 'soso')
.snapshots()
.listen((snapshot) {
_feelsoso = snapshot.docs.length;
notifyListeners();
});
FirebaseFirestore.instance
.collection('feelbook')
.where('feel', isEqualTo: 'bad')
.snapshots()
.listen((snapshot) {
_feelbad = snapshot.docs.length;
notifyListeners();
});
//リスナー
// FirebaseのAuth libralyはこのcallbackが必要。
FirebaseAuth.instance.userChanges().listen((user) {
if (user != null) {
_loginState = ApplicationLoginState.loggedIn;
// Add from here
_guestBookSubscription = FirebaseFirestore.instance
.collection('guestbook')
.orderBy('timestamp', descending: true)
.snapshots()
.listen((snapshot) {
_guestBookMessages = [];
snapshot.docs.forEach((document) {
_guestBookMessages.add(
GuestBookMessage(
name: document.data()['name'] as String,
message: document.data()['text'] as String,
),
);
});
notifyListeners();
});
_attendingSubscription = FirebaseFirestore.instance
.collection('attendees')
.doc(user.uid)
.snapshots()
.listen((snapshot) {
if (snapshot.data() != null) {
if (snapshot.data()!['attending'] as bool) {
_attending = Attending.yes;
} else {
_attending = Attending.no;
}
} else {
_attending = Attending.unknown;
}
notifyListeners();
});
//気分のリスナー
_feelSubscription = FirebaseFirestore.instance
.collection('feelbook')
.doc(user.uid)
.snapshots()
.listen((snapshot) {
if (snapshot.data() != null) {
if (snapshot.data()!['feel'] as String == 'good') {
_feel = Feel.good;
} else if(snapshot.data()!['feel'] as String == 'soso'){
_feel = Feel.soso;
} else{
_feel = Feel.bad;
}
} else {
_feel = Feel.unknown;
}
notifyListeners();
});
// to here.
} else {
_loginState = ApplicationLoginState.loggedOut;
// Add from here
_guestBookMessages = [];
_guestBookSubscription?.cancel();
_attendingSubscription?.cancel();
_feelSubscription?.cancel();
// to here.
}
notifyListeners();
});
}
ApplicationLoginState _loginState = ApplicationLoginState.loggedOut;
ApplicationLoginState get loginState => _loginState;
String? _email;
String? get email => _email;
// Add from here
StreamSubscription<QuerySnapshot>? _guestBookSubscription;
List<GuestBookMessage> _guestBookMessages = [];
List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
// to here.
void startLoginFlow() {
_loginState = ApplicationLoginState.emailAddress;
notifyListeners();
}
void verifyEmail(
String email,
void Function(FirebaseAuthException e) errorCallback,
) async {
try {
var methods =
await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);
if (methods.contains('password')) {
_loginState = ApplicationLoginState.password;
} else {
_loginState = ApplicationLoginState.register;
}
_email = email;
notifyListeners();
} on FirebaseAuthException catch (e) {
errorCallback(e);
}
}
void signInWithEmailAndPassword(
String email,
String password,
void Function(FirebaseAuthException e) errorCallback,
) async {
try {
await FirebaseAuth.instance.signInWithEmailAndPassword(
email: email,
password: password,
);
} on FirebaseAuthException catch (e) {
errorCallback(e);
}
}
void cancelRegistration() {
_loginState = ApplicationLoginState.emailAddress;
notifyListeners();
}
void registerAccount(String email, String displayName, String password,
void Function(FirebaseAuthException e) errorCallback) async {
try {
var credential = await FirebaseAuth.instance
.createUserWithEmailAndPassword(email: email, password: password);
await credential.user!.updateProfile(displayName: displayName);
} on FirebaseAuthException catch (e) {
errorCallback(e);
}
}
void signOut() {
FirebaseAuth.instance.signOut();
}
}
class GuestBook extends StatefulWidget {
// Modify the following line
GuestBook({required this.addMessage, required this.messages});
final FutureOr<void> Function(String message) addMessage;
final List<GuestBookMessage> messages; // new
@override
_GuestBookState createState() => _GuestBookState();
}
class _GuestBookState extends State<GuestBook> {
final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
final _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Form(
key: _formKey,
child: Row(
children: [
Expanded(
child: TextFormField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'Leave a message',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Enter your message to continue';
}
return null;
},
),
),
SizedBox(width: 8),
StyledButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
await widget.addMessage(_controller.text);
_controller.clear();
}
},
child: Row(
children: [
Icon(Icons.send),
SizedBox(width: 4),
Text('SEND'),
],
),
),
],
),
),
),
SizedBox(height: 8),
for (var message in widget.messages)
Paragraph('${message.name}: ${message.message}'),
SizedBox(height: 8),
]
);
}
}
enum Attending { yes, no, unknown }
class GuestBookMessage {
GuestBookMessage({required this.name, required this.message});
final String name;
final String message;
}
class YesNoSelection extends StatelessWidget {
const YesNoSelection({required this.state, required this.onSelection});
final Attending state;
//こいつ何?
final void Function(Attending selection) onSelection;
@override
Widget build(BuildContext context) {
switch (state) {
case Attending.yes:
return Padding(
padding: EdgeInsets.all(8.0),
child: Row(
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(elevation: 0),
onPressed: () => onSelection(Attending.yes),
child: Text('YES'),
),
SizedBox(width: 8),
TextButton(
onPressed: () => onSelection(Attending.no),
child: Text('NO'),
),
],
),
);
case Attending.no:
return Padding(
padding: EdgeInsets.all(8.0),
child: Row(
children: [
TextButton(
onPressed: () => onSelection(Attending.yes),
child: Text('YES'),
),
SizedBox(width: 8),
ElevatedButton(
style: ElevatedButton.styleFrom(elevation: 0),
onPressed: () => onSelection(Attending.no),
child: Text('NO'),
),
],
),
);
default:
return Padding(
padding: EdgeInsets.all(8.0),
child: Row(
children: [
StyledButton(
onPressed: () => onSelection(Attending.yes),
child: Text('YES'),
),
SizedBox(width: 8),
StyledButton(
onPressed: () => onSelection(Attending.no),
child: Text('NO'),
),
],
),
);
}
}
}
enum Feel { good, soso, bad, unknown }
class HowAreYou extends StatelessWidget {
//コンストラクタ
const HowAreYou({required this.state, required this.onSelection});
final Feel state;
final void Function(Feel selection) onSelection;
@override
Widget build(BuildContext context) {
switch (state) {
case Feel.good:
return Padding(
padding: EdgeInsets.all(8.0),
child: Row(
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(elevation: 0),
onPressed: () => onSelection(Feel.good),
child: Text('良い'),
),
SizedBox(width: 8),
TextButton(
onPressed: () => onSelection(Feel.soso),
child: Text('まあまあ'),
),
SizedBox(width: 8),
TextButton(
onPressed: () => onSelection(Feel.bad),
child: Text('悪い'),
),
],
),
);
case Feel.soso:
return Padding(
padding: EdgeInsets.all(8.0),
child: Row(
children: [
TextButton(
onPressed: () => onSelection(Feel.good),
child: Text('良い'),
),
SizedBox(width: 8),
ElevatedButton(
style: ElevatedButton.styleFrom(elevation: 0),
onPressed: () => onSelection(Feel.soso),
child: Text('まあまあ'),
),
SizedBox(width: 8),
TextButton(
onPressed: () => onSelection(Feel.bad),
child: Text('悪い'),
),
],
),
);
case Feel.bad:
return Padding(
padding: EdgeInsets.all(8.0),
child: Row(
children: [
TextButton(
onPressed: () => onSelection(Feel.good),
child: Text('良い'),
),
SizedBox(width: 8),
TextButton(
onPressed: () => onSelection(Feel.soso),
child: Text('まあまあ'),
),
SizedBox(width: 8),
ElevatedButton(
style: ElevatedButton.styleFrom(elevation: 0),
onPressed: () => onSelection(Feel.bad),
child: Text('悪い'),
),
],
),
);
default:
return Padding(
padding: EdgeInsets.all(8.0),
child: Row(
children: [
StyledButton(
onPressed: () => onSelection(Feel.good),
child: Text('良い'),
),
SizedBox(width: 8),
StyledButton(
onPressed: () => onSelection(Feel.soso),
child: Text('まあまあ'),
),
SizedBox(width: 8),
StyledButton(
onPressed: () => onSelection(Feel.bad),
child: Text('悪い'),
),
],
),
);
}
}
}