Flutterの実践導入で用いるBLoC Patternの全体像と押さえておくポイント
SHARES
この記事はnote.comからの転載です。
https://note.com/yamarkz/n/n7f9106e53179
はじめに
こんにちは!プロダクトチームの山口(@yamarkz)です。 最近はFlutterを用いたアプリケーション開発に取り組んでおり、そこで採用しているデザインパターンの1つである BLoC Pattern について、自身が調査した内容を整理し、実践導入する上で押さえておくポイントを紹介していきたいと思います。日本ではプロダクションレベルで採用されている例が少ないので、ぜひ、この記事を参考に検討してみてください。
今回紹介する内容の背景を少しご紹介すると、弊社Housmartでは定期的(3ヶ月に1度など)にLab Weekと題しまして、1週間ほど業務時間を使い、新しい技術や日頃手に届きにくい技術課題の解決に取り組む期間を設けています。 詳細は下記記事リンクを参照。
自分はこのLab Weekで、Flutterを用いた中規模アプリケーション開発に必要な設計周りの調査を行い、中でもBLoC Patternについてキャッチアップを行いました。本記事がこれからFlutterとBLoC Patternを用いてアプリケーション開発を行う方の役に立てれば嬉しいです。
紹介
本記事で紹介したBLoC Patternの内容を踏まえて、BLoC Patternを用いたFlutter製のアプリケーションをリリースしました! ご興味あれば、こちらの記事も読んでみてください😄
紹介 その2
Bloc Patternの応用実装をテーマとした記事を書きました。 本記事で紹介している内容を含んだ実装コードも公開しているので、こちらも合わせて読んでみてください。
本記事の対象読者
本記事のゴール
目次
先に全体感を掴んだ後に、実践導入する上でのポイントを紹介します!
FlutterとBLoC Pattern
FlutterとBLoC Patternについては既に多くの紹介記事が存在するため、ここでは詳細な内容にまで踏み込みませんが、簡単に概要だけ。
Flutter
Flutterは2018年12月に正式リリースされたクロスプラットフォーム(iOS/Android)アプリケーション開発フレームワークです。これはGoogleが本腰を入れて開発に着手しており、現在じわじわと勢いをつけ注目を集めてきています。類似のクロスプラットフォームのフレームワークにはXamarinやReact Nativeなどがあり、Flutterはこれらと比較されて紹介されることが多いです。詳細な解説については公式のドキュメントを参照してみてください。
・ Flutter - Beautiful native apps in record time
BLoC Pattern
BLoC PatternはFlutterでのアプリケーション開発時に用いる、状態管理手法の1つです。巷ではReduxやScoped Modelと肩を並べて紹介されることが多く、有用な状態管理手法の1つとしての地位が確立されています。 BLoC Patternという名称のBLoC というのはB usiness Lo gic C omponentの頭文字を取った略称で、その名の通りビジネスロジックをコンポーネント単位で管理しやすくするためのパターンです。こちらも既に多くの有益な紹介記事などが出ているので、詳細な説明はそちらを参考にしてみてください。
- ・ 長めだけどたぶんわかりやすいBLoCパターンの解説
- ・ BLoCパターンとはなにか - FlutterとAngularの間でModelのコードを再利用する実践を通じての考察
- ・ FlutterのBLoCパターンについて得た知見
(補足) 本記事で紹介するBLoC Patternの内容はネイティブアプリにのみ適用する場合の話であり、厳密なBLoC Pattern (ex: Webとネイティブのロジック再利用など)の紹介ではありません。なので、わかりやすさを重視するため本来の文脈とは異なる表現になる可能性があります。正確な内容は公式ドキュメントを読むことをオススメします。URL
BLoC Patternを用いた全体像
BLoC Patternを用いた場合のFlutter Appの全体像を概念として表すと、以下の様になります。
初見で見た場合は何がどうなっているのかパッと見わかり辛いかもしれません。既にBLoC Patternを利用したことがある方からすると、なんとなく図の雰囲気に共感していただけるのではないでしょうか。 後ほどこの図の詳細を辿って全体感を掴んでいくのですが、もっとざっくりとレイヤーを分けて全体を見てみたいと思います。
3つのレイヤー
BLoC Patternを利用してアプリケーションを構築する場合、アプリケーションの構造は大きく3つのレイヤーに分けることができます。
1. Widget tree 2. BLoC 3. Resource
Widget Tree (Screen / Component) FlutterではUIをWidgetと呼ばれるもので構成します。 「FlutterのUIは全てWidgetで〜」というのがFlutterの謳い文句ですが、もう少し実践的なWidgetの構成を考えた場合、WidgetをScreenとComponentに分ける と良いです。
Screenとはその名の通り画面のことを指しており、複数の画面で構成されるアプリの場合、1画面 = 1Screenファイルという粒度でWidgetを分割します。画面の大枠を作るScreenに対して、詳細なUIを構成するボタンやリストのアイテムなど、部品単位のWidgetがComponent Widgetです。
Component Widgetはメソッド抽出と同じ要領で、深くネストしたWidgetを部分的に切り出したり、共通で使い回したい部分を抽出して使い回せる様にすることで、重複するコードの削除やコードの変更可用性を向上させます。 FlutterはUIをWidgetで構成するという統一されたルールに従うので、容易にUIを構築できる反面、複数のWidgetを何重にも重ねてしまう(ネストする)ため、必然的に階層が深くなってしまいます。
深くなることでコードの見通しが悪くなったり、変更を加えるのが大変になる状況が容易に発生し得るので、それらを避けるためにも、Widgetを一定の粒度でComponentに切り出して対応していくことを意識していくと良いです。
BLoC (RxDart )
BLoCはFlutter界隈で新しく定義されたアプリケーション開発におけるパターン(構造)です。一見真新しいパラダイムの様にも思えますが、従来のネイティブアプリケーション界隈で確立されたReactive Programming、Streams、MVVMにFlutter独自の要素を組み合わせて、昇華させた実装パターンと考えるとわかりやすいかもしれません。※厳密には異なります ネイティブ開発を経験されている方でしたら、構造や機能を類推して考えられると思うので取っ掛かりには苦労しないかと思います。
FlutterではこのBLoCがWidgetに関連したデータの受け渡しと、それに関連する処理を一手に担うことで、Widget側にビジネスロジックが散らかることを防ぎ、結果としてUIとロジックを明瞭に分離できる様になります 。 明瞭な分離定義による効果として、実装追加時の迷いが減ることや、変更の影響範囲が局所化されたりします。また、Widgetのビルド回数が減らせることでアプリパフォーマンス的にも向上するといったメリットも享受することができます。
言葉でBLoCの存在を説明するのには限界があるので、Fluttter Expertの方々が公開しているBLoC PatternのExample実装を参考に見てみることをオススメします。Githubなどで検索するといくつかヒットしますが、個人的に参考になったリポジトリは下記です。
・ Streams - BLoC - Reactive Programming ・ MyMovies
これらのコードを読み解く際のポイントとして、WidgetとBLoCの結びつき、BLoCからAPIコールロジックの呼び出しの関係性を意識して読むと理解が早いと思います。
BLoC PatternではWidgetとBLoC間のやり取りがStream機能を用いて行われるのですが、Flutterで使われるDart言語には標準で強力なStream機能が備わっており、これだけでも通常の実装には十分事足りるようになっています。 標準機能でも十分事は足りるのですが、さらに拡張機能を盛り込んだコミュニティ開発のパッケージとしてRxDartというのがあります。こちらはReactiveXの文脈にある機能群が揃っているもので、基本的な扱い方と振る舞いは標準のStreamと同じなのですが、RxDartの方が細かい部分まで手が届く(キャッシュや初期値など)機能が備わっています。なので、実践利用の観点で機能が必要な場合は迷わずRxDartを使いましょう
・ 【BLoC / RxDart入門】Flutterの公式チュートリアルを書き換える
また、Widget側には標準でStreamを受け取り展開するWidgetであるStream Builder Widgetが備わっており、容易にBLoCからのデータの受け取りと展開が可能です。
・ StreamBuilder T class
この様にFlutterでは標準で機能が提供されており、BLoC Patternを使い回す土壌は完全に揃っているので、あとはそれらを扱い倒すのみとなっています。
Resource (Model / API)
Resourceはアプリ内で使用するリソース(データなど)の提供を担う層で、サーバーとの通信を行う処理や定数などを定義します。この層の処理は基本的にはBLoCから呼び出されます。具体的には、外部サーバーと通信を行うAPIハンドリングの処理、受け取ったデータをアプリ内で扱いやすくするために、クラスインスタンスへデータマッピングする処理、Resouceをひとまとまりにして扱いやすくるRepositoryなどが置かれます。 界隈の表現の特徴として "hogehoge_api_provider.dart といった感じに、語尾がproviderという命名で統一しているのをよく見かけるので、そういった命名規約にするとわかりやすくなりそうです。 Resource層の実装に関してはFlutter独自の特徴などはなく、従来のネイティブ開発時と同じスタンスでAPIハンドリングとマッピングデータのレスポンス処理を実装する形で問題ないと思います。 実装の参考として@konifarさんが実装されたdroidkaigi2018-flutterが参考になります。
ここまでで、ざっくり **Widget Tree、BLoC、Resource ** と分けて要点を紹介しました。今回触れた話題は全てBLoC Patternの文脈の中でも特筆すべき要点にのみ絞っているため、これらは全体のほんの一部にすぎません。 ここまでで、大まかにFlutter上でどういった概念が組み合わさっているのかその雰囲気を掴んでいただけたのではないでしょうか。見知らぬ単語や新しい概念の関係性について、疑問に浮かんだものを1つ1つ分解して理解していくことで、ここまでの内容の意味合いが掴めるかと思います。 また、Flutter×Bloc Patternを理解する上で必要な知識と理解は、既に界隈にたくさんの有益な記事があるので、こちらも合わせて読んでみてください。
・ Flutter JP
余談ですが、キャッチアップを行った個人的な感想として、FlutterでBLoC Patternを採用し、スピード感を持って実装を行う際にはUIを良い感じに作り上げるWidget力 と、Streamの流れとロジックを組み立てるRx力 が重要かなと思いました。これらを図で表すと以下の様な範囲を示していて。
具体的にはWidgetの特性とその組み方に習熟して期待する表現を素早く構築する力と、画面に表示したいデータや、実現したいアクティビティをStreamを活用して一連の流れとして作り上げる力のことです。この2つを意識することがFlutter×BLoC Patternを身に着けるには最速な気がしています。
次に、BLoC Patternに焦点を絞って実践に取り入れる際の押さえておくポイントをいくつか紹介したいと思います。
BLoC Patternを適用する上で押さえておくポイント
Local BLoC / Global BLoC
BLoC Patternの概念を理解して、いざ実践適用しようとした時に
「どの粒度でBLoCを適用すれば良いのだろうか?」
という疑問が生まれました。
この疑問に対して思いついた粒度の候補としては、
などがあるのかなと考えました。
疑問について色々と調べたところ、界隈で提案されている適用アプローチの1つに、スクリーンごとのBLoCとアプリ全体で共有するBLoCに分けて管理するという手法が存在しました。 この手法は人によって呼ばれ方が異なっており、共通のスタンダードな定義がなかったので、ここではスクリーンごとに参照スコープがクローズドなものをLocal BLoC 、参照スコープがオープンなものをGlobal BLoC と呼ぶことにします。このLocal BlocとGlobal Blocを図で表すと以下の様になります。
上の図では色ごとに範囲が異なっていることが見て取れると思います。 この色の範囲がBLoCを参照することができるスコープの広さを表しており、Root BLoC (オレンジ)は配下にある全てのWidgetから参照することが可能です。Global BLoCにはアプリケーション全体で共有したいもの、もしくは複数のスクリーンで共通利用したい処理をまとめて定義します。 Global Blocに定義するものの具体的な例としては、メディアアプリにおける記事のお気に入り登録機能(Favorite Bloc)であったり、ECアプリでいうカートへの登録機能(Cart Bloc)などです。これらがGlobal Blocとして定義されることで、アプリケーション全体で呼び出されて使われ感じになります。
Global BLoCに対してLocal BLoCであるScreen 1 BLoCは、Screen Widget 1に依存しており、BLoCへの参照スコープが配下のWidgetにみ制限されています。このLocal BLoCには単一のスクリーンでのみ使用する処理、具体的には、特定画面で表示するリストデータの取得処理や、データ登録の送信処理などをまとめて定義します。
この様にBLoCを**"スコープの広さ"** という切り口で分けるとLocal BLoC とGlobal BLoC という定義をすることができ、処理のまとまりがスクリーンにのみ紐づくものと、アプリケーション全体で共有するものに分けられるため、単純でわかりやすい構造になります。 逆に複数の異なる機能群を共通のBlocに1つにまとめてしまうと、巨大なGlobal Singleton Blocとなり、見通しが悪く、スクリーンによってはメモリを使い続けてしまうといった悪影響が生まれたりと弊害が大きくなってしまいます。なので、中規模程度(複数スクリーン、複数API使用)でBloc Patternを適用したい場合には、参照スコープを切り口にLocal BlocとGlobal Blocという分け方でBlocを定義して適用すると良いです。
ちなみに、現在取り組んでいるアプリケーション開発に適用しているBLoC Patternの適用構造は基本この方針に則って実装していますが、今の所特段問題もなく、むしろ非常に見通しよく開発できているので実感値的にもこの切り口でのBLoC定義が良いなと思っています。
執筆後記 本文を執筆後に知ったことなのですが、どうやら上記で記した状態管理の考え方は宣言的UI というパラダイムでの考え方を踏襲している様です。こちらの資料も参考になるかもしれません。
執筆後記 11/8 11/7に開催されたFlutter Meetup Tokyo 12で「BLoC PatternにおけるBLoC分割の方針と実践例」というタイトルで話をしました。こちらの資料も参考にしてみてください。
BLoC Providerの導入
BLoCをWidgetに注入する際に、Providerを用いることがデファクトスタンダードになっています。このProviderとは、WidgetとBLoCの中継ぎを担う存在で、Providerを経由することで、孫Widget以下からの参照の効率性を良くする機能を含ませたり。また、注入処理を汎用的なProviderとして定義することで、追加実装がわかりやすくなります。
Provider周りは昨年から色々と扱い方の議論がされてきており、各々が独自に実装をしている状況でしたが、最近になってGoogleが公式推奨としたパッケージなどが出てきました。Providerの機能を独自で実装するのも有りだと思いますが、特にこだわりがなければ基本的にパッケージを使うと良いです。このあたりの話は、既に@kabochapoさんが丁寧な解説記事で紹介してくださっているので、弊社ではそちらを参考に導入をすすめました。
また、Provider周りの詳細な仕組みと注意点などについては、@_monoさんの記事が懇切丁寧にまとまっているので、こちらを読んで理解するのが一番早いのでオススメです。
disposeの動作を担保する
BLoCを扱う上で注意することの主要な話題としてdisposeの設定があります。 界隈にとってはこの話題は既に2周ぐらいしている感じなのですが、初見でも"ここだけは覚えておいて!"というくらい大事なところなので外さず紹介します。
// ex: Widget and dispose
class ItemListState extends State<ItemList> {
@override
void initState() {
super.initState();
_scrollController.addListener(_scrollListener);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_bloc = ItemListBlocProvider.of(context);
_bloc.loadItems();
}
@override
void dispose() {
_bloc.dispose(); // widgetのdispose時にdisposeを呼ぶ
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(child: ....)
}
}
class ItemListBloc {
int page = 0;
List<Item> items = [];
final _listItemSubject = PublishSubject<List<Item>>();
Observable<List<Item>> get listItem => _listItemSubject.stream;
void load() {
page++;
final values = await _repository.getItems(page);
items.addAll(values);
_subject.sink.add(items);
}
// disposeを呼ぶとstreamをclose
void dispose() {
_listItemSubject.close();
}
}
Widgetのdispose処理時にインスタンス化されたBLoCの中で宣言されているstreamをcloseする処理を仕込み、Widgetの破棄をトリガーにして、Streamが閉じられる様にします。こうすることで、Streamが確保しているメモリ空間がWidgetの破棄に付随して解放されるようなります。その結果、アプリケーションが不用意にメモリを喰い潰して期待しない動きをしない様になります。このdispose処理に伴うStreamを閉じる処理は、特にNavigationなどで画面を作る場合に有効で、bottom navigationなどで画面切り替える形式のスクリーンに紐づくBLoCはアプリケーションが終了するとそもそもWidget自体破棄されます。
ディレクトリ構成 / UI構成
初見でのキャッチアップでは大局的な概念関係の理解も大事ですが、実際のコードを読み解くことも大事で、コード間の具体的な構造を捉えることは、仕組みの理解を早めます。 コードを読み解く際には、まず全体のディレクトリ構成を把握して、配置されているコードの意図を汲み取るのが最速のキャッチアップ法の1つだと個人的には思っており、下記に弊社の事例として構造把握が容易になるディレクトリの構造例を紹介します。
また、実際に開発に着手し始めるとUI関係の遷移と構成がわかりづらくなるため、簡易的にもUIの構成図を用意すると人と議論する際の内容にズレが生じにくくなります。 自分たちは下記の様な簡易的なUI構成と関連するBLoCの詳細情報を記載した図を用意して、都度困りごとがあればそれを参照しながら議論する形で開発を進めています。
ここまででBLoCの適用粒度、Providerの導入、disposeの設定、ディレクトリ構成/UI構成という実践導入のポイントを紹介しました。どの内容も実際に作ってみて実感値でわかるものだと思うので、これらを参考にFlutterの実装に着手してみてください。
最後に
現在実践導入に向けてFlutterで開発を進めている最中なのですが、先に紹介したLab Weekを用いてFlutterのキャッチアップに取り組めたことで、とてもスムーズに開発のスタートを切ることができました。また、今回自分は技術検証からアーキテクチャの選定、採用、実装と0ベースでFlutterの開発に着手でき、非常に知見溢れる開発に取り組めています。 本記事は新たに開発に取り組んで行く際に、これまで自身が疑問に思ったポイントと大局的な概念関係を中心に、今後BLoC Patternを使ってみようとしている方の足掛けになる様な内容にしようと思い、執筆しました。 BLoC Patternを語る上では、まだまだ足りない要素(BLoC間の連携 / RxDartの使い所 / メジャーパッケージ etc...)がたくさんあるのですが、それらは機会があれば紹介できればと思っています。
参考文献
- ・ FlutterのBLoC(Business Logic Component)のライフサイクルを正確に管理して提供するbloc_providerパッケージの解説
- ・ 長めだけどたぶんわかりやすいBLoCパターンの解説
- ・ FlutterのBLoCパターンについて得た知見
- ・ Flutter DemoアプリをBLoCパターンで書き直してみた
- ・ BLoCパターンとはなにか - FlutterとAngularの間でModelのコードを再利用する実践を通じての考察
- ・ Reactive Programming - Streams - BLoC - Practical Use Cases
- ・ Architect your Flutter project using BLOC pattern