BLoCとSQLite

# BLoCとSQLite

Flutter開発環境構築の後、何から始めようかと考えた結果、ローカルDBでデータを保持できるようにしようと思ったので、BLoCを使ってDatabaseとのやりとりしようと思い立ったので、まずは簡単なメモアプリを作ってみる。（主流はFirebaseなんだろうけど、できることから一つずつということで…）

## BLoCとSQLiteの準備
BLoCおよびSQLiteに必要なライブラリを導入。

``` yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^2.1.1
  equatable: ^1.0.0
  sqflite: ^1.2.0
  path_provider: ^1.5.1
```



今回DBに登録するメモのオブジェクトを定義。

（オブジェクトはシンプルにIDとメモ内容のみ）

``` dart
class Memo {
  final int id;
  final String memo;

  Memo({this.id, this.memo})
      : assert(memo != null);

  factory Memo.fromDatabaseJson(Map<String, dynamic> data) =>
      Memo(id: data['id'], memo: data['memo']);

  Map<String, dynamic> toDatabaseJson() => {
        'id': this.id,
        'memo': this.memo,
      };
}
```



## メモ一覧のリスト表示機能の実装

ListViewでメモの一覧を画面に表示し、その画面上でメモの作成、登録、削除をする画面を今回のサンプルとする。



### メモ一覧画面のイベント定義

メモ一覧画面のイベントを定義。

```dart
import 'package:equatable/equatable.dart';
import 'package:flutter/cupertino.dart';
import 'package:bloc_database_sample/models/memo.dart';

abstract class MemoListEvent extends Equatable {
  MemoListEvent([List props = const []]) : super();

  @override
  List<Object> get props => [];
}

class MemoListLoad extends MemoListEvent {
  @override
  String toString() => 'MemoListLoad';
}

class AddMemo extends MemoListEvent {
  final Memo memo;

  AddMemo({@required this.memo}) : assert(memo != null);

  @override
  String toString() => 'AddMemo';
}

class UpdateMemo extends MemoListEvent {
  final Memo memo;

  UpdateMemo({@required this.memo}) : assert(memo != null);

  @override
  String toString() => 'UpdateMemo';
}

class DeleteMemo extends MemoListEvent {
  final int id;

  DeleteMemo({@required this.id}) : assert(id != null);

  @override
  String toString() => 'DeleteMemo';
}

class DeleteAllMemo extends MemoListEvent {
  @override
  String toString() => 'DeleteMemo';
}

```
### メモ一覧イベントに対する状態定義

各イベント発生後の結果の状態を定義。

```dart
import 'package:equatable/equatable.dart';
import 'package:flutter/cupertino.dart';
import 'package:bloc_database_sample/models/memo.dart';

abstract class MemoListState extends Equatable {
  MemoListState([List props = const []]) : super();

  @override
  List<Object> get props => [];
}

class MemoListEmpty extends MemoListState {
  @override
  String toString() => 'MemoListEmpty';
}

class MemoListInProgress extends MemoListState {
  @override
  String toString() => 'MemoListInProgress';
}

class MemoListSuccess extends MemoListState {
  final Stream<List<Memo>> memoList;

  MemoListSuccess({@required this.memoList})
      : assert(memoList != null),
        super([memoList]);

  @override
  String toString() => 'MemoListSuccess';
}

class MemoListFailure extends MemoListState {
  final Error error;

  MemoListFailure({@required this.error})
      : assert(error != null),
        super([error]);

  @override
  String toString() => 'MemoListFailure';
}
```



### BLoCの実装

BLoCクラスには初期状態を表すプロパティと指定されたイベントから新たな状態を作り出すメソッド`mapEventToState`を実装する必要がある。

```dart
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:bloc_database_sample/models/memo.dart';
import 'package:bloc_database_sample/blocs/memo_list_event.dart';
import 'package:bloc_database_sample/blocs/memo_list_repository.dart';
import 'package:bloc_database_sample/blocs/memo_list_state.dart';

class MemoListBloc extends Bloc<MemoListEvent, MemoListState> {
  final MemoListRepository _memoListRepository;

  MemoListBloc({@required MemoListRepository memoListRepository})
      : assert(memoListRepository != null),
        _memoListRepository = memoListRepository;

  @override
  MemoListState get initialState => MemoListEmpty();

  Stream<MemoListState> mapEventToState(MemoListEvent event) async* {
    if (event is MemoListLoad) {
      yield* _mapMemoLoadToState();
    } else if (event is AddMemo) {
      yield* _mapAddMemoToState(event.memo);
    } else if (event is UpdateMemo) {
      yield* _mapUpdateMemoToState(event.memo);
    } else if (event is DeleteMemo) {
      yield* _mapDeleteMemoToState(event.id);
    } else if (event is DeleteAllMemo) {
      yield* _mapDeleteAllMemoToState();
    }
  }

  Stream<MemoListState> _mapMemoLoadToState() async* {
    yield MemoListInProgress();
    try {
      yield MemoListSuccess(memoList: _memoListRepository.getAllMemos());
    } catch (_) {
      yield MemoListFailure(error: _);
    }
  }

  Stream<MemoListState> _mapAddMemoToState(Memo memo) async* {
    yield MemoListInProgress();
    try {
      yield MemoListSuccess(
          memoList: _memoListRepository.insertMemo(memo: memo));
    } catch (_) {
      yield MemoListFailure(error: _);
    }
  }

  Stream<MemoListState> _mapUpdateMemoToState(Memo memo) async* {
    yield MemoListInProgress();
    try {
      yield MemoListSuccess(
          memoList: _memoListRepository.updateMemo(memo: memo));
    } catch (_) {
      yield MemoListFailure(error: _);
    }
  }

  Stream<MemoListState> _mapDeleteMemoToState(int id) async* {
    yield MemoListInProgress();
    try {
      yield MemoListSuccess(memoList: _memoListRepository.deleteMemoById(id));
    } catch (_) {
      yield MemoListFailure(error: _);
    }
  }

  Stream<MemoListState> _mapDeleteAllMemoToState() async* {
    yield MemoListInProgress();
    try {
      yield MemoListSuccess(memoList: _memoListRepository.deleteAllMemos());
    } catch (_) {
      yield MemoListFailure(error: _);
    }
  }
}
```

イベントの種類が増えた場合、`mapEventToState`に新たなイベントを定義して対応するのがよいそう。



### Database Repositoryの定義

BLoCクラスから各イベント状態に応じてコールされるRepositoryを定義。

```dart
import 'package:bloc_database_sample/models/memo.dart';
import 'package:bloc_database_sample/dao/memo_dao.dart';
import 'package:bloc_database_sample/blocs/memo_list_repository.dart';

class MemoRepository extends MemoListRepository {
  final memoDao = MemoDao();

  MemoRepository();

  Stream<List<Memo>> getAllMemos({String query}) =>
      memoDao.getMemos(query: query);

  Stream<List<Memo>> insertMemo({Memo memo}) {
    memoDao.createMemo(memo);
    return memoDao.getMemos();
  }

  Stream<List<Memo>> updateMemo({Memo memo}) {
    memoDao.updateMemo(memo);
    return memoDao.getMemos();
  }

  Stream<List<Memo>> deleteMemoById(int id) {
    memoDao.deleteMemo(id);
    return memoDao.getMemos();
  }

  Stream<List<Memo>> deleteAllMemos() {
    memoDao.deleteAllMemos();
    return memoDao.getMemos();
  }
}
```

### Data Access Objectの定義

Repositoryクラスからコールされる実際にDatabaseにアクセスするクラスを定義。

```dart
import 'dart:async';
import 'package:bloc_database_sample/database/database.dart';
import 'package:bloc_database_sample/models/memo.dart';

class MemoDao {
  final dbProvider = DatabaseProvider.dbProvider;

  Future<int> createMemo(Memo memo) async {
    final db = await dbProvider.database;
    int result = await db.insert(memoTable, memo.toDatabaseJson());
    return result;
  }

  Stream<List<Memo>> getMemos({List<String> columns, String query}) async* {
    final db = await dbProvider.database;

    List<Map<String, dynamic>> result;

    if (query != null) {
      if (query.isNotEmpty) {
        result = await db.query(memoTable,
            columns: columns, where: 'memo LIKE ?', whereArgs: ['%$query']);
      }
    } else {
      result = await db.query(memoTable, columns: columns);
    }
    List<Memo> memos = result.isNotEmpty
        ? result.map((item) => Memo.fromDatabaseJson(item)).toList()
        : [];
    yield memos;
  }

  Future<int> updateMemo(Memo memo) async {
    final db = await dbProvider.database;

    int result = await db.update(memoTable, memo.toDatabaseJson(),
        where: 'id = ?', whereArgs: [memo.id]);

    return result;
  }

  Future<int> deleteMemo(int id) async {
    final db = await dbProvider.database;
    int result = await db.delete(memoTable, where: 'id = ?', whereArgs: [id]);
    return result;
  }

  Stream<int> deleteAllMemos() async* {
    final db = await dbProvider.database;
    int result = await db.delete(
      memoTable,
    );
    yield result;
  }
}
```

### Databaseクラスの定義

データベースの作成、更新を行うクラスを定義

```dart
import 'dart:async';
import 'dart:io';

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

final memoTable = 'Memo';

class DatabaseProvider {
  static final DatabaseProvider dbProvider = DatabaseProvider();

  Database _database;

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

  createDatabase() async {
    Directory documentsDirectory = await getApplicationDocumentsDirectory();
    String path;
    try {
       path = join(documentsDirectory.path, "MemoData.db");
    } catch(_) {
      print(_.toString());
    }

    var database = await openDatabase(path,
        version: 1, onCreate: initDB, onUpgrade: onUpgrade);

    return database;
  }

  void onUpgrade(Database database, int oldVersion, int newVersion) async{
    if (newVersion > oldVersion) {
      await database.execute("DROP TABLE $memoTable");
    }
  }

  void initDB(Database database, int version) async {
    await database.execute("CREATE TABLE $memoTable ("
        "id INTEGER PRIMARY KEY, "
        "memo TEXT "
        ")");
  }
}
```



## メモ一覧のUI部分の実装

``` dart
import 'package:bloc_database_sample/blocs/memo_list_bloc.dart';
import 'package:flutter/material.dart';
import 'package:bloc_database_sample/ui/home_page.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc_database_sample/blocs/memo_list_repository.dart';
import 'package:bloc_database_sample/repository/memo_repository.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter bloc database demo',
      theme: ThemeData(
          primarySwatch: Colors.indigo, canvasColor: Colors.transparent),
      home: BlocProvider<MemoListBloc>(
          create: (context) =>
              MemoListBloc(memoListRepository: MemoRepository()),
          child: HomePage()),
    );
  }
}
```

```dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc_database_sample/blocs/memo_list_event.dart';
import 'package:bloc_database_sample/blocs/memo_list_state.dart';
import 'package:bloc_database_sample/blocs/memo_list_bloc.dart';
import 'package:bloc_database_sample/models/memo.dart';

class HomePage extends StatefulWidget {
  _HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    BlocProvider.of<MemoListBloc>(context).add(MemoListLoad());
    return Scaffold(
        resizeToAvoidBottomInset: false,
        appBar: AppBar(
          title: Text('Memo List'),
        ),
        body: SafeArea(
          child: Container(
              color: Colors.white,
              padding:
              const EdgeInsets.only(left: 2.0, right: 2.0, bottom: 2.0),
              child: Container(child: getMemosWidget(context))),
        ),
        floatingActionButtonLocation: FloatingActionButtonLocation.endTop,
        floatingActionButton: Padding(
          padding: EdgeInsets.only(bottom: 25),
          child: FloatingActionButton(
            elevation: 5.0,
            onPressed: () {
              _showAddMemoSheet(context);
            },
            backgroundColor: Colors.white,
            child: Icon(
              Icons.add,
              size: 32,
              color: Colors.indigoAccent,
            ),
          ),
        ));
  }

  Widget getMemosWidget(BuildContext context) {
    return BlocBuilder<MemoListBloc, MemoListState>(
      bloc: BlocProvider.of<MemoListBloc>(context),
      builder: (context, state) {
        if (state is MemoListInProgress) {
          return Center(
            child: CircularProgressIndicator(),
          );
        }
        if (state is MemoListSuccess) {
          return StreamBuilder(
            stream: state.memoList,
            builder:
                (BuildContext context, AsyncSnapshot<List<Memo>> snapshot) {
              if (!snapshot.hasData) {
                return Center(child: CircularProgressIndicator());
              }
              if (snapshot.hasError) {
                return Center(
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: <Widget>[Text('Failure')],
                  ),
                );
              }
              return ListView.builder(
                  itemCount: snapshot.data.length,
                  itemBuilder: (BuildContext context, int index) {
                    final memo = snapshot.data[index];
                    final Widget dismissibleCard = new Dismissible(
                        background: Container(
                          child: Padding(
                            padding: EdgeInsets.only(left: 10),
                            child: Align(alignment: Alignment.centerLeft,
                              child: Text("Deleting",
                                style: TextStyle(color: Colors.white),
                              ),
                            ),
                          ),
                          color: Colors.redAccent,
                        ),
                        onDismissed: (direction) {
                          setState(() {
                            snapshot.data.removeAt(index);
                            BlocProvider.of<MemoListBloc>(context).add(DeleteMemo(id: memo.id));
                          });
                        },
                        key: new ObjectKey(memo),
                        child: Card(
                          child: Column(
                            mainAxisSize: MainAxisSize.max,
                            children: <Widget>[
                              ListTile(
                                title: Text(memo.memo,
                                    style: TextStyle(fontWeight: FontWeight.bold)),
                              )
                            ],
                          ),
                        )
                    );
                    return dismissibleCard;
                  });
            },
          );
        }
        if (state is MemoListFailure) {
          return Center(
            child: Text('Failure'),
          );
        }
        return Container();
      },
    );
  }

  void _showAddMemoSheet(BuildContext context) {
    final _memoDescriptionFormController = TextEditingController();
    showModalBottomSheet(
        context: context,
        builder: (builder) {
          return new Padding(
            padding: EdgeInsets.only(
                bottom: MediaQuery
                    .of(context)
                    .viewInsets
                    .bottom),
            child: new Container(
              color: Colors.transparent,
              child: new Container(
                height: 230,
                decoration: new BoxDecoration(
                    color: Colors.white,
                    borderRadius: new BorderRadius.only(
                        topLeft: const Radius.circular(10.0),
                        topRight: const Radius.circular(10.0))),
                child: Padding(
                  padding: EdgeInsets.only(
                      left: 15, top: 25.0, right: 15, bottom: 30),
                  child: ListView(
                    children: <Widget>[
                      Row(
                        mainAxisSize: MainAxisSize.min,
                        crossAxisAlignment: CrossAxisAlignment.center,
                        children: <Widget>[
                          Expanded(
                            child: TextFormField(
                              controller: _memoDescriptionFormController,
                              textInputAction: TextInputAction.newline,
                              maxLines: 4,
                              style: TextStyle(
                                  fontSize: 21, fontWeight: FontWeight.w400),
                              autofocus: true,
                              decoration: const InputDecoration(
                                  hintText: 'take down note...',
                                  labelText: 'New Memo',
                                  labelStyle: TextStyle(
                                      color: Colors.indigoAccent,
                                      fontWeight: FontWeight.w500)),
                              validator: (String value) {
                                if (value.isEmpty) {
                                  return 'Empty memo!';
                                }
                                return value.contains('')
                                    ? 'Do not use the @ char.'
                                    : null;
                              },
                            ),
                          ),
                          Padding(
                            padding: EdgeInsets.only(left: 5, top: 15),
                            child: CircleAvatar(
                              backgroundColor: Colors.indigoAccent,
                              radius: 18,
                              child: IconButton(
                                icon: Icon(
                                  Icons.save,
                                  size: 22,
                                  color: Colors.white,
                                ),
                                onPressed: () {
                                  final newMemo = Memo(
                                      memo: _memoDescriptionFormController
                                          .value.text);
                                  if (newMemo.memo.isNotEmpty) {
                                    BlocProvider.of<MemoListBloc>(context).add(
                                        AddMemo(memo: newMemo));
                                    Navigator.pop(context);
                                  }
                                },
                              ),
                            ),
                          )
                        ],
                      ),
                    ],
                  ),
                ),
              ),
            ),
          );
        });
  }
}
```



BLoCを使うにあたって`BLoC Provider`を使うのがよいらしいんだけど、使い方がよくわからない。BLoCで非同期処理を行うにあたってメモリリークの発生を防ぐためにStreamを閉じる必要があるらしいんだけど、参考にした書籍にはそのあたりには言及されてなかった。たまたま書籍のサンプルが今回と同じようにmainのwidgetのbuildメソッド内でBlocProviderを使用しているケースだから特に意識する必要がないということ・・？



[参考書籍]

> [Flutter モバイルアプリ開発バイブル](https://book.mynavi.jp/ec/products/detail/id=108125)

[参考サイト]

>[Medium](https://medium.com/@vaygeth/reactive-flutter-todo-app-using-bloc-design-pattern-b71e2434f692)