Help us understand the problem. What is going on with this article?

【Dart/Flutter】導入したBLoCパターンアーキテクチャについて全体像をまとめてみた

※BLoCパターンについては徐々に改善を進めておりまして、記事を更新する可能性があります。

Dart/Flutterの開発環境

  • Flutter 1.9.1+hotfix.6
  • Dart 2.5.0

FlutterのBLoCパターンとは

BLoCパターンは、Business Logic Componentの略ですね。

ビジネスロジックを1つのコンポーネントとして管理することで、UIと明確に責務を分割するために利用するアーキテクチャであると私は解釈しています。

BLoCパターンについては、下の動画でみっちり解説されているので参考にしましょう。
参考:https://www.youtube.com/watch?v=PLHln7wHgPE

BLoCパターン及びUIのガイドラインについて

スクリーンショット 2019-10-27 23.24.42.png
引用:https://www.youtube.com/watch?v=PLHln7wHgPE

スクリーンショット 2019-10-27 23.25.56.png
引用:https://www.youtube.com/watch?v=PLHln7wHgPE

BLoCパターンのガイドラインは、下の記事にもまとめられているので参考にしましょう。
参考:https://ntaoo.hatenablog.com/entry/2018/10/08/072933

※既に引用してる人がいるため、ガイドラインのスクショだけを貼り付けました。

FlutterでBLoCパターンアーキテクチャを導入した理由

FlutterでBLoCパターンのアーキテクチャを導入した理由としては下記です。

  • 単純にファイルごとの責務分割をしてデータの流れを一方通行にしたかったため
  • 楽にリビルドを最小限に抑える実装にしたかったため
  • 状態を一箇所にまとめることでコードの見通しがよくしたかったため

Flutterで開発している時に、1ファイルで結構なんでも出来てしまっていました。

それこそ、サーバーへリクエストして、レスポンスのJsonをデコードしてモデルにオブジェクトをマッピングして、マッピングされたデータを取り出して、UIに描画するみたいなことが、1ファイルで出来てしまったのです。

ファイルがきちんと責務分割されていない上に、色んな方向にデータが飛び交っていたことから、開発メンバーが機能追加したら原因不明のバグ(解析にアホほど時間がかかるという意味の)が頻繁に起きていました。

また、状態管理について特に何も考えていなかったため、Android StudioのFlutter Performanceを見ると、えげつないくらいリビルドが走っていました。

それと、ページ遷移をする際にNavigator.of(context).push()の引数でオブジェクトを渡しまくっていて、Widgetごとに状態を管理する状況で、状態管理が超絶煩雑になってしまっており、新しいページを追加したり、機能追加するたびに脳内のメモリが持って行かれていました。

とにかく、カオスな状況を打開したくて、BLoCパターンのアーキテクチャを導入しようと思ったわけです。

StatefulWidgetを利用しない方向性で開発する

今ジョインしている案件では、StatefulWidgetで状態管理をゴリゴリやっていたことから、上述したような問題が発生してしまっていました。(単に、上手に扱えていないだけだと思うが、、、)

A widget that does not require mutable state.

引用:https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html

単純にStatelessWidgetを使ってしまえば、Widgetで状態管理することがないので、上述したような問題は発生しなくなります。

責務分割については、StatefulWidgetは関係ないだろうという意見もあるかと思います。(ModelとかAPIのコールとかは分割出来るので)
ただ、状態をWidgetに持っている以上は、setState()とかを使ったりする状況を考慮すると、責務分割について限界があるかなと思いました。
多分、上手い方法が他にもあるんだろうけど、だったら最初からUIとロジックした方が楽だよねという思想から、脳死でBLoCを使えばいいじゃんとなった次第です。

また、StatefulWidgetでは、リビルドを最小限に抑えるみたいなコードの書き方は出来るには出来るのですが、そういうコードの書き方で脳内のメモリを食うのも嫌だな〜と思ったので、BLoCを使って楽にリビルドを最小限に抑えようと思いました。

リビルドを最小限に抑えるコードの書き方については、下記が参考になるかと思います。
参考:https://medium.com/flutter-jp/state-performance-7a5f67d62edd

あと、BLoCパターンだからって、StatefulWidget使えなくもないとは思うのですが、※BLoCパターンのガイドラインに書いてあるから外れる実装を可能にしてしまうなと判断したため、一切StatefulWidgetを使わない方向性で進めるかとなりました。

※上記について、インプットとアウトプットをsink/stream以外にsetState()使えることと、Widgetで状態管理してしまったら異なるプラットフォーム間で状態を使いまわせなくなってしまうことが、BLoCパターンのガイドラインに外れてしまうことと解釈しました。

Flutterで導入したBLoCパターンの全体像

導線がやや長くなってしまいましたが、導入したBLoCパターンについて本題に入ろうと思います。

実装したBLoCパターンを文章だけで説明すると、かなり複雑になってしまうのでポンチ絵を用意しました。

BLoCパターンの全体像について.001.jpeg

ちなみに、BLoCパターンのアーキテクチャについては、下の記事をめっちゃ参考にしました。
参考:https://note.mu/yamarkz/n/n7f9106e53179

めちゃくちゃ参考にこそしましたが、自分たちが開発しやすい方向性で設計しているので、当然ながら全く一緒なアーキテクチャにはなっていません。

ディレクトリ構成

導入したBLoCパターンのディレクトリ構成を共有します。

なんらかの記事詳細ページを取得するというアプリを作るという前提で、ディレクトリを構成してサンプルのソースコードを書いています。

├── lib
│   ├── app
│   │   ├── blocs
│   │   │   └── article_bloc.dart
│   │   ├── repository
│   │   │   └── article_repository.dart
│   │   ├── resources
│   │   │   ├── api
│   │   │   │   └── article_api.dart
│   │   │   └── models
│   │   │       └── article.dart
│   │   └── widgets
│   │       ├── components
│   │       │   ├── atoms
│   │       │   ├── molecules
│   │       │   ├── organisms
│   │       │   │   └── article_organisms.dart
│   │       │   └── templates
│   │       │       └── article_template.dart
│   │       └── screens
│── │──           └── article_screen.dart

※BLoCに関するディレクトリ以外は省略しています。

BLoCパターンの全体像を見た人からすると、Repository層が別ディレクトリに切り分けられていることについては疑問に感じる人もいるかもしれません。
僕個人としても、Repository層Resource層に内包されてもいいかなとは思いました。
ただ、APIコールに必要なオブジェクトを定義したり、ModelへレスポンスのJsonをマッピングさせるための関数を発火させたりと、割と色んなことをやっています。
仮に、Repository層が肥大化した場合に、内部でディレクトリを分割することを想定して、ディレクトリをResource層から分けました。
ただ、現状だとそこまで肥大化しておらず、分けたとしてもひたすら冗長になるかなと思って、Repository層の中身はディレクトリで分けられていません。

導入したBLoCパターンの処理の流れ

下記が実装したおおまかな処理の流れです。

1. ProviderのWidget(この場合はArticleBloc型)が起動
2. builderにBLoCのオブジェクトを渡す
3. 渡したBLoCのオブジェクトのコンストラクタ起動
4. コンストラクタの中の関数がRepositoryの関数を動かす
5. RepositoryからAPI Providerに必要なオブジェクトを引数に渡す
6. API Providerからサーバーへコールする(リクエスト)
7. Repositoryに返ってきたJson形式のレスポンスをModelでパースしてオブジェクトにマッピング
8. BLoCのStreamにレスポンスをadd
9. Screen層から受け取った状態をComponent層でProvider.ofを使って状態を受け取る

これから、それぞれのレイヤーについて、実装したソースコードを載せていきます。

※1ソースコードやパッケージについての細かい解説は別の記事でやります。
※2GETリクエストだけの実装だけをまとめたので、StreamにaddしてPOSTするような処理は別の記事でやります。

UI層

UI層については、Screen層Component層で分けています。

それぞれの責務は下記の通りに分けました。

- Screen層では、BLoC層で管理されている状態をProviderで受け取って、下位Widget(Component層)で扱えるようにする
- Component層では、Screen層からProvider.ofで状態を受け取ってUIを描画する

Component層については、ゆるくAtomic Designを採用しています。

ゆるくというのは、Organisms(生体)層で止めているということです。

そこまで、肥大化していないアプリでは、使いまわさないコンポーネントを、Molecules(分子)レベルやAtoms(原子)レベルで作ったとしても、ただ冗長になるかなと思ったからですね。

とはいえ、Componentとして分割しないことには、FlutterではWidgetのネストがひたすら深くなってしまうのも見通しが悪くなるなと思ったため、Organisms層で止めているというワケです。

今後、アプリが肥大化した場合に、Molecules層Atoms層までのComponentが発生するかと思ってます。

ちなみに、Atomic Designについては下の記事を参考にしました。
参考:https://design.dena.com/design/atomic-design-を分かったつもりになる/

Screen層

article_screen.dart
class ArticleScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider<ArticleBloc>(
      builder: (context) => ArticleBloc(),
      child: ArticleTemplate(),
      dispose: (context, value) => value.dispose(),
    );
  }
}

Screen層は、Providerを使ってBLoC層のオブジェクトを受け取って

見た目となる部分は、Component層にどんどん書いていきます。

Providerの詳しい挙動については下の記事にまとめました。

参考:https://qiita.com/arthur_foreign/private/fde6164b707840b1d4d5

Component層

article_template.dart
class ArticleTemplate extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final ArticleBloc _bloc = Provider.of<ArticleBloc>(context);
    return Scaffold(
      body: Container(
        child: StreamBuilder(
          stream: _bloc.articleStream,
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              return Container(
                child: Column(
                  children: <Widget>[
                    // ここで、Organisms層のComponentを呼び出す
                    // snapshot.data.xxxxのような形で受け取ったオブジェクトを描画
                  ]
                ),
              );
            } else {
              return Container();
            }
          },
        ),
      )
    );
  }
}

Screen層から受け取ったオブジェクトを、Provider.ofを使って受け取りましょう。

StreamBuilder()を使えば、Streamから受け取ったオブジェクトを描画出来るかと思います。

BLoC層

BLoC層を扱う前に、Streamの概念を理解しておく必要があります。

以下の記事にStreamについてまとめたので、参考にしていただけると幸いです。

参考:https://qiita.com/arthur_foreign/items/4d85423e9307512237da

今回導入したBLoCパターンだと、Streamは以下の図のような形で扱ってます。

BLoCパターンのStream図.001.jpeg

※上記の図については、別記事で詳しく解説します。

BLoCパターンのStream図(API).001.jpeg

要するに、Streamで状態を一元管理してるので、生じた変更差分もAPIコールして受け取ったレスポンスも全部ぶん投げれるというワケです。

また、BLoC層の具体的なソースコードは、RxDartを利用しています。

RxDartについて詳しく知りたい人は、下に実装と実行結果とイメージ図を載せた記事をまとめてるので、参考にしていただけると幸いです。

参考:https://qiita.com/arthur_foreign/items/a10d3d4e303b2f77e87d

article_bloc.dart
class ArticleBloc {

  // ObserverとStreamを継承したSubjectを定義
  final _articleInitialPublishSubject = PublishSubject<Article>();

  ArticleBloc() {
    fetchArticle();
  }

  // Streamは連続したObserverの配列みたいなものを定義
  Stream<Article> get articleInitialStream => _articleInitialPublishSubject.stream;

  // StreamにObserverをaddするためのアクセッサを定義
  Sink<Article> get articleInitialSink => _articleInitialPublishSubject.sink;

  // Repository層を経由してRequest
  void fetchArticle() async {
    final _articleResponse = await ArticleRepository().fetchArticleRepository();
    articleInitialSink.add(_articleResponse);
  }

  void dispose() {
    _articleInitialPublishSubject.close();
  }
}

Streamに対して、APIコールして返ってきたレスポンスをaddするといった実装になっています。

また、rxdartについては下の記事を参考にしました。
参考1:https://qiita.com/sensuikan1973/items/64f1a6235bd8ecaf9067
参考2:https://qiita.com/tetsufe/items/521014ddc59f8d1df581

Resource層

Resource層は、サーバーに対してAPIコールしてレスポンスしたデータを扱います。

Repository層Model層API Provider層という責務で切り分けました。

- Repository層は、BLoC層から呼ばれて、API Provider層とModel層とやり取りをします。また、レスポンスのJsonをデコードしたり、API Providerに必要なオブジェクト(エンドポイントのURLやリクエストのHeaderやBody)を定義します。
- Model層は、デコードされたJsonをオブジェクトにマッピングします。
- API Provider層は、サーバーに対してリクエストを送ります。

これから、実装したソースコードを載せていきます。

Repository層

article_repository.dart
class ArticleRepository {
  fetchArticleRepository() async {

    final String _requestArticleUrl = 'https://arthur-foreign.com/articles/1';

    final _articleResponse = await ArticleApiProvider().fetchArticleApi(_requestArticleUrl);

    if (_articleResponse.statusCode == 200) {
      final _decodedArticleResponse = await json.decode(_articleResponse.body);
      return Article.fromJson(_decodedArticleResponse);
    } else {
      throw Exception("Error");
    }
  }
}

BLoC層から呼ばれて、API Provider層Model層とやり取りします。

今は、エンドポイントのURLしか定義してませんが、リクエストヘッダーやボディもここで定義する想定です。

※実際に存在しないサーバーのエンドポイントなので注意しましょう。

Model層

article.dart
class Article {
  final String id;
  final String title;
  final String description;
  final String body;

  Article({this.id, this.title, this.description, this.body});

  factory Article.fromJson(Map<String, dynamic> json) {
    return Article(
      id: json['id'],
      title: json['title'],
      description: json['description'],
      body: json['body'],
    );
  }
}

Repository層デコードされたJsonを引数に渡して、オブジェクトにマッピングしましょう。

すると、いちいちサーバーに対してAPIコールせずに、インスタンスのやり取りだけで済みます。

API Provider層

article_api_provider.dart
class ArticleApiProvider {
  fetchArticleApi(String _requestArticleUrl) async {
    final _response = await http.get(_requestArticleUrl);
    return _response;
  }
}

API Provider層は、サーバーに対してリクエストして、レスポンスをRepository層に返してあげるだけにしてます。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした