63
58

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-02-07

はじめに

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

flutter_todo_app.mp4.gif

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

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

ソースとバージョン

  • Flutter 1.15.2-pre.7
  • Dart 2.8.0

全体アーキテクチャ

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

flutter_architecture.png

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

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

ディレクトリ構成

image.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パッケージを使って書き換えたものを見てみると良いと思います。

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

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

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

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

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

Blocってなに?

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

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

Modelについて

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

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の3つのListがありますが、_allTodoList配列をfilterする形でそれぞれのリストを作っています。余計なDBアクセスを防ぐためです。

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から呼び出せるインターフェースです。ここではシンプルにつないでいるだけになります。

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というパッケージをつかっています。
ここはsqliteへの手続きを書いています。

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

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を書き始めてまだ数週間ですが、非常にいい感じです。何が良いって、以下の3つの理由で開発スピードが圧倒的に高まります。

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

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

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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
63
58

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?