Flutter 架構:Provider vs BLoC
已發表: 2020-04-17使用 Flutter 編寫應用程序為選擇架構創造了巨大的機會。 通常情況下,“我應該選擇哪一個?”這個問題的最佳答案。 是“視情況而定”。 當你得到這個答案時,你可以確定你找到了一位編程專家。
在本文中,我們將瀏覽移動應用程序中最流行的屏幕,並在兩種最流行的 Flutter 架構中實現它們: Provider和BLoC 。 因此,我們將了解每種解決方案的優缺點,這將幫助我們為下一個模塊或應用程序選擇正確的 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 架構可能至關重要。 從理論上講,顯示列表本身並不困難。 例如,當我們添加對每個元素執行特定操作的能力時,情況會變得更加棘手。 這應該會導致應用程序中不同位置的變化。 在我們的列表中,我們將能夠選擇每個元素,並且每個選定的元素都將顯示在不同屏幕上的單獨列表中。

因此,我們必須存儲已選擇的元素,以便它們可以顯示在新屏幕上。 此外,每次點擊複選框時,我們都需要重建視圖,以實際顯示選中/取消選中。
列表項模型看起來很簡單:
類社交媒體{ 內部標識; 字符串標題; 字符串圖標資產; 布爾是最喜歡的; 社交媒體( {@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——我們假設我們已經從存儲庫接收到數據。 我們只需要響應一個事件—— CheckboxChecked 。 因此,我們將使用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 中創建包含多個字段的表單
長表單可能很棘手,尤其是當需求假設不同的驗證變體,或者輸入文本後屏幕上的一些變化時。 在示例屏幕上,我們有一個由多個字段和“下一步”按鈕組成的表單。 在表單完全有效之前,這些字段將自動驗證並禁用按鈕。 單擊按鈕後,將打開一個新屏幕,其中包含在表單中輸入的數據。
我們必須驗證每個字段並檢查整個表單更正以正確設置按鈕狀態。 然後,需要為下一個屏幕存儲收集到的數據。

如何使用 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 與邏輯很好地分離,不會強制為每個用戶交互創建單獨的狀態,這意味著您通常不必編寫大量代碼來處理簡單的案例。 但這可能會在更複雜的情況下導致問題。
點擊這裡查看整個項目。