Flutterのpackage:providerを使ったBloc的アーキテクチャ全体像をサンプルで理解するまとめ

#はじめに

Flutterを実践的に理解するために、2020年2月現在でだいたいこんな感じのアーキテクチャで作っていけばいいだろう、というサンプルアプリを作りました。以下のようなTodoアプリです。

![flutter_todo_app.mp4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24308/9a24b82d-cae6-7bd2-9e80-e5eab212ffc4.gif)

仕様は、Todoが追加されるとローカルDBに保持され、未完了と完了のチェックをつけることが出来ます。画面はAll、Incompleted、Completedの３タブに分かれてそれぞれのリストが表示されます。ゴミ箱マークからデータの削除が出来ます。

本投稿は、これまでiOSやAndroidの開発の経験はあるが、これからFlutterを触っていこうと思っている方に向けて書いております。全体像を掴むきっかけになれば嬉しいです。

# ソースとバージョン

https://github.com/nishiyamaosamu/flutter_todo_app_sample


- Flutter 1.15.2-pre.7
- Dart 2.8.0

# 全体アーキテクチャ

まずは全体アーキテクチャを見ていきましょう。

![flutter_architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24308/69fe7489-7646-a44b-a5b3-d4235b93e383.png)


シンプルなMVVMアーキテクチャです。GoogleがオススメしているProviderパッケージを利用して、ビュー(UI)とロジックやステート(Model)を適切に分離することが最も重要なポイントです。まずは全体像を説明し、詳細は後述していきます。

- UI・・・Stateless Widgetを中心としたViewで構成されます。
- Model・・・BlocやViewModelと呼ばれる層で、ロジックとステート（状態）を持ちます。
- Repository・・・Modelが外部リソースとやりとりする、その接続点としてRepositoryをインターフェースとして扱います。
- DAOやAPI・・・外部リソースへのアクセスを行います。（本アプリではAPIは扱っていません）

# ディレクトリ構成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24308/a3a112dd-b78f-55e6-8b59-c70168a7c243.png)


アーキテクチャに加えて、以下のディレクトリも切っています。

- Entity・・・アプリケーション内で扱われるオブジェクト群です。APIやDBなど外部リソースから取得したJSONなどのデータを、適切なオブジェクトへと変換します。

- Service・・・本アプリではsqliteデータベースの初期化するクラスを持ちます。ここは少し曖昧な命名としています。

これらの命名は絶対的なものではないので、参考にしつつもアプリ特性やチームの共通言語を中心に設計していきましょう。

# UIについて

本アプリではStateless Widgetを中心にUIが構成されています。なぜStatelessなのかというと、その方がよりViewとしての役割に集中するように制約が生まれ、ロジックやステートを分離することが出来るからです。

#### StatefulWidgetとStatelessWidgetの違い

StatefulWidgetというのはこれまでのiOSのViewControllerやAndroidのActivityと似ているものです。Viewを制御しつつ、そこで使われる値（ステート）を管理することも出来るものです。
一方StatelessWidgetはiOSのStoryboardやAndroidのXMLに近いものと考えるとわかりやすいかもしれません。プログラマブルにテンプレートを構成するためのクラスになります。

#### StatelessWidgetの値を変化させたい場合どうするの？

StatelessWidgetが値を持たない代わりに、Providerというパッケージを活用していきます。Providerは主に状態管理とDI（依存性注入)を担ってくれるものです。Googleが推奨しているパッケージなので、安心して利用することができそうです。
（DIの説明は長くなるのでここでは割愛します）

# Providerパッケージについて

まずProviderの使われ方を理解するのに、`flutter create`すると生まれるCounterアプリをProviderパッケージを使って書き換えたものを見てみると良いと思います。

こちらの記事が分かりやすかったので紹介しておきます。

- [【Flutter】provider で状態管理するサンプルを作ったので紹介【初心者向け】](https://gurutaka-log.com/flutter-provider-sample)

Providerのざっくりポイントとなるのは以下の4つです。

- ChangeNotifierのModelを作って、ロジックと値（ステート）を作る
- ChangeNotifierProviderで、ModelをWidgetで使えるように内包する
- Provider.ofを使ってModelを呼び出し、値（ステート）をViewに使う
- Model内のnotifyListeners()で値（ステート）の変更を通知する

Providerを活用することでシンプルにUIとロジックとステートを分離することができ、リアクティブなアプリを作って行けそうな感じがしてきます。

Providerの詳細は↓の諸先輩方の記事が理解を深めてくれるのでおすすめです。

- [[Flutter] package:provider の各プロバイダの詳細](https://qiita.com/kabochapo/items/a90d8438243c27e2f6d9)
- [InheritedWidget を完全に理解する 🎯(Flutterフレームワーク・providerパッケージを支える重要なWidget)](https://medium.com/flutter-jp/inherited-widget-37495200d965)

### Blocってなに？

「Flutter アーキテクチャ」などで検索するとBlocという単語がめっちゃ出てきます。BlocとはBusiness Logic Componentの略で実装パターンのことです。ざっくりいうとUIからロジック分離した部品を作ろうぜってことです。Googleが提唱したBlocパターンはStreamを使うという方針があるので、今回はBloc的アーキテクチャと呼ぶことにしました。2018年にGoogleは「Blocパターンで作ろうぜ」って言ってましたが、2019年になって「Providerパッケージを使おうぜ」に変わりました。いまはProviderがデファクトスタンダードとなってきています。

Blocの詳しいことは以下の諸先輩方の記事におまかせします。

- [長めだけどたぶんわかりやすいBLoCパターンの解説](https://qiita.com/kabochapo/items/8738223894fb74f952d3)
- [まだ BLoC で消耗してるの？](https://ja.unflf.com/flutter/provider/)

# Modelについて

ここからコードを追いながら見ていきましょう。
まずはTodoModelについて説明していきます。

```dart:todo_mode.dart
class TodoModel with ChangeNotifier{  
  List<Todo> _allTodoList = [];
  List<Todo> get allTodoList => _allTodoList;
  List<Todo> get incompletedTodoList => _allTodoList.where((todo) => todo.isDone == false).toList();
  List<Todo> get completedTodoList => _allTodoList.where((todo) => todo.isDone == true).toList();
...
```

EntityとしてのTodo型をListの配列に詰めたTodoリストの値を保持しているモデルです。
allとincompletedとcompletedの３つのListがありますが、`_allTodoList`配列をfilterする形でそれぞれのリストを作っています。余計なDBアクセスを防ぐためです。



```dart:todo_mode.dart
...
  final TodoRepository repo = TodoRepository();

  TodoModel(){
    _fetchAll();
  }

  void _fetchAll() async {
    _allTodoList = await repo.getAllTodos();
    notifyListeners();
  }

  void add(Todo todo) async {
    await repo.insertTodo(todo);
    _fetchAll();
  }

...
```

外部リソースとやり取りするためのTodoRepositoryを作ります。TodoRepositoryはローカルDBへの接続をしています。
`_fetchAll()`でローカルDBから現在の最新一覧を取ってきます。`_allTodoList`の値が更新されるタイミングで、`notifyListeners()`で各StatelessWidgetに変更が通知され、表示が最新に更新されます。
`TodoModel()`のコンストラクタや、`add`されたあとにも`_fetchAll()`を呼び出して最新データをDBから取り出します。
ここでは常にDBからfetchしていますが、これがAPIの場合にはそのタイミングは様々になると思うので、その場合は適宜調整していくことになります。

# Repositoryについて

RepositoryはModelから呼び出せるインターフェースです。ここではシンプルにつないでいるだけになります。

```dart:todo_repository.dart
class TodoRepository {
  final todoDao = TodoDao();
  Future getAllTodos() => todoDao.getAll();
  Future insertTodo(Todo todo) => todoDao.create(todo);
  Future updateTodo(Todo todo) => todoDao.update(todo);
  Future deleteTodoById(int id) => todoDao.delete(id);
  //not use this sample
  Future deleteAllTodos() => todoDao.deleteAll();
}
```
Futureは非同期処理のクラスです。JavascriptでいうところのPromiseと言えるでしょう。DBへのアクセスは非同期で行われるため、Futureクラスで返されていきます。

# DAO（Data Access Object）

DBにはsqliteを使っていますが、[sqflite](https://pub.dev/packages/sqflite)というパッケージをつかっています。
ここはsqliteへの手続きを書いています。

```dart:todo_dao.dart
class TodoDao {
  final dbProvider = DatabaseService.dbProvider;
  final tableName = DatabaseService.todoTableName;

  Future<int> create(Todo todo) async {
    final db = await dbProvider.database;
    var result = db.insert(tableName, todo.toDatabaseJson());
    return result;
  }

  Future<List<Todo>> getAll() async {
    final db = await dbProvider.database;
    List<Map<String, dynamic>> result = await db.query(tableName);
    List<Todo> todos = result.isNotEmpty
        ? result.map((item) => Todo.fromDatabaseJson(item)).toList()
        : [];
    return todos;
  }
```

`create`では、`todo.toDatabaseJson()`でTodo型からMapに変換しています。逆に`getAll`では`Todo.fromDatabaseJson(item)`でMapからTodo型を生成してListをつくっています。次にTodo型のエンティティの中身を見ていきましょう。

# Entity

```dart:todo.dart
class Todo {
  int id;
  String title;
  bool isDone;

  Todo({this.id, this.title, this.isDone = false});

  factory Todo.fromDatabaseJson(Map<String, dynamic> data) => Todo(
    id: data['id'],
    title: data['title'],
    isDone: data['is_done'] == 1 ? true : false,
  );

  Map<String, dynamic> toDatabaseJson() => {
    "id": this.id,
    "title": this.title,
    "is_done": this.isDone ? 1 : 0,
  };
}
```

このようにFactoryでTodoを作ったり、TodoからJsonとなるMap型を作ったり出来るようにしています。

# おわりに、Flutter所感

Flutterを書き始めてまだ数週間ですが、非常にいい感じです。何が良いって、以下の３つの理由で開発スピードが圧倒的に高まります。

1. クロスプラットフォーム
2. ホットリロード
3. 宣言的UI

1や2は分かっていましたが、3.宣言的UIのおかげでもうAutoLayoutに挙動にイラついたり（iOS）、ListViewのためにわざわざAdapter作ったり（Android)しなくて良くなりました（笑） 代わりにまだまだシガラミ多そうではありますが、トレードオフと考えても2020年以降は積極的にFlutterを採用していくケースが増えてくると思います。

ということで、これからFlutter開発をはじめていきましょう！

##### おまけに、Flutterは公式サンプルがいっぱい

- https://flutter.github.io/samples/#/
- https://github.com/flutter/samples/blob/master/INDEX.md
- https://flutter.dev/docs/cookbook