Flutter 架構:Provider vs BLoC

已發表: 2020-04-17

使用 Flutter 編寫應用程序為選擇架構創造了巨大的機會。 通常情況下,“我應該選擇哪一個?”這個問題的最佳答案。 是“視情況而定”。 當你得到這個答案時,你可以確定你找到了一位編程專家。

在本文中,我們將瀏覽移動應用程序中最流行的屏幕,並在兩種最流行的 Flutter 架構中實現它們: ProviderBLoC 。 因此,我們將了解每種解決方案的優缺點,這將幫助我們為下一個模塊或應用程序選擇正確的 Flutter 架構。

Flutter架構簡介

為 Flutter 開發項目選擇架構非常重要,這主要是因為我們正在處理一種不太常用的聲明式編程範式。 這完全改變了原生 Android 或 iOS 開發人員熟悉的管理狀態的方法,即強制編寫代碼。 在應用程序的一個地方可用的數據在另一個地方並不那麼容易獲得。 我們沒有直接引用樹中的其他視圖,我們可以從中獲得它們的當前狀態。

Flutter 中的 Provider 是什麼

顧名思義, Provider是一種Flutter架構,將當前的數據模型提供到我們當前需要的地方。 它包含一些數據並在發生更改時通知觀察者。 在 Flutter SDK 中,這種類型稱為ChangeNotifier 。 為了使ChangeNotifier類型的對象可用於其他小部件,我們需要ChangeNotifierProvider 。 它為其所有後代提供觀察對象。 能夠接收當前數據的對像是Consumer ,它在其構建函數的參數中有一個ChangeNotifier實例,可用於為後續視圖提供數據。

Flutter 中的 BLoC 是什麼

業務邏輯組件是一種 Flutter 架構,與移動領域的流行解決方案(如 MVP 或 MVVM)更相似。 它提供了表示層與業務邏輯規則的分離。 這是 Flutter 強烈強調的聲明式方法的直接應用,即UI = f (state) 。 BLoC 是來自用戶界面的事件所在的地方。 在這一層中,由於將業務規則應用於給定事件,BLoC 以特定狀態響應,然後返回到 UI。 當視圖層接收到一個新狀態時,它會根據當前狀態的需要重建它的視圖。

開發服務圖標

對 Flutter 開發感到好奇?

查看我們的解決方案

如何在 Flutter 中創建列表

可滾動列表可能是移動應用程序中最流行的視圖之一。 因此,在這裡選擇正確的 Flutter 架構可能至關重要。 從理論上講,顯示列表本身並不困難。 例如,當我們添加對每個元素執行特定操作的能力時,情況會變得更加棘手。 這應該會導致應用程序中不同位置的變化。 在我們的列表中,我們將能夠選擇每個元素,並且每個選定的元素都將顯示在不同屏幕上的單獨列表中。

在 Flutter 中創建列表

因此,我們必須存儲已選擇的元素,以便它們可以顯示在新屏幕上。 此外,每次點擊複選框時,我們都需要重建視圖,以實際顯示選中/取消選中。

列表項模型看起來很簡單:

 類社交媒體{
 內部標識;
 字符串標題;
 字符串圖標資產;
 布爾是最喜歡的;

 社交媒體(
     {@required this.id,
     @required this.title,
     @required this.iconAsset,
     this.isFavourite = false});

 無效 setFavourite(bool isFavourite) {
   this.isFavourite = isFavourite;
 }
}

如何使用 Provider 創建列表

在提供者模式中,上述模型必須存儲在一個對像中。 該對象應擴展ChangeNotifier以便能夠從應用程序中的另一個位置訪問SocialMedia

 類 SocialMediaModel 擴展 ChangeNotifier {
 final List<SocialMedia> _socialMedia = [ /* 一些社交媒體對象 */ ];

 UnmodifiableListView<SocialMedia> 獲得收藏{
   return UnmodifiableListView(_socialMedia.where((item) => item.isFavourite));
 }

 UnmodifiableListView<SocialMedia> 全部獲取 {
   返回 UnmodifiableListView(_socialMedia);
 }


無效 setFavourite(int itemId, bool isChecked) {
 _社交媒體
     .firstWhere((item) => item.id == itemId)
     .setFavourite(isChecked);
 通知監聽器();
}

此對像中需要在視圖上重建的任何更改都必須使用notifyListeners() 發出信號。 在setFavourite()方法指示 Flutter 重新渲染 UI 片段的情況下,它將觀察此對象的變化。

現在我們可以繼續創建列表。 要使用元素填充ListView ,我們需要訪問SocialMediaModel對象,該對象存儲所有元素的列表。 你可以通過兩種方式做到這一點:

  • Provider.of<ModelType>(上下文,聽:假)
  • 消費者

第一個提供觀察到的對象,並允許我們決定對對象執行的操作是否應該重建當前小部件,使用監聽參數。 這種行為在我們的例子中很有用。

 類 SocialMediaListScreen 擴展 StatelessWidget {
 社交媒體列表屏幕();

 @覆蓋
 小部件構建(BuildContext 上下文){
   var socialMedia = Provider.of<SocialMediaModel>(上下文,聽:假);

   返回列表視圖(
     兒童:socialMedia.all
         .map((item) => CheckboxSocialMediaItem(item: item))
         .toList(),
   );
 }
}

我們需要一份所有社交媒體的列表,但沒有必要重新構建整個列表。 讓我們看一下列表項小部件的外觀。

 類 CheckboxSocialMediaItem 擴展 StatelessWidget {
 最終社交媒體項目;

 CheckboxSocialMediaItem({Key key, @required this.item}) : super(key: key);

 @覆蓋
 小部件構建(BuildContext 上下文){
   返回填充(
     填充:常量 EdgeInsets.all(Dimens.paddingDefault),
     孩子:行(
       孩子們: [
         消費者<SocialMediaModel>(
           建造者:(上下文,模型,孩子){
             返回複選框(
               值:item.isFavourite,
               onChanged: (isChecked) =>
                   model.setFavourite(item.id, isChecked),
             );
           },
         ),
         社交媒體項目(
           項目:項目,
         )
       ],
     ),
   );
 }
}

我們監聽複選框值的變化,並根據檢查狀態更新模型。 複選框值本身是使用數據模型中的屬性設置的。 這意味著選擇後,模型會將isFavourite字段更改為true 。 但是,在我們重建複選框之前,視圖不會顯示此更改。 在這裡,一個Consumer對象提供了幫助。 它提供觀察對象並在收到有關模型更改的信息後重建其所有後代。

值得將Consumer僅放置在需要更新小部件的地方,以避免不必要的重建視圖。 請注意,例如,如果復選框選擇將觸發一些額外的操作,例如更改項目的標題,則必須將Consumer在小部件樹中移到更高的位置,以便成為負責顯示標題的小部件的父級. 否則,標題視圖將不會更新。

創建一個最喜歡的社交媒體屏幕看起來很相似。 我們將使用Provider獲得最喜歡的項目列表。

 類 FavouritesListScreen 擴展 StatelessWidget {
 收藏夾列表屏幕();

 @覆蓋
 小部件構建(BuildContext 上下文){
   var list = Provider.of<SocialMediaModel>(context, listen: false).favourites;

   返回列表視圖(
     孩子:列表
         .map((項目) => 填充(
             填充:常量 EdgeInsets.all(Dimens.paddingDefault),
             孩子:SocialMediaItem(項目:項目)))
         .toList(),
   );
 }
}

調用build方法時, Provider將返回當前收藏的社交媒體列表。

如何使用 BLoC 創建列表

在我們的簡單應用程序中,到目前為止我們有兩個屏幕。 它們每個都有自己的BLoC對象。 但是,請記住,主屏幕上的選定項目將出現在最喜歡的社交媒體列表中。 因此,我們必須以某種方式將復選框選擇事件轉移到屏幕之外。 解決方案是創建一個額外的BLoC對象,該對象將處理影響許多屏幕狀態的事件。 我們稱之為 global BLoC 。 然後,分配給各個屏幕的BLoC對象將監聽全局BLoC狀態的變化並做出相應的響應。

在創建BLoC對象之前,您應該首先考慮視圖將能夠發送哪些事件到 BLoC 層以及它將響應哪些狀態。 在全局BLoC的情況下,事件和狀態將如下所示:

 抽像類 SocialMediaEvent {}

類 CheckboxChecked 擴展 SocialMediaEvent {
 最終布爾 isChecked;
 最終 int itemId;

 CheckboxChecked(this.isChecked, this.itemId);
}


抽像類 SocialMediaState {}

類 ListPresented 擴展 SocialMediaState {
 最終列表<SocialMedia> 列表;

 ListPresented(this.list);
}

CheckboxChecked事件必須在全局BLoC中,因為它會影響許多屏幕的狀態——而不僅僅是一個。 當涉及到狀態時,我們有一個可以在其中顯示列表的狀態。 從全局BLoC的角度來看,沒有必要創建更多的狀態。 兩個屏幕都應該顯示該列表,並且專用於特定屏幕的各個BLoC應該處理它。 全局BLoC本身的實現將如下所示:

 類 SocialMediaBloc 擴展 Bloc<SocialMediaEvent, SocialMediaState> {
 最終的 SimpleSocialMediaRepository 存儲庫;

 社交媒體塊(this.repository);

 @覆蓋
 SocialMediaState 獲取初始狀態 => ListPresented(repository.getSocialMedia);

 @覆蓋
 Stream<SocialMediaState> mapEventToState(SocialMediaEvent 事件) async* {
   如果(事件為 CheckboxChecked){
     yield _mapCheckboxCheckedToState(event);
   }
 }

 SocialMediaState _mapCheckboxCheckedToState(CheckboxChecked 事件) {
   final updatedList = (state as ListPresented).list;
   更新列表
       .firstWhere((item) => item.id == event.itemId)
       .setFavourite(event.isChecked);
   返回列表呈現(更新列表);
 }
}

初始狀態是ListPresented——我們假設我們已經從存儲庫接收到數據。 我們只需要響應一個事件—— Ch​​eckboxChecked 。 因此,我們將使用setFavourite方法更新所選元素,並發送包裝在ListPresented狀態的新列表。

現在我們需要在點擊複選框時發送CheckboxChecked事件。 為此,我們需要在可以附加onChanged回調的地方創建一個SocialMediaBloc實例。 我們可以使用BlocProvider獲取這個實例——它看起來類似於上面討論的模式中的Provider 。 要使這樣的BlocProvider工作,在小部件樹的較高位置,您必須初始化所需的BLoC對象。 在我們的示例中,它將在 main 方法中完成:

 void main() => runApp(BlocProvider(
   創建:(上下文){
     返回社交媒體塊(SimpleSocialMediaRepository());
   },
   孩子:ArchitecturesSampleApp()));

多虧了這一點,在主列表代碼中,我們可以輕鬆地使用BlocProvider.of ()調用BLoC並使用add方法向它發送事件:

 類 SocialMediaListScreen 擴展 StatefulWidget {
 _SocialMediaListState createState() => _SocialMediaListState();
}

類 _SocialMediaListState 擴展 State<SocialMediaListScreen> {
 @覆蓋
 小部件構建(BuildContext 上下文){
   返回 BlocBuilder<SocialMediaListBloc, SocialMediaListState>(
     構建器:(上下文,狀態){
       如果(狀態為 MainListLoaded){
         返回列表視圖(
           兒童:state.socialMedia
               .map((項目) => CheckboxSocialMediaItem(
                     項目:項目,
                     onCheckboxChanged: (isChecked) =>
                         BlocProvider.of<SocialMediaBloc>(上下文)
                             .add(CheckboxChecked(isChecked, item.id)),
                   ))
               .toList(),
         );
       } 別的 {
         返回中心(子:文本(Strings.emptyList));
       }
     },
   );
 }
}

我們已經將CheckboxChecked事件傳播到BLoC ,我們也知道BLoC將如何響應此類事件。 但實際上……什麼會導致列表在已選中復選框的情況下重建? 全局BLoC不支持更改列表狀態,因為它由分配給屏幕的單個BLoC對象處理。 解決方案是前面提到的偵聽全局BLoC以更改狀態並根據此狀態進行響應。 下面,專門用於主要社交媒體列表的BLoC帶有一個複選框:

 類 SocialMediaListBloc
   擴展 Bloc<SocialMediaListEvent, SocialMediaListState> {
 最終社交媒體塊主塊;

 SocialMediaListBloc({@required this.mainBloc}) {
   mainBloc.listen((狀態){
     如果(狀態為 ListPresented){
       add(ScreenStart(state.list));
     }
   });
 }

 @覆蓋
 SocialMediaListState 獲取初始狀態 => MainListEmpty();

 @覆蓋
 流<SocialMediaListState> mapEventToState(
     SocialMediaListEvent 事件)異步* {
   開關(event.runtimeType){
     案例屏幕開始:
       yield MainListLoaded((event as ScreenStart).list);
       休息;
   }
 }
}

SocialMediaBloc返回狀態ListPresented時,將通知SocialMediaListBloc 。 請注意, ListPresented傳達了一個列表。 它包含有關使用複選框檢查項目的更新信息。

同樣,我們可以創建一個專門用於收藏夾社交媒體屏幕的BLoC

 類 FavouritesListBloc 擴展 Bloc<FavouritesListEvent, FavouritesListSate> {
 最終社交媒體塊主塊;

 FavouritesListBloc({@required this.mainBloc}) {
   mainBloc.listen((狀態){
     如果(狀態為 ListPresented){
       add(FavouritesScreenStart(state.list));
     }
   });
 }

 @覆蓋
 FavouritesListSate 獲取初始狀態 => FavouritesListEmpty();

 @覆蓋
 Stream<FavouritesListSate> mapEventToState(FavouritesListEvent 事件) async* {
   如果(事件是 FavouritesScreenStart){
     var favouritesList = event.list.where((item) => item.isFavourite).toList();
     產生收藏夾列表加載(收藏夾列表);
   }
 }
}

更改全局BLoC中的狀態會導致使用當前列表觸發FavouritesScreenStart事件。 然後,過濾掉標記為收藏的項目,並在屏幕上顯示這樣的列表。

如何在 Flutter 中創建包含多個字段的表單

長表單可能很棘手,尤其是當需求假設不同的驗證變體,或者輸入文本後屏幕上的一些變化時。 在示例屏幕上,我們有一個由多個字段和“下一步”按鈕組成的表單。 在表單完全有效之前,這些字段將自動驗證並禁用按鈕。 單擊按鈕後,將打開一個新屏幕,其中包含在表單中輸入的數據。

我們必須驗證每個字段並檢查整個表單更正以正確設置按鈕狀態。 然後,需要為下一個屏幕存儲收集到的數據。

在 Flutter 中創建包含多個字段的表單

如何使用 Provider 創建具有多個字段的表單

在我們的應用程序中,我們將需要第二個ChangeNotifier ,專用於個人信息屏幕。 因此,我們可以使用MultiProvider ,我們在其中提供ChangeNotifier對象列表。 它們將可供MultiProvider的所有後代使用。

 類 ArchitecturesSampleApp 擴展 StatelessWidget {
 最終的 SimpleSocialMediaRepository 存儲庫;

 ArchitecturesSampleApp({Key key, this.repository}) : super(key: key);

 @覆蓋
 小部件構建(BuildContext 上下文){
   返回多提供者(
     提供者:[
       ChangeNotifierProvider<SocialMediaModel>(
         創建:(上下文)=> SocialMediaModel(存儲庫),
       ),
       ChangeNotifierProvider<PersonalDataNotifier>(
         創建:(上下文)=> PersonalDataNotifier(),
       )
     ],
     孩子:MaterialApp(
       標題:Strings.architecturesSampleApp,
       debugShowCheckedModeBanner:假,
       主頁:開始屏幕(),
       路線:<String, WidgetBuilder>{
         Routes.socialMedia: (context) => SocialMediaScreen(),
         Routes.favourites: (context) => FavouritesScreen(),
         Routes.personalDataForm: (context) => PersonalDataScreen(),
         Routes.personalDataInfo: (context) => PersonalDataInfoScreen()
       },
     ),
   );
 }
}

在這種情況下, PersonalDataNotifier將充當業務邏輯層——他將驗證字段,訪問數據模型以進行更新,並更新視圖所依賴的字段。

表單本身是來自 Flutter 的一個非常好的 API,我們可以使用屬性驗證器自動附加驗證,並使用onSaved回調將表單中的數據保存到模型中。 我們會將驗證規則委託給PersonalDataNotifier ,當表單正確時,我們會將輸入的數據傳遞給它。

這個屏幕上最重要的事情是監聽每個字段的變化,並根據驗證結果啟用或禁用按鈕。 我們將使用來自Form對象的回調onChange 。 在其中,我們將首先檢查驗證狀態,然後將其傳遞給PersonalDataNotifier

 形式(
 鍵:_formKey,
 自動驗證:真,
 onChanged: () => _onFormChanged(personalDataNotifier),
 孩子:

無效_onFormChanged(PersonalDataNotifierpersonalDataNotifier){
 var isValid = _formKey.currentState.validate();
 個人數據通知器.onFormChanged(isValid);
}

PersonalDataNotifier中,我們將準備isFormValid變量。 我們將修改它(不要忘記調用notifyListeners() ),在視圖中,我們將根據其值更改按鈕狀態。 請記住使用參數listen: true來獲取Notifier實例——否則,我們的視圖將不會重建,按鈕狀態將保持不變。

 var personalDataNotifier = Provider.of<PersonalDataNotifier>(context, listen: true);

實際上,考慮到我們在其他地方使用了personalDataNotifier ,不需要重新加載視圖,上面的行不是最佳的,應該將listen參數設置為false 。 我們唯一想要重新加載的是按鈕,所以我們可以將它包裝在一個經典的Consumer中:

 消費者<PersonalDataNotifier>(
 builder: (context, notifier, child) {
   返回凸起按鈕(
     孩子:文本(Strings.addressNext),
     onPressed:notifier.isFormValid
         ? /* 按鈕啟用時的動作 */
         : 無效的,
     顏色:顏色.藍色,
     disabledColor:顏色.灰色,
   );
 },
)

多虧了這一點,我們不會在每次使用通知程序時強制其他組件重新加載。

在顯示個人數據的視圖中,不會再有任何問題——我們可以訪問PersonalDataNotifier並從那裡下載更新的模型。

如何使用 BLoC 創建具有多個字段的表單

對於上一個屏幕,我們需要兩個BLoC對象。 因此,當我們添加另一個“雙屏”時,我們將總共有四個。 與Provider的情況一樣,我們可以使用MultiBlocProvider來處理它,它的工作原理幾乎相同。

 無效 main() => runApp(
     MultiBlocProvider(提供者:[
       塊提供者(
         創建:(上下文)=> SocialMediaBloc(SimpleSocialMediaRepository()),
       ),
       塊提供者(
           創建:(上下文)=> SocialMediaListBloc(
               mainBloc: BlocProvider.of<SocialMediaBloc>(context))),
       塊提供者(
         創建:(上下文)=> PersonalDataBloc(),
       ),
       塊提供者(
         創建:(上下文)=> PersonalDataInfoBloc(
             mainBloc: BlocProvider.of<PersonalDataBloc>(context)),
       )
     ],孩子:ArchitecturesSampleApp()),
   );

BLoC模式一樣,最好從可能的狀態和操作開始。

 抽像類 PersonalDataState {}

類 NextButtonDisabled 擴展 PersonalDataState {}

類 NextButtonEnabled 擴展 PersonalDataState {}

類 InputFormCorrect 擴展 PersonalDataState {
 最終的 PersonalData 模型;

 InputFormCorrect(this.model);
}

此屏幕上發生的變化是按鈕狀態。 因此,我們需要單獨的狀態。 此外, InputFormCorrect狀態將允許我們發送表單收集的數據。

 抽像類 PersonalDataEvent {}

類 FormInputChanged 擴展 PersonalDataEvent {
 最終布爾是有效的;
 FormInputChanged(this.isValid);
}

類 FormCorrect 擴展 PersonalDataEvent {
 最終的個人數據表格數據;

 FormCorrect(this.formData);
}

監聽表單的變化是至關重要的,因此FormInputChanged事件。 當表單正確時,將發送FormCorrect事件。

在驗證方面,如果將其與 Provider 進行比較,這裡會有很大的不同。 如果我們想將所有驗證邏輯包含在BLoC層中,每個字段都會有很多事件。 此外,許多州會要求視圖顯示驗證消息。

這當然是可能的,但這就像與TextFormField API 的鬥爭,而不是利用它的好處。 因此,如果沒有明確的原因,您可以將驗證留在視圖層中。

按鈕狀態將取決於BLoC發送到視圖的狀態:

 BlocBuilder<PersonalDataBloc, PersonalDataState>(
   構建器:(上下文,狀態){
 返回凸起按鈕(
   孩子:文本(Strings.addressNext),
   onPressed:狀態為 NextButtonEnabled
       ? /* 按鈕啟用時的動作 */
       : 無效的,
   顏色:顏色.藍色,
   disabledColor:顏色.灰色,
 );
})

PersonalDataBloc中的事件處理和狀態映射如下:

 @覆蓋
Stream<PersonalDataState> mapEventToState(PersonalDataEvent 事件) async* {
 如果(事件是FormCorrect){
   產量 InputFormCorrect(event.formData);
 } else if (event is FormInputChanged) {
   產量 mapFormInputChangedToState(event);
 }
}

PersonalDataState mapFormInputChangedToState(FormInputChanged 事件) {
 如果(事件.isValid){
   返回 NextButtonEnabled();
 } 別的 {
   返回 NextButtonDisabled();
 }
}

至於帶有個人數據摘要的屏幕,情況與前面的示例類似。 附加到此屏幕的BLoC將從表單屏幕的BLoC中檢索模型信息。

 類 PersonalDataInfoBloc
   擴展 Bloc<PersonalDataInfoEvent, PersonalDataInfoState> {
 最終的 PersonalDataBloc 主塊;

 PersonalDataInfoBloc({@required this.mainBloc}) {
   mainBloc.listen((狀態){
     如果(狀態為 InputFormCorrect){
       添加(PersonalDataInfoScreenStart(state.model));
     }
   });
 }

 @覆蓋
 PersonalDataInfoState 獲取初始狀態 => InfoEmpty();

 @覆蓋
 Stream<PersonalDataInfoState> mapEventToState(PersonalDataInfoEvent 事件) async* {
   如果(事件是 PersonalDataInfoScreenStart){
     產量 InfoLoaded(event.model);
   }
 }
}

Flutter 架構:要記住的注意事項

上面的例子足以表明兩種架構之間存在明顯的差異。 BLoC 很好地將視圖層與業務邏輯分離。 這需要更好的可重用性和可測試性。 似乎要處理簡單的情況,您需要編寫比Provider更多的代碼。 如您所知,在這種情況下,隨著應用程序複雜性的增加,這種 Flutter 架構將變得更加有用。

帶走圖標

想為您的企業構建面向未來的應用程序嗎?

讓我們取得聯繫

Provider還將 UI 與邏輯很好地分離,不會強制為每個用戶交互創建單獨的狀態,這意味著您通常不必編寫大量代碼來處理簡單的案例。 但這可能會在更複雜的情況下導致問題。

點擊這裡查看整個項目。