FlutterでTodoアプリを作成 ~第二話 あなたのする事もう消えません。~

# 1. 前回のあらすじ
第一話では、todoアプリの作成をしました。
まだご覧になられていない方は是非ご覧ください！
[第一話はこちらから](https://qiita.com/tsu_saint/items/dbf7cb61040bbff105df)

# 2. Firebaseの導入経緯
ですが、作成したtodoを直接配列に格納していたのでビルドし直すと、表示されているtodoが画面から消えてしまいます。
なのでFirebaseを導入してFirestoreにデータを保存していきたいと思いました！
導入で参考にしたのは公式ドキュメントになります。


# 3. 完成したアプリ
![todo_app.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/714563/dc83741e-a6ca-c96c-c04d-45dc6590f84e.gif)


# 4. 追加したファイル
domainディレクトリあった``todo_domain.dart``をReNameし、``todo_domain_old.dart``に変更して、
``todo_domain.dart``を追加しました。（配列追加番もなんとなく残したくて。。笑）

追加したファイルにFirebaseの操作（データを取得したり追加したり等）や、取得したデータの加工のメソッドを記載していきました。
ui/todoディレクトリのtest.dartはお気になさらず。。（試したいことがあったので一時的に作成したファイルです。）

![スクリーンショット 2020-10-10 12.35.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/714563/2b5fa3f2-da78-09e8-5e95-f044dbd1f90e.png)

# 5. 修正後の各ファイルの説明
- main.dart 

```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

```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``にメソッドとして用意して呼び出すようにしました。


```list.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

更新日を表示できるように修正しました。

``` complete.dart
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

作成日を表示できるように修正しました。

``` incomplete.dart
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

コンストラクタ用に用意しました。

```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処理）や、取得したデータの加工のメソッドを記載したファイルになります。

```todo_domain.dart
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の公式ドキュメント](https://firebase.flutter.dev/docs/firestore/usage)

ただ、いつものsql書く感じで、``orderBy()``と``where()``を同時に使用できるんじゃないかなと思いメソッドチェーンしてみたんですが、``PlatformException``が発生してしまったので、同時には無理なのかあ
また触って色々試してみます。

間違えてる箇所等ご指摘ありましたらコメントお願い致します！