Flutter アーキテクチャ: プロバイダーと BLoC
公開: 2020-04-17Flutter でアプリを作成すると、アーキテクチャを選択する絶好の機会が生まれます。 よくあることですが、「どれを選べばいいの?」という質問へのベストアンサー。 「場合による」です。 その答えが得られたら、プログラミングの専門家を見つけたことを確信できます。
この記事では、モバイル アプリケーションで最も一般的な画面について説明し、それらを最も一般的な 2 つの Flutter アーキテクチャであるProviderとBLoCに実装します。 その結果、各ソリューションの長所と短所を学び、次のモジュールまたはアプリケーションに適した Flutter アーキテクチャを選択するのに役立ちます。
Flutter アーキテクチャの簡単な紹介
Flutter 開発プロジェクトのアーキテクチャを選択することは非常に重要です。これは主に、あまり使用されていない宣言型プログラミング パラダイムを扱っているためです。 これにより、ネイティブの Android または iOS 開発者が慣れ親しんできたステート管理へのアプローチが完全に変わり、コードを命令的に記述できるようになりました。 アプリケーションのある場所で利用可能なデータは、別の場所で取得するのはそれほど簡単ではありません。 ツリー内の他のビューへの直接参照はありません。そこから現在の状態を取得できます。
Flutter のプロバイダーとは
名前が示すように、 Providerは Flutter アーキテクチャであり、現在必要な場所に現在のデータ モデルを提供します。 いくつかのデータが含まれており、変更が発生したときにオブザーバーに通知します。 Flutter SDK では、この型はChangeNotifierと呼ばれます。 タイプChangeNotifierのオブジェクトを他のウィジェットで使用できるようにするには、 ChangeNotifierProviderが必要です。 そのすべての子孫に監視対象オブジェクトを提供します。 現在のデータを受け取ることができるオブジェクトはConsumerで、ビルド関数のパラメーターにChangeNotifierインスタンスがあり、後続のビューにデータを供給するために使用できます。
Flutter の BLoC とは
ビジネス ロジック コンポーネントは、MVP や MVVM などのモバイルで一般的なソリューションによく似た Flutter アーキテクチャです。 これにより、ビジネス ロジック ルールからプレゼンテーション層が分離されます。 これは、Flutter が強く強調している宣言型アプローチ、つまりUI = f (state)を直接適用したものです。 BLoC は、ユーザー インターフェイスからのイベントが移動する場所です。 このレイヤー内で、特定のイベントにビジネス ルールを適用した結果として、BLoC は特定の状態で応答し、UI に戻ります。 ビューレイヤーが新しい状態を受け取ると、現在の状態が必要とするものに従ってビューを再構築します。
Flutter の開発に興味がありますか?
ソリューションを見るFlutter でリストを作成する方法
スクロール可能なリストは、おそらくモバイル アプリケーションで最も人気のあるビューの 1 つです。 したがって、ここでは適切な Flutter アーキテクチャを選択することが重要になる場合があります。 理論的には、リスト自体を表示することは難しくありません。 たとえば、各要素に対して特定のアクションを実行する機能を追加すると、状況はさらに複雑になります。 これにより、アプリのさまざまな場所で変更が発生するはずです。 リストでは、各要素を選択することができ、選択した各要素が別の画面の個別のリストに表示されます。

したがって、選択した要素を保存して、新しい画面に表示できるようにする必要があります。 さらに、実際にチェック/チェック解除を表示するには、チェックボックスがタップされるたびにビューを再構築する必要があります。
リスト アイテム モデルは非常に単純に見えます。
クラスソーシャルメディア{ 整数 ID; 文字列のタイトル。 文字列 iconAsset; bool isFavorite; ソーシャルメディア( {@必須 this.id, @必須 this.title, @必須 this.iconAsset, this.isFavourite = false}); void setFavourite(bool isFavourite) { this.isFavourite = isFavourite; } }
Provider でリストを作成する方法
Provider パターンでは、上記のモデルをオブジェクトに格納する必要があります。 オブジェクトは、 ChangeNotifierを拡張して、アプリ内の別の場所からSocialMediaにアクセスできるようにする必要があります。
class SocialMediaModel は ChangeNotifier を拡張します { final List<SocialMedia> _socialMedia = [ /* いくつかのソーシャル メディア オブジェクト */ ]; UnmodifiableListView<SocialMedia> お気に入りを取得 { return UnmodifiableListView(_socialMedia.where((item) => item.isFavourite)); } UnmodifiableListView<SocialMedia> すべてを取得 { UnmodifiableListView(_socialMedia) を返します。 } void setFavourite(int itemId, bool isChecked) { _ソーシャルメディア .firstWhere((アイテム) => item.id == itemId) .setFavourite(isChecked); notifyListeners(); }
ビューでの再構築が必要になるこのオブジェクトの変更は、 notifyListeners()を使用して通知する必要があります。 UI フラグメントを再レンダリングするよう Flutter に指示するsetFavourite()メソッドの場合、このオブジェクトの変更が観察されます。
これで、リストの作成に進むことができます。 ListViewを要素で埋めるには、すべての要素のリストを格納するSocialMediaModelオブジェクトを取得する必要があります。 次の 2 つの方法で実行できます。
- Provider.of<ModelType>(context, listen: false)
- 消費者
最初のものは観察されたオブジェクトを提供し、 listenパラメータを使用して、オブジェクトに対して実行されたアクションが現在のウィジェットを再構築する必要があるかどうかを決定できるようにします。 この動作は、私たちの場合に役立ちます。
class SocialMediaListScreen extends StatelessWidget { SocialMediaListScreen(); @オーバーライド ウィジェットのビルド(BuildContext コンテキスト) { var socialMedia = Provider.of<SocialMediaModel>(context, listen: false); リストビューを返します( 子: socialMedia.all .map((アイテム) => CheckboxSocialMediaItem(アイテム: アイテム)) .toList()、 ); } }
すべてのソーシャル メディアのリストが必要ですが、リスト全体を再構築する必要はありません。 リスト アイテム ウィジェットがどのようなものか見てみましょう。
class CheckboxSocialMediaItem extends StatelessWidget { 最終的なソーシャルメディア アイテム。 CheckboxSocialMediaItem({Key key, @required this.item}) : super(key: key); @オーバーライド ウィジェットのビルド(BuildContext コンテキスト) { パディングを返す( パディング: const EdgeInsets.all(Dimens.paddingDefault), 子: 行( 子供: [ Consumer<ソーシャルメディアモデル>( ビルダー: (コンテキスト、モデル、子) { チェックボックスを返す( 値: item.isFavorite, onChanged: (isChecked) => model.setFavourite(item.id, isChecked), ); }、 )、 SocialMediaItem( アイテム: アイテム, ) ]、 )、 ); } }
チェックボックスの値の変化をリッスンし、チェック状態に基づいてモデルを更新します。 チェックボックスの値自体は、データ モデルのプロパティを使用して設定されます。 これは、選択後、モデルがisFavouriteフィールドをtrueに変更することを意味します。 ただし、チェックボックスを再構築するまで、ビューはこの変更を表示しません。 ここでは、 Consumerオブジェクトにヘルプが付属しています。 観察されたオブジェクトを提供し、モデルの変更に関する情報を受け取った後、すべての子孫を再構築します。
不必要なビューの再構築を避けるために、ウィジェットを更新する必要がある場所にのみConsumerを配置する価値があります。 たとえば、チェックボックスの選択がアイテムのタイトルの変更などの追加のアクションをトリガーする場合、タイトルの表示を担当するウィジェットの親になるように、 Consumerをウィジェット ツリーの上位に移動する必要があることに注意してください。 . そうしないと、タイトル ビューが更新されません。
お気に入りのソーシャル メディア画面の作成も同様です。 Providerを使用して、お気に入りのアイテムのリストを取得します。
class FavouritesListScreen extends StatelessWidget { FavoritesListScreen(); @オーバーライド ウィジェットのビルド(BuildContext コンテキスト) { var list = Provider.of<SocialMediaModel>(context, listen: false).favorites; リストビューを返します( 子: リスト .map((アイテム) => パディング( パディング: const EdgeInsets.all(Dimens.paddingDefault), 子: SocialMediaItem(アイテム: アイテム))) .toList()、 ); } }
buildメソッドが呼び出されると、プロバイダーはお気に入りのソーシャル メディアの現在のリストを返します。
BLoC でリストを作成する方法
この単純なアプリケーションでは、これまでのところ 2 つの画面があります。 それぞれに独自のBLoCオブジェクトがあります。 ただし、メイン画面で選択したアイテムは、お気に入りのソーシャル メディアのリストに表示されることに注意してください。 したがって、どうにかしてチェックボックスの選択イベントを画面外に転送する必要があります。 解決策は、多くの画面の状態に影響を与えるイベントを処理する追加のBLoCオブジェクトを作成することです。 これをグローバルBLoCと呼びましょう。 次に、個々の画面に割り当てられたBLoCオブジェクトは、グローバルなBLoC状態の変化をリッスンし、それに応じて応答します。
BLoCオブジェクトを作成する前に、まず、ビューが BLoC レイヤーに送信できるイベントと、それが応答する状態について考える必要があります。 グローバルBLoCの場合、イベントと状態は次のようになります。
抽象クラス SocialMediaEvent {} class CheckboxChecked extends SocialMediaEvent { 最終ブール値がチェックされます。 最終的な int itemId; CheckboxChecked(this.isChecked, this.itemId); } 抽象クラス SocialMediaState {} class ListPresented extends SocialMediaState { 最終 List<SocialMedia> リスト; ListPresented(this.list); }
CheckboxCheckedイベントは、1 つだけでなく多くの画面の状態に影響するため、グローバルBLoCにある必要があります。 状態に関しては、リストを表示する準備ができている状態があります。 グローバルBLoCビューの観点からは、これ以上ステートを作成する必要はありません。 両方の画面にリストが表示され、特定の画面専用の個々のBLoCがそれを処理する必要があります。 グローバルBLoC自体の実装は次のようになります。
class SocialMediaBloc extends Bloc<SocialMediaEvent, SocialMediaState> { 最終的な SimpleSocialMediaRepository リポジトリ。 SocialMediaBloc(this.repository); @オーバーライド SocialMediaState get initialState => ListPresented(repository.getSocialMedia); @オーバーライド Stream<SocialMediaState> mapEventToState(SocialMediaEvent event) async* { if (イベントは CheckboxChecked) { yield _mapCheckboxCheckedToState(イベント); } } SocialMediaState _mapCheckboxCheckedToState(CheckboxChecked イベント) { 最終的な updatedList = (ListPresented としての状態).list; 更新リスト .firstWhere((アイテム) => item.id == event.itemId) .setFavourite (event.isChecked); ListPresented(updatedList) を返します。 } }
初期状態はListPresentedです。リポジトリからデータを既に受け取っていると仮定します。 CheckboxCheckedという 1 つのイベントにのみ応答する必要があります。 そのため、 setFavouriteメソッドを使用して選択した要素を更新し、 ListPresented状態でラップされた新しいリストを送信します。
ここで、チェックボックスをタップするときにCheckboxCheckedイベントを送信する必要があります。 これを行うには、 onChangedコールバックをアタッチできる場所にSocialMediaBlocのインスタンスが必要です。 このインスタンスは、 BlocProviderを使用して取得できます。これは、上記のパターンのProviderに似ています。 このようなBlocProviderを機能させるには、ウィジェット ツリーの上位で、目的のBLoCオブジェクトを初期化する必要があります。 この例では、メイン メソッドで実行されます。
void main() => runApp(BlocProvider( 作成: (コンテキスト) { return SocialMediaBloc(SimpleSocialMediaRepository()); }、 子: ArchitecturesSampleApp()));
このおかげで、メイン リスト コードでは、 BlocProvider.of ()を使用してBLoCを簡単に呼び出し、 addメソッドを使用してそれにイベントを送信できます。
class SocialMediaListScreen extends StatefulWidget { _SocialMediaListState createState() => _SocialMediaListState(); } class _SocialMediaListState extends State<SocialMediaListScreen> { @オーバーライド ウィジェットのビルド(BuildContext コンテキスト) { return BlocBuilder<SocialMediaListBloc, SocialMediaListState>( ビルダー: (コンテキスト、状態) { if (状態は MainListLoaded です) { リストビューを返します( 子: state.socialMedia .map((アイテム) => CheckboxSocialMediaItem( アイテム: アイテム, onCheckboxChanged: (isChecked) => BlocProvider.of<SocialMediaBloc>(コンテキスト) .add(CheckboxChecked(isChecked, item.id)), ))) .toList()、 ); } そうしないと { センターを返します(子:Text(Strings.emptyList)); } }、 ); } }
BLoCへのCheckboxCheckedイベントの伝播は既にあり、 BLoCがそのようなイベントにどのように応答するかもわかっています。 しかし実際には…チェックボックスが選択された状態でリストが再構築される原因は何ですか? グローバルBLoCは、画面に割り当てられた個々のBLoCオブジェクトによって処理されるため、リスト状態の変更をサポートしていません。 解決策は、前述のグローバルBLoCをリッスンして状態を変更し、この状態に従って応答することです。 以下は、チェックボックス付きのメインのソーシャル メディア リスト専用のBLoCです。

クラス SocialMediaListBloc extends Bloc<SocialMediaListEvent, SocialMediaListState> { 最終的な SocialMediaBloc メインブロック。 SocialMediaListBloc({@required this.mainBloc}) { mainBloc.listen((状態) { if (状態は ListPresented です) { add(ScreenStart(state.list)); } }); } @オーバーライド SocialMediaListState get initialState => MainListEmpty(); @オーバーライド Stream<SocialMediaListState> mapEventToState( SocialMediaListEvent イベント) async* { スイッチ (event.runtimeType) { ケーススクリーンスタート: yield MainListLoaded((ScreenStart としてのイベント).list); 壊す; } } }
SocialMediaBlocが状態ListPresentedを返すと、 SocialMediaListBlocに通知されます。 ListPresentedはリストを伝達することに注意してください。 チェックボックスでアイテムをチェックすることに関する更新情報を含むものです。
同様に、お気に入りのソーシャル メディア画面専用のBLoCを作成できます。
class FavouritesListBloc extends Bloc<FavouritesListEvent, FavouritesListSate> { 最終的な SocialMediaBloc メインブロック。 FavouritesListBloc({@required this.mainBloc}) { mainBloc.listen((状態) { if (状態は ListPresented です) { add(FavoritesScreenStart(state.list)); } }); } @オーバーライド FavouritesListSate get initialState => FavouritesListEmpty(); @オーバーライド Stream<FavouritesListSate> mapEventToState(FavouritesListEvent event) async* { if (イベントは FavouritesScreenStart です) { var favouritesList = event.list.where((item) => item.isFavourite).toList(); yield FavouritesListLoaded(favoritesList); } } }
グローバルBLoCの状態を変更すると、現在のリストでFavouritesScreenStartイベントが発生します。 次に、お気に入りとしてマークされたアイテムがフィルタリングされ、そのようなリストが画面に表示されます。
Flutter で多くのフィールドを持つフォームを作成する方法
要件がさまざまな検証バリアントを想定している場合や、テキストを入力した後に画面にいくつかの変更がある場合は特に、長いフォームは注意が必要です。 サンプル画面には、いくつかのフィールドと「次へ」ボタンで構成されるフォームがあります。 フィールドは自動的に検証され、フォームが完全に有効になるまでボタンは無効になります。 ボタンをクリックすると、フォームに入力されたデータを含む新しい画面が開きます。
ボタンの状態を適切に設定するには、各フィールドを検証し、フォーム全体の修正を確認する必要があります。 次に、収集したデータを次の画面のために保存する必要があります。

Provider を使用して多くのフィールドを持つフォームを作成する方法
このアプリケーションでは、個人情報画面専用の 2 つ目のChangeNotifierが必要です。 したがって、 ChangeNotifierオブジェクトのリストを提供するMultiProviderを使用できます。 これらは、 MultiProviderのすべての子孫で利用できます。
class ArchitecturesSampleApp extends StatelessWidget { 最終的な SimpleSocialMediaRepository リポジトリ。 ArchitecturesSampleApp({Key key, this.repository}) : super(key: key); @オーバーライド ウィジェットのビルド(BuildContext コンテキスト) { マルチプロバイダを返す( プロバイダー: [ ChangeNotifierProvider<ソーシャルメディアモデル>( 作成: (コンテキスト) => SocialMediaModel(リポジトリ)、 )、 ChangeNotifierProvider<PersonalDataNotifier>( 作成: (コンテキスト) => PersonalDataNotifier(), ) ]、 子:MaterialApp( タイトル: Strings.architecturesSampleApp, debugShowCheckedModeBanner: false, ホーム: StartScreen(), ルート: <String, WidgetBuilder>{ Routes.socialMedia: (コンテキスト) => SocialMediaScreen(), Routes.favourites: (コンテキスト) => FavouritesScreen(), Routes.personalDataForm: (コンテキスト) => PersonalDataScreen(), Routes.personalDataInfo: (コンテキスト) => PersonalDataInfoScreen() }、 )、 ); } }
この場合、 PersonalDataNotifierはビジネス ロジック レイヤーとして機能します。フィールドを検証し、更新のためにデータ モデルにアクセスし、ビューが依存するフィールドを更新します。
フォーム自体は Flutter の非常に優れた API であり、プロパティバリデーターを使用して自動的に検証をアタッチし、 onSavedコールバックを使用してフォームからモデルにデータを保存できます。 検証ルールをPersonalDataNotifierに委譲し、フォームが正しい場合は、入力されたデータをフォームに渡します。
この画面で最も重要なことは、各フィールドの変更をリッスンし、検証結果に応じてボタンを有効または無効にすることです。 FormオブジェクトからコールバックonChangeを使用します。 その中で、最初に検証ステータスを確認し、それをPersonalDataNotifierに渡します。
形( キー: _formKey、 自動検証: 真、 onChanged: () => _onFormChanged(personalDataNotifier), 子: void _onFormChanged(PersonalDataNotifier personalDataNotifier) { var isValid = _formKey.currentState.validate(); personalDataNotifier.onFormChanged(isValid); }
PersonalDataNotifierでは、 isFormValid変数を用意します。 それを変更し ( notifyListeners()を呼び出すことを忘れないでください)、ビューで、その値に応じてボタンの状態を変更します。 パラメータlisten: trueを使用してNotifierインスタンスを取得することを忘れないでください。それ以外の場合、ビューは再構築されず、ボタンの状態は変更されません。
var personalDataNotifier = Provider.of<PersonalDataNotifier>(context, listen: true);
実際には、ビューのリロードが必要ない他の場所でpersonalDataNotifierを使用するという事実を考えると、上記の行は最適ではなく、 listenパラメーターをfalseに設定する必要があります。 リロードしたいのはボタンだけなので、従来のConsumerでラップできます。
Consumer<PersonalDataNotifier>( ビルダー: (コンテキスト、通知者、子) { RaisedButton を返す( 子: Text(Strings.addressNext), onPressed: notifier.isFormValid ? /* ボタン有効時の動作 */ : ヌル、 色: Colors.blue, disabledColor: Colors.grey, ); }、 )
このおかげで、ノーティファイアを使用するたびに他のコンポーネントを強制的にリロードする必要がありません。
個人データを表示するビューでは、これ以上の問題はありません。PersonalDataNotifierにアクセスでき、そこから更新されたモデルをダウンロードできます。
BLoC で多くのフィールドを持つフォームを作成する方法
前の画面では、2 つのBLoCオブジェクトが必要でした。 したがって、別の「ダブル スクリーン」を追加すると、全部で 4 つになります。 Providerの場合と同様に、ほぼ同じように動作するMultiBlocProviderで処理できます。
void main() => runApp( MultiBlocProvider(プロバイダー: [ ブロックプロバイダ( 作成: (コンテキスト) => SocialMediaBloc(SimpleSocialMediaRepository()), )、 ブロックプロバイダ( 作成: (コンテキスト) => SocialMediaListBloc( mainBloc: BlocProvider.of<SocialMediaBloc>(context))), ブロックプロバイダ( 作成: (コンテキスト) => PersonalDataBloc(), )、 ブロックプロバイダ( 作成: (コンテキスト) => PersonalDataInfoBloc( mainBloc: BlocProvider.of<PersonalDataBloc>(コンテキスト))、 ) ]、子: ArchitecturesSampleApp())、 );
BLoCパターンと同様に、考えられる状態とアクションから始めるのが最善です。
抽象クラス PersonalDataState {} クラス NextButtonDisabled は PersonalDataState {} を拡張します クラス NextButtonEnabled は PersonalDataState を拡張します {} class InputFormCorrect extends PersonalDataState { 最終的な PersonalData モデル。 InputFormCorrect(this.model); }
この画面で変化しているのはボタンの状態です。 したがって、それには別の状態が必要です。 さらに、 InputFormCorrect状態により、フォームが収集したデータを送信できます。
抽象クラス PersonalDataEvent {} class FormInputChanged extends PersonalDataEvent { 最終ブール値は有効です。 FormInputChanged(this.isValid); } class FormCorrect extends PersonalDataEvent { 最終的な PersonalData formData; FormCorrect(this.formData); }
フォームの変更をリッスンすることは非常に重要であるため、 FormInputChangedイベントを使用します。 フォームが正しい場合、 FormCorrectイベントが送信されます。
検証に関しては、プロバイダーと比較すると、ここに大きな違いがあります。 すべての検証ロジックをBLoCレイヤーに含めたい場合は、フィールドごとに多くのイベントが必要になります。 さらに、多くの州では、ビューに検証メッセージを表示する必要があります。
もちろん、これは可能ですが、 TextFormField API の利点を利用するのではなく、それとの戦いのようなものです。 したがって、明確な理由がない場合は、検証をビュー レイヤーに残すことができます。
ボタンの状態は、 BLoCによってビューに送信される状態によって異なります。
BlocBuilder<PersonalDataBloc, PersonalDataState>( ビルダー: (コンテキスト、状態) { RaisedButton を返す( 子: Text(Strings.addressNext), onPressed: 状態は NextButtonEnabled ? /* ボタン有効時の動作 */ : ヌル、 色: Colors.blue, disabledColor: Colors.grey, ); }))
イベント処理とPersonalDataBlocの状態へのマッピングは次のようになります。
@オーバーライド Stream<PersonalDataState> mapEventToState(PersonalDataEvent イベント) async* { if (イベントは FormCorrect) { yield InputFormCorrect(event.formData); } そうでなければ (イベントは FormInputChanged です) { yield mapFormInputChangedToState(イベント); } } PersonalDataState mapFormInputChangedToState(FormInputChanged イベント) { if (event.isValid) { NextButtonEnabled() を返します。 } そうしないと { NextButtonDisabled() を返します。 } }
個人データの要約を含む画面については、状況は前の例と似ています。 この画面に添付されたBLoCは、フォーム画面のBLoCからモデル情報を取得します。
クラス PersonalDataInfoBloc extends Bloc<PersonalDataInfoEvent, PersonalDataInfoState> { 最終的な PersonalDataBloc mainBloc; PersonalDataInfoBloc({@required this.mainBloc}) { mainBloc.listen((状態) { if (状態はInputFormCorrect) { add(PersonalDataInfoScreenStart(state.model)); } }); } @オーバーライド PersonalDataInfoState get initialState => InfoEmpty(); @オーバーライド Stream<PersonalDataInfoState> mapEventToState(PersonalDataInfoEvent イベント) async* { if (イベントは PersonalDataInfoScreenStart) { yield InfoLoaded(event.model); } } }
Flutter アーキテクチャ: 注意事項
上記の例は、2 つのアーキテクチャ間に明確な違いがあることを示すのに十分です。 BLoCは、ビュー レイヤーをビジネス ロジックから非常にうまく分離します。 これにより、再利用性とテスト容易性が向上します。 単純なケースを処理するには、 Providerよりも多くのコードを記述する必要があるようです。 ご存知のように、その場合、この Flutter アーキテクチャは、アプリケーションの複雑さが増すにつれて、より有用になります。
ビジネス用に未来志向のアプリを構築したいですか?
連絡しましょうまた、プロバイダーは UI をロジックから適切に分離し、ユーザー インタラクションごとに個別の状態の作成を強制しません。つまり、単純なケースを処理するために大量のコードを記述する必要がないことがよくあります。 しかし、これはより複雑なケースで問題を引き起こす可能性があります。
プロジェクト全体をチェックするには、ここをクリックしてください。