Flutterでアプリを作るアーキテクチャの一例
- app
- pages
- widgets
- handlers
- helpers
- domain
- models
- services
- infra
- remote
- shared_preference
- device_info
UIに関連するコード。
pagesは画面、widgetsはパーツやコンポーネントを置く。
Flutterでは画面とパーツに大きな違いはないが、 人間の認識では違うものなので、なんとなく分けておく。
依存: handlersとhelpersに依存する。domainやinfraを直接使うことはない。
UIからの入力を受け付ける。
アプリケーションのロジックを書く場所。 アプリ/UIに近いロジックを書き、 domain/servicesにあるドメインロジックを使う。
イベント Notification
が通知されてくるので、
必要に応じてserviceに処理をさせる。
状態とイベントを合成することでserviceへの入力になる。
状態は持たず、Providerによって親から提供された状態を得る。
このため、Widget treeのことは知っている。 UIを構築はしないがWidgetやFlutterには依存している。
画面やコンポーネントに対応した、小さいhandlerがたくさんある。 serviceよりも細かい単位で区切られる(ことが多いと思われる)。
例: NoteShowHandler, NoteEditHandler <-> NoteService
依存: domain/services に依存する。infraを直接使うことはない。
UIに関する処理を行うが、UIパーツではないもの。
日付のフォーマット、数値の変換など。
依存: 他のモジュールには依存しない。
ビジネスロジックを置いておくところ。
ドメインにおけるデータをモデリングしたクラスを定義する。
ビジネスロジックは持たない、単なるデータクラス。
依存: 他のモジュールに依存しない。
ドメインにおけるビジネスロジックを記述する。
infraの各モジュールを利用し、リモートやローカルやデバイスを操作する。
依存: infraを使う。
サーバAPIとの通信、デバイス機能の利用など、Serviceから利用されるモジュール郡。
リポジトリ層でもある。
依存: models以外のモジュールには依存しない。
このサンプルアプリの状態管理について知る前に、その前段として ValueNotifier, ChangeNotifier と ValueListenableProvider, ChangeNotifierProvider を使う方法について。
ValueListenableProvider<int>(
create: (BuildContext context) => ValueNotifier<int>(0),
child: const YourCounterWidget(),
);
使う側は Provider.of もしくは Consumer で値を得る。
Consumer<int>(
builder: (BuildContext context, int value, Widget child) {
return Text('count is ${value}');
},
);
final int value = Provider.of<int>(context, listen: true);
return Text('count is ${value}');
本アプリでも、構造や内部の仕組みは、ValueNotifier, ChangeNotifier と似たようなことを行っている。
ただし、別の箇所にあるWidgetが、同じ状態を参照するために、 store_builder というライブラリを使っている。
SubjectProvider<int>(
initialValue: (BuildContext context) => 0,
id: 'my counter',
child: const YourCounterWidget(),
);
※consumeするほうは同じ
https://pub.dev/packages/flutter_redux
Reduxは状態を扱うStoreがアプリ内にひとつだけ存在する。 このため、状態の管理が中央で行われ、扱いやすい。
しかしながら、このStoreにはアプリ内のさまざまな状態が含まれる。
どれかを更新すると、すべての関連するWidgetの問い合わせの処理 (Storeから一部の状態を取り出してViewModelを作る)が実行される。
Widgetのrebuildではないが、不要な処理が実行されることになり、 アプリが大きくなったときのパフォーマンス上の懸念のため、採用しない。
※どの程度の影響があるか計測はしていない
Flutter には Notification というクラスがあり、 Widget ツリーをさかのぼってイベントを伝えることができる。
※プッシュ通知のことではなく、Flutter内の仕組み
例えば、スクロールできる Widget でスクロールすると ScrollNotification が発行されており、 RefreshIndicator はこれを利用して実装されている。
ボタンなどで Notification を発行し、 NotificationListener で処理を行う。
NotificationListener<SomeNotification>(
onNotification: (SomeNotification notification) {
if (notification is SomeNotification) {
// イベントに応じた処理
}
return true;
},
child: const YourChildWidget(),
);
notification_handler
ライブラリを使う。
NotificationHandler で、子孫で発生した Notification を受け取り、 ハンドリングの状態を返す(provideされる)。
NotificationHandler<ArticleEvent, ArticleState>(
initialState: const ArticleInital(),
onEvent: (BuildContext context, ArticleEvent event) async* {
final StoredSubject<Article> subject = subjectOf<Article>(context);
if (event is ArticleStart) {
yield const ArticleLoading();
subject.value = await loadArticle();
yield const ArticleSuccess();
}
...
},
builder: builder,
child: child,
);