1. 前回のあらすじ
第一話では、todoアプリの作成をしました。
まだご覧になられていない方は是非ご覧ください!
第一話はこちらから
2. Firebaseの導入経緯
ですが、作成したtodoを直接配列に格納していたのでビルドし直すと、表示されているtodoが画面から消えてしまいます。
なのでFirebaseを導入してFirestoreにデータを保存していきたいと思いました!
導入で参考にしたのは公式ドキュメントになります。
3. 完成したアプリ
4. 追加したファイル
domainディレクトリあったtodo_domain.dart
をReNameし、todo_domain_old.dart
に変更して、
todo_domain.dart
を追加しました。(配列追加番もなんとなく残したくて。。笑)
追加したファイルにFirebaseの操作(データを取得したり追加したり等)や、取得したデータの加工のメソッドを記載していきました。
ui/todoディレクトリのtest.dartはお気になさらず。。(試したいことがあったので一時的に作成したファイルです。)
5. 修正後の各ファイルの説明
- main.dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:todo_app/ui/todo/list.dart';
void main() async{
WidgetsFlutterBinding.ensureInitialized();
// Firebaseを使用する前FlutterFileを初期化
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Todoアプリ',
home: TodoList(),
);
}
}
- pubspec.yaml
dependencies:
flutter:
sdk: flutter
provider: ^4.3.2+2
# FirebaseとFirestoreを使用できるようにFlutterで下記二行を追加
firebase_core: ^0.5.0
cloud_firestore: "^0.14.0+2"
追加したあとはfluuter pub get
コマンド実行をお忘れなく!
- list.dart
画面で修正前と後で変わった点は、cardのtodo内容を表示するときに作成日も表示させるようにした箇所くらいです!
あとは、todo完了済みの時、完了済みがわかるように橙色?になるようにしているのですが、
そこをtodo_domain.dart
にメソッドとして用意して呼び出すようにしました。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:todo_app/domain/todo_domain.dart';
import 'complete.dart';
import 'incomplete.dart';
class TodoList extends StatelessWidget {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<TodoDomain>(
create: (_) => TodoDomain()..getTodos(),
child: Scaffold(
appBar: AppBar(
title: Text('TodoLists'),
actions: <Widget>[
Consumer<TodoDomain>(
builder: (context, model, child) {
return IconButton(
icon: Icon(Icons.add),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'追加したいTodoを教えて!',
style: TextStyle(fontSize: 14),
),
content: Form(
key: _formKey,
child: TextFormField(
keyboardType: TextInputType.multiline,
maxLines: null,
// ignore: missing_return
validator: (value) {
if (value.isEmpty) {
return '今日する事教えてくれないの。。?';
}
},
onChanged: (String text) async {
model.todo = text;
},
),
),
actions: <Widget>[
FlatButton(
child: Text('OK'),
onPressed: () async {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
}
try {
// todoの追加
await model.addTodo();
Navigator.pop(context);
} catch (e) {
print('今日する事が入力されていません');
}
},
),
],
);
},
);
},
);
},
)
],
),
body: Consumer<TodoDomain>(
builder: (context, model, child) {
final todos = model.todos;
final cards = todos
.map(
(todo) => Card(
child: Container(
color: model.getCompletedColor(todo.isCompleted),
child: ListTile(
title: Text(
model.getText(
date: todo.createdAt.toDate(),
text: todo.text,
),
style: TextStyle(fontSize: 14),
),
contentPadding: EdgeInsets.all(8),
onLongPress: () {
if (!todo.isCompleted) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Align(
alignment: Alignment.center,
child: Text(
'完了しましたか?',
style: TextStyle(fontSize: 14),
textAlign: TextAlign.center,
),
),
// AlertDialogではボタン関係はactionsで定義しなくてはいけない
actions: [
FlatButton(
onPressed: () async {
await model.completeTodo(id: todo.id);
Navigator.of(context).pop();
},
child: Text('OK'),
),
FlatButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('NG'),
),
],
);
},
);
}
},
// ListTileのtrailingに2つアイコンを並べたい時はWrapしてあげる
trailing: Wrap(
children: [
IconButton(
icon: Icon(Icons.edit),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
'追加したTodo間違えちゃった?',
style: TextStyle(fontSize: 14),
),
content: Form(
key: _formKey,
child: TextFormField(
keyboardType: TextInputType.multiline,
maxLines: null,
initialValue: todo.text,
// ignore: missing_return
validator: (value) {
if (value.isEmpty) {
return '今日する事教えてくれないの。。?';
}
},
onChanged: (String text) {
model.todo = text;
},
),
),
// AlertDialogではボタン関係はactionsで定義しなくてはいけない
actions: <Widget>[
FlatButton(
child: Text('OK'),
onPressed: () async {
if (_formKey.currentState
.validate()) {
_formKey.currentState.save();
}
try {
// todoの追加
await model.updateTodoText(
id: todo.id,
text: model.todo);
// TodoListsページに戻って、dialogを閉じる
Navigator.pop(context);
} catch (e) {
print('今日する事が入力されていません');
}
},
),
],
);
},
);
},
),
IconButton(
icon: Icon(
Icons.delete,
color: Colors.redAccent,
),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Align(
alignment: Alignment.center,
child: Text(
'本当に削除しますか?',
style: TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
),
actions: [
FlatButton(
onPressed: () async {
await model.deleteTodo(id: todo.id);
Navigator.of(context).pop();
},
child: Text('OK'),
),
FlatButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('NG'),
),
],
);
},
);
},
),
],
),
),
),
),
)
.toList();
return ListView(
children: cards,
);
},
),
drawer: Consumer<TodoDomain>(
builder: (context, model, child) {
return Drawer(
child: ListView(
children: <Widget>[
SizedBox(
height: 80,
child: DrawerHeader(
child: Text(
'Menu',
style: TextStyle(fontSize: 36, color: Colors.white),
),
decoration: BoxDecoration(color: Colors.blue),
),
),
Card(
child: ListTile(
title: Text('Incomplete'),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => TodoIncomplete(),
),
);
},
),
),
Card(
child: ListTile(
title: Text('Complete'),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => TodoComplete(),
),
);
},
),
)
],
),
);
},
),
),
);
}
}
- complete.todo
更新日を表示できるように修正しました。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:todo_app/domain/todo_domain.dart';
class TodoComplete extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<TodoDomain>(
create: (_) => TodoDomain()..getCompleteTodos(),
child: Scaffold(
appBar: AppBar(
title: Text('completeTodo'),
),
body: Consumer<TodoDomain>(
builder: (context, model, child) {
final completeTodos = model.completeTodos;
final cards = completeTodos
.map(
(completeTodo) => Card(
child: Container(
child: ListTile(
title: Text(
model.getText(
date: completeTodo.updatedAt.toDate(),
text: completeTodo.text,
),
),
),
),
),
)
.toList();
return ListView(
children: cards,
);
},
),
),
);
}
}
- incomplete.todo
作成日を表示できるように修正しました。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:todo_app/domain/todo_domain.dart';
class TodoIncomplete extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<TodoDomain>(
create: (_) => TodoDomain()..getIncompleteTodos(),
child: Scaffold(
appBar: AppBar(
title: Text('IncompleteTodo'),
),
body: Consumer<TodoDomain>(
builder: (context, model, child) {
final incompleteTodos = model.incompleteTodos;
final cards = incompleteTodos
.map(
(incompleteTodo) => Card(
child: Container(
child: ListTile(
title: Text(
model.getText(
date: incompleteTodo.createdAt.toDate(),
text: incompleteTodo.text,
),
),
),
),
),
)
.toList();
return ListView(
children: cards,
);
},
),
),
);
}
}
- todo_entity.dart
コンストラクタ用に用意しました。
import 'package:cloud_firestore/cloud_firestore.dart';
class TodoEntity {
// DocumentSnapshotにはFirestore内のドキュメントが含まれたデータが格納されている
TodoEntity(DocumentSnapshot doc) {
// ドキュメントから指定したフィールドの値を取得し、インスタンス変数に値を格納してオブジェクトを作成
id = doc.id;
text = doc.data()['text'];
isCompleted = doc.data()['isCompleted'];
createdAt = doc.data()['createdAt'];
updatedAt = doc.data()['updatedAt'];
}
// インスタンス変数
String id;
String text;
bool isCompleted;
Timestamp createdAt;
Timestamp updatedAt;
}
- todo_domain.dart
Firebaseの操作(CRUD処理)や、取得したデータの加工のメソッドを記載したファイルになります。
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import '../entity/todo_entity.dart';
class TodoDomain extends ChangeNotifier {
String todo;
List<TodoEntity> todos = [];
List<TodoEntity> incompleteTodos = [];
List<TodoEntity> completeTodos = [];
// todosコレクション
final todosCollection = FirebaseFirestore.instance.collection('todos');
// todoの取得
Future getTodos() async {
// 作成日順に保存しているドキュメントを取得
final todos = await todosCollection.orderBy('createdAt').get();
final listTodos = todos.docs
.map(
(doc) => TodoEntity(doc),
)
.toList();
this.todos = listTodos;
notifyListeners();
}
// todoの追加
Future addTodo() async {
if (todo.isEmpty) {
return;
}
await todosCollection.add(
{
'text': todo,
'isCompleted': false,
'createdAt': DateTime.now(),
'updatedAt': DateTime.now()
},
);
todo = null;
this.getTodos();
}
// textの更新
Future updateTodoText({String id, String text}) async {
if (todo.isEmpty) {
return;
}
await todosCollection.doc(id).update(
{
'text': text,
'updatedAt': DateTime.now(),
},
);
todo = null;
this.getTodos();
}
// todoの削除
Future deleteTodo({String id}) async {
await todosCollection.doc(id).delete();
this.getTodos();
}
// todoを完了状態に更新
Future completeTodo({String id}) async {
await todosCollection.doc(id).update(
{'isCompleted': true, 'updatedAt': DateTime.now()},
);
this.getTodos();
}
// 未完了のtodoの取得
Future getIncompleteTodos() async {
final incompleteTodos =
await todosCollection.where('isCompleted', isEqualTo: false).get();
final todoLists = incompleteTodos.docs
.map(
(todo) => TodoEntity(todo),
)
.toList();
this.incompleteTodos = todoLists;
notifyListeners();
}
// 完了済みのtodoの取得
Future getCompleteTodos() async {
final completeTodos =
await todosCollection.where('isCompleted', isEqualTo: true).get();
final todoLists = completeTodos.docs
.map(
(todo) => TodoEntity(todo),
)
.toList();
this.completeTodos = todoLists;
notifyListeners();
}
// 完了済みか識別できる色の取得
// ignore: missing_return
Color getCompletedColor(bool isCompleted) {
if (isCompleted) {
return Colors.amber;
}
}
// formatした登録日付とtodo内容を取得
String getText({DateTime date, String text}) {
return "${date.year}年 ${date.month}月${date.day}日 ${date.hour}時${date.minute}分\n$text";
}
}
6. Firebaseを使用してみての感想
CRUD処理は公式ドキュメントをみながら出来たのでそこまで大きく躓くことはなかったです!
Firestoreの公式ドキュメント
ただ、いつものsql書く感じで、orderBy()
とwhere()
を同時に使用できるんじゃないかなと思いメソッドチェーンしてみたんですが、PlatformException
が発生してしまったので、同時には無理なのかあ
また触って色々試してみます。
間違えてる箇所等ご指摘ありましたらコメントお願い致します!