Flutterでsqliteを使ったTodoアプリを作る

# 参考にしたサイト
sqliteを使ったListViewの作り方がコードつきで解説されています。
sqliteだけでなくBLoCパターンを使った実装の勉強にもなりました。

[Using SQLite in Flutter - Flutter Community - Medium](https://medium.com/flutter-community/using-sqlite-in-flutter-187c1a82e8b)

# ソースコード
## sqliteなしバージョン
https://github.com/popy1017/flutter-simple-todo-app

sqliteなしのTodoアプリを作ったときのメモはこちら
https://qiita.com/popy1017/items/6b2c95d3c03b35ae97e0

今回は、上記のコードをベースにsqliteを使う部分を追加していきました。

## sqlite使用バージョン(この記事のアウトプット)
https://github.com/popy1017/flutter-sqlite-todo-app

# 必要プラグインをインストール
##### pubspec.yaml

```yaml
dependencies:
  〜〜〜
  
  sqflite: ^1.2.0
  path_provider: ^1.6.0
  
  〜〜〜
```
sqflite: sqliteをflutterで使うためのプラグイン
path_provider: 保存場所のパスを見つけるためのプラグイン

# db_provider.dart を作る
##### db_provider.dart

```dart
class DBProvider {
  // privateなコンストラクタ
  DBProvider._();
  static final DBProvider db = DBProvider._();
}
```
## DBを取得する関数を追加
```dart
// db_provider.dart

import 'dart:io';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite/sqlite_api.dart';

class DBProvider {
  DBProvider._();
  static final DBProvider db = DBProvider._();

  static Database _database;

  Future<Database> get database async {
    if (_database != null)
      return _database;

    // DBがなかったら作る
    _database = await initDB();
    return _database;
  }

  Future<Database> initDB() async {
    Directory documentsDirectory = await getApplicationDocumentsDirectory();

    // import 'package:path/path.dart'; が必要
    // なぜか サジェスチョンが出てこない
    String path = join(documentsDirectory.path, "TodoDB.db");
    
    return await openDatabase(path, version: 1, onCreate: _createTable);
  }

  Future<void> _createTable(Database db, int version) async {
    return await db.execute(
      "CREATE TABLE Todo ("
      "id TEXT PRIMARY KEY,"
      "title TEXT,"
      "dueDate TEXT,"
      "note TEXT"
      ")"
    );
  }
}
```
### Futureとasync/await
参考サイト
https://qiita.com/akatsukaha/items/f7c4fa9746e69f9b458d

JavaScriptで言うと、Future → Promise、async/awaitはそのまま。
非同期的な処理を同期的に扱うための書き方。
非同期的な処理としては、ファイル読み込みやDB操作、HTTP通信等がある。

## モデルを作る
```dart
// todo.dart

class Todo {
  String id;
  String title;
  DateTime dueDate;
  String note;

  Todo({this.id, @required this.title, @required this.dueDate, @required this.note});
  Todo.newTodo() {
    title = "";
    dueDate = DateTime.now();
    note = "";
  }

  assignUUID() {
    id = Uuid().v4();
  }

  factory Todo.fromMap(Map<String, dynamic> json) => Todo(
    id: json["id"],
    title: json["title"],
    // DateTime型は文字列で保存されているため、DateTime型に変換し直す
    dueDate: DateTime.parse(json["dueDate"]).toLocal(), 
    note: json["note"]
  );

  Map<String, dynamic> toMap() => {
    "id": id,
    "title": title,
    // sqliteではDate型は直接保存できないため、文字列形式で保存する
    "dueDate": dueDate.toUtc().toIso8601String(),
    "note": note
  };
}
```
### sqliteで使用できる型
https://pub.dev/packages/sqflite
Integer、Real、Text、Blobの4つ。
**DateTimeとかBoolはない！！！**
**DateTimeとかBoolはない！！！(2回目)**

>DateTime is not a supported SQLite type. Personally I store them as int (millisSinceEpoch) or string (iso8601)
bool is not a supported SQLite type. Use INTEGER and 0 and 1 values.

Bool型がないのに気づくのに1〜2時間かかったので強調(←ちゃんとドキュメント読んでから使えばよかった。。。)

#### じゃあDateTimeとかBool保存したいときどうすんのって話
sqliteに保存するときは、サポートしている型に変換。
sqliteからデータを取得するときは、Dartの型に変換。
DateTime型と文字列の相互変換はタイムゾーンとか気にしないといけないので少し面倒。
下記がとても参考になりました。
https://qiita.com/TomK/items/132831ab45e2aba822a8
https://medium.com/flutter-jp/datetime-17e618c8e26e

### Map型
JavaScriptで言う"key":"value"形式の連想配列的な感じ？？
今回使うsqfliteのメソッドが`Map<String, dynamic>`を引数にとってる。

### factory って何？
詳しくはどんな意味があるのかわかりませんでしたが、なんとなくstaticなコンストラクタのような感じ。
ファクトリーパターンなるものを簡単に実装するためのものらしい。
https://qiita.com/shoheiyokoyama/items/d752834a6a2e208b90ca
>Factoryパターン
>Factoryクラスがオブジェクトの生成処理に加えて生成するオブ>ジェクトの種類の変更をファクトリの処理の中で動的に行う

## CRUD 関数を作る
```dart
// db_provider.dart

class DBProvider {
  〜〜〜
  static final _tableName = "Todo";

  createTodo(Todo todo) async {
    final db = await database;
    var res = await db.insert(_tableName, todo.toMap());
    return res;
  }

  getAllTodos() async {
    final db = await database;
    var res = await db.query(_tableName);
    List<Todo> list =
        res.isNotEmpty ? res.map((c) => Todo.fromMap(c)).toList() : [];
    return list;
  }

  updateTodo(Todo todo) async {
    final db = await database;
    var res  = await db.update(
      _tableName, 
      todo.toMap(),
      where: "id = ?",
      whereArgs: [todo.id]  
    );
    return res;
  }

  deleteTodo(String id) async {
    final db = await database;
    var res = db.delete(
      _tableName,
      where: "id = ?",
      whereArgs: [id]
    );
    return res;
  }
}
```
## BLoCクラスをDBProviderを使うように修正する
```dart
// todo_bloc.dart

class TodoBloc {

  final _todoController = StreamController<List<Todo>>();
  Stream<List<Todo>> get todoStream => _todoController.stream;

  getTodos() async {
    _todoController.sink.add(await DBProvider.db.getAllTodos());
  }

  TodoBloc() {
    getTodos();
  }

  dispose() {
    _todoController.close();
  }

  create(Todo todo) {
    todo.assignUUID();
    DBProvider.db.createTodo(todo);
    getTodos();
  }

  update(Todo todo) {
    DBProvider.db.updateTodo(todo);
    getTodos();
  }

  delete(String id) {
    DBProvider.db.deleteTodo(id);
    getTodos();
  }
}
```