Arhitectură Flutter: Provider vs BLoC
Publicat: 2020-04-17Scrierea de aplicații cu Flutter creează oportunități grozave de a alege arhitectura. Așa cum este adesea cazul, cel mai bun răspuns la întrebarea „Pe care ar trebui să aleg?” este „Depinde”. Când primești răspunsul, poți fi sigur că ai găsit un expert în programare.
În acest articol, vom parcurge cele mai populare ecrane din aplicațiile mobile și le vom implementa în cele mai populare două arhitecturi Flutter: Provider și BLoC . Ca urmare, vom afla avantajele și dezavantajele fiecărei soluții, ceea ce ne va ajuta să alegem arhitectura Flutter potrivită pentru următorul nostru modul sau aplicație.
Scurtă introducere în arhitectura Flutter
Alegerea arhitecturii pentru un proiect de dezvoltare Flutter este de mare importanță, în primul rând datorită faptului că avem de-a face cu o paradigmă de programare declarativă mai puțin folosită. Acest lucru schimbă complet abordarea de a gestiona starea cu care dezvoltatorii nativi Android sau iOS erau familiarizați, scriind codul în mod imperativ. Datele disponibile într-un loc în aplicație nu sunt atât de ușor de obținut în altul. Nu avem referiri directe la alte vederi din arbore, din care am putea câștiga starea lor actuală.
Ce este Provider în Flutter
După cum sugerează și numele, Provider este o arhitectură Flutter care oferă modelul de date actual în locul în care avem nevoie de el. Conține unele date și anunță observatorii când are loc o schimbare. În Flutter SDK, acest tip este numit ChangeNotifier . Pentru ca obiectul de tip ChangeNotifier să fie disponibil pentru alte widget-uri, avem nevoie de ChangeNotifierProvider . Oferă obiecte observate pentru toți descendenții săi. Obiectul care poate primi date curente este Consumer , care are o instanță ChangeNotifier în parametrul funcției de compilare care poate fi folosită pentru a alimenta vizualizările ulterioare cu date.
Ce este BLoC în Flutter
Business Logic Components este o arhitectură Flutter mult mai asemănătoare cu soluțiile populare din mobil, cum ar fi MVP sau MVVM. Oferă separarea stratului de prezentare de regulile logicii de afaceri. Aceasta este o aplicare directă a abordării declarative pe care Flutter o subliniază puternic, adică UI = f (stare) . BLoC este un loc unde au loc evenimentele din interfața cu utilizatorul. În acest strat, ca urmare a aplicării regulilor de afaceri la un anumit eveniment, BLoC răspunde cu o stare specifică, care apoi se întoarce la UI. Când stratul de vizualizare primește o nouă stare, își reconstruiește vederea în funcție de ceea ce necesită starea curentă.
Ești curios despre dezvoltarea Flutter?
Vezi soluțiile noastreCum se creează o listă în Flutter
O listă derulabilă este probabil una dintre cele mai populare vizualizări în aplicațiile mobile. Prin urmare, alegerea arhitecturii Flutter potrivite ar putea fi crucială aici. Teoretic, afișarea listei în sine nu este dificilă. Situația devine mai complicată când, de exemplu, adăugăm capacitatea de a efectua o anumită acțiune asupra fiecărui element. Acest lucru ar trebui să provoace o schimbare în diferite locuri din aplicație. În lista noastră, vom putea selecta fiecare dintre elemente, iar fiecare dintre cele selectate va fi afișat într-o listă separată pe un ecran diferit.

Prin urmare, trebuie să stocăm elementele care au fost selectate, astfel încât acestea să poată fi afișate pe un nou ecran. În plus, va trebui să reconstruim vizualizarea de fiecare dată când se atinge caseta de selectare, pentru a afișa de fapt bifarea/debifarea.
Modelul articolului din listă arată foarte simplu:
clasa SocialMedia { int id; Titlul șirului; String iconAsset; bool este Favorit; Social Media( {@required this.id, @required this.title, @required this.iconAsset, this.isFavourite = fals}); void setFavourite(bool este Favorit) { this.isFavourite = este Favorit; } }
Cum se creează o listă cu furnizorul
În modelul Provider, modelul de mai sus trebuie să fie stocat într-un obiect. Obiectul ar trebui să extindă ChangeNotifier pentru a putea accesa SocialMedia dintr-un alt loc din aplicație.
clasa SocialMediaModel extinde ChangeNotifier { final List<SocialMedia> _socialMedia = [ /* unele obiecte de social media */ ]; UnmodifiableListView<SocialMedia> obține favorite { returnează UnmodiableListView(_socialMedia.where((item) => item.isFavourite)); } UnmodiableListView<SocialMedia> obțineți toate { returnează UnmodiableListView(_socialMedia); } void setFavourite(int itemId, bool isChecked) { _socialMedia .firstWhere((articol) => item.id == itemId) .setFavourite(este Verificat); notifyListeners(); }
Orice modificare a acestui obiect, care va necesita reconstruirea vizualizării, trebuie semnalată folosind notifyListeners() . În cazul metodei setFavourite() pentru a instrui Flutter să redea fragmentul UI, aceasta va observa modificarea acestui obiect.
Acum putem trece la crearea listei. Pentru a umple ListView cu elemente, va trebui să ajungem la obiectul SocialMediaModel , care stochează o listă cu toate elementele. O poți face în două moduri:
- Provider.of<ModelType>(context, ascultați: fals)
- Consumator
Primul oferă obiectul observat și ne permite să decidem dacă acțiunea efectuată asupra obiectului ar trebui să reconstruiască widget-ul curent, folosind parametrul listen . Acest comportament va fi util în cazul nostru.
clasa SocialMediaListScreen extinde StatelessWidget { SocialMediaListScreen(); @trece peste Creare widget (context BuildContext) { var socialMedia = Provider.of<SocialMediaModel>(context, asculta: false); returnează ListView( copii: socialMedia.all .map((articol) => Casetă de selectareSocialMediaItem(articol: articol)) .a lista(), ); } }
Avem nevoie de o listă cu toate rețelele sociale, dar nu este nevoie să reconstruim întreaga listă. Să aruncăm o privire la cum arată widget-ul elementului din listă.
clasa CheckboxSocialMediaItem extinde StatelessWidget { articol final SocialMedia; CheckboxSocialMediaItem({Cheie, @necesar acest.articol}): super(cheie: cheie); @trece peste Creare widget (context BuildContext) { returnează umplutura ( umplutură: const EdgeInsets.all(Dimens.paddingDefault), copil: rând( copii: [ Consumator<SocialMediaModel>( constructor: (context, model, copil) { casetă de selectare return ( valoare: item.isFavourite, onChanged: (isChecked) => model.setFavourite(item.id, isChecked), ); }, ), SocialMediaItem( item: item, ) ], ), ); } }
Ascultăm modificarea valorii casetei de selectare și actualizăm modelul în funcție de starea de verificare. Valoarea casetei de selectare în sine este setată folosind proprietatea din modelul de date. Aceasta înseamnă că după selecție, modelul va schimba câmpul isFavourite la true . Cu toate acestea, vizualizarea nu va prezenta această modificare până când vom reconstrui caseta de selectare. Aici, un obiect Consumer vine cu ajutor. Acesta furnizează obiectul observat și reconstruiește toți descendenții săi după ce primește informații despre schimbarea modelului.
Merită să plasați Consumer numai acolo unde este necesar să actualizați widget-ul pentru a evita vizualizările de reconstrucție inutile. Vă rugăm să rețineți că, dacă, de exemplu, selectarea casetei de selectare va declanșa o acțiune suplimentară, cum ar fi schimbarea titlului articolului, Consumatorul ar trebui să fie mutat mai sus în arborele widget, astfel încât să devină părintele widget-ului responsabil pentru afișarea titlului. . În caz contrar, vizualizarea titlului nu va fi actualizată.
Crearea unui ecran de social media preferat va arăta similar. Vom primi o listă de articole preferate folosind Provider .
clasa FavoritesListScreen extinde StatelessWidget { FavoriteListScreen(); @trece peste Creare widget (context BuildContext) { var list = Provider.of<SocialMediaModel>(context, listen: false).favourites; returnează ListView( copii: lista .map((articol) => Umplutură( umplutură: const EdgeInsets.all(Dimens.paddingDefault), copil: SocialMediaItem(articol: articol))) .a lista(), ); } }
Când metoda de compilare este apelată, Furnizorul va returna lista curentă a rețelelor sociale favorite.
Cum se creează o listă cu BLoC
În aplicația noastră simplă, avem două ecrane până acum. Fiecare dintre ele va avea propriul său obiect BLoC . Cu toate acestea, rețineți că elementele selectate de pe ecranul principal urmează să apară pe lista rețelelor sociale favorite. Prin urmare, trebuie să transferăm cumva evenimentele de selectare a casetei de selectare în afara ecranului. Soluția este de a crea un obiect BLoC suplimentar care va gestiona evenimentele care afectează starea multor ecrane. Să-i spunem BLoC global. Apoi, obiectele BLoC alocate ecranelor individuale vor asculta modificările stărilor globale BLoC și vor răspunde în consecință.
Înainte de a crea un obiect BLoC , ar trebui mai întâi să vă gândiți la ce evenimente va putea trimite vizualizarea către stratul BLoC și la ce stări va răspunde. În cazul BLoC global, evenimentele și stările vor fi după cum urmează:
clasa abstractă SocialMediaEvent {} clasa CheckboxChecked extinde SocialMediaEvent { bool final este Verificat; final int itemId; CheckboxChecked(this.isChecked, this.itemId); } clasa abstractă SocialMediaState {} clasa ListPresented extinde SocialMediaState { Lista finală<SocialMedia>; ListPresented(this.list); }
Evenimentul CheckboxChecked trebuie să fie în BLoC global, deoarece va afecta starea multor ecrane – nu doar a unuia. Când vine vorba de state, avem una în care lista este gata de afișat. Din punctul de vedere al BLoC global, nu este nevoie să se creeze mai multe state. Ambele ecrane ar trebui să afișeze lista și BLoC -urile individuale dedicate ecranului specific ar trebui să aibă grijă de ea. Implementarea BLoC globală în sine va arăta astfel:
clasa SocialMediaBloc extinde Bloc<SocialMediaEvent, SocialMediaState> { depozitul final SimpleSocialMediaRepository; SocialMediaBloc(this.repository); @trece peste SocialMediaState get initialState => ListPresented(repository.getSocialMedia); @trece peste Flux<SocialMediaState> mapEventToState(eveniment SocialMediaEvent) asincron* { if (evenimentul este CheckboxChecked) { randament _mapCheckboxCheckedToState(eveniment); } } SocialMediaState _mapCheckboxCheckedToState(eveniment CheckboxChecked) { final updatedList = (stare ca ListPresented).list; lista actualizată .firstWhere((element) => item.id == event.itemId) .setFavourite(event.isChecked); returnează ListPresented(Lista actualizată); } }
Starea inițială este ListPresented – presupunem că am primit deja date din depozit. Trebuie să răspundem la un singur eveniment – CheckboxChecked . Deci vom actualiza elementul selectat folosind metoda setFavourite și vom trimite noua listă înfășurată în stare ListPresented .
Acum trebuie să trimitem evenimentul CheckboxChecked când atingem caseta de selectare. Pentru a face acest lucru, vom avea nevoie de o instanță de SocialMediaBloc într-un loc unde putem atașa apelul invers onChanged . Putem obține această instanță folosind BlocProvider – arată similar cu Provider din modelul discutat mai sus. Pentru ca un astfel de BlocProvider să funcționeze, mai sus în arborele widget, trebuie să inițializați obiectul BLoC dorit. În exemplul nostru, se va face în metoda principală:
void main() => runApp(BlocProvider( creați: (context) { returnează SocialMediaBloc(SimpleSocialMediaRepository()); }, copil: ArchitecturesSampleApp()));
Datorită acestui fapt, în codul listei principale, putem apela cu ușurință BLoC folosind BlocProvider.of () și îi putem trimite un eveniment folosind metoda add :
clasa SocialMediaListScreen extinde StatefulWidget { _SocialMediaListState createState() => _SocialMediaListState(); } clasa _SocialMediaListState extinde State<SocialMediaListScreen> { @trece peste Creare widget (context BuildContext) { returnează BlocBuilder<SocialMediaListBloc, SocialMediaListState>( constructor: (context, stare) { if (starea este MainListLoaded) { returnează ListView( copii: stat.socialMedia .map((element) => Caseta de selectareSocialMediaItem( item: item, onCheckboxChanged: (isChecked) => BlocProvider.of<SocialMediaBloc>(context) .add(CheckboxChecked(isChecked, item.id)), )) .a lista(), ); } altfel { return Center(copil: Text(Strings.emptyList)); } }, ); } }
Avem deja propagarea evenimentului CheckboxChecked către BLoC , știm și cum va răspunde BLoC la un astfel de eveniment. Dar de fapt... ce va face ca lista să se reconstruiască cu caseta de selectare deja selectată? BLoC global nu acceptă modificarea stărilor listei, deoarece este gestionat de obiecte BLoC individuale alocate ecranelor. Soluția este ascultarea unui BLoC global menționat anterior pentru schimbarea stării și răspunsul în funcție de această stare. Mai jos, BLoC dedicat listei principale de social media cu o casetă de selectare:

clasa SocialMediaListBloc extinde Bloc<SocialMediaListEvent, SocialMediaListState> { final SocialMediaBloc mainBloc; SocialMediaListBloc({@required this.mainBloc}) { mainBloc.listen((state) { if (starea este ListPresented) { add(ScreenStart(state.list)); } }); } @trece peste SocialMediaListState get initialState => MainListEmpty(); @trece peste Stream<SocialMediaListState> mapEventToState( eveniment SocialMediaListEvent) asincron* { comutare (event.runtimeType) { case ScreenStart: randament MainListLoaded((eveniment ca ScreenStart).list); pauză; } } }
Când SocialMediaBloc returnează starea ListPresented , SocialMediaListBloc va fi notificat. Rețineți că ListPresented transmite o listă. Este cel care conține informații actualizate despre verificarea articolului cu caseta de selectare.
În mod similar, putem crea un BLoC dedicat ecranului de social media preferat:
clasa FavoritesListBloc extinde Bloc<FavouritesListEvent, FavouritesListSate> { final SocialMediaBloc mainBloc; FavoritesListBloc({@required this.mainBloc}) { mainBloc.listen((state) { if (starea este ListPresented) { add(FavouritesScreenStart(state.list)); } }); } @trece peste FavoritesListSate get initialState => FavoriteListEmpty(); @trece peste Stream<FavouritesListSate> mapEventToState(eveniment FavouritesListEvent) asincron* { if (evenimentul este FavoritesScreenStart) { var favoritesList = event.list.where((articol) => item.isFavourite).toList(); randament FavoritesListLoaded(favouritesList); } } }
Schimbarea stării în BLoC global are ca rezultat declanșarea evenimentului FavouritesScreenStart cu lista curentă. Apoi, elementele marcate ca favorite sunt filtrate și o astfel de listă se afișează pe ecran.
Cum se creează un formular cu multe câmpuri în Flutter
Formele lungi pot fi dificile, mai ales când cerințele presupun diferite variante de validare, sau unele modificări pe ecran după introducerea textului. Pe ecranul exemplu, avem un formular format din mai multe câmpuri și butonul „URMĂTOR”. Câmpurile vor fi validate automat și butonul va fi dezactivat până când formularul este complet valabil. După ce faceți clic pe butonul, se va deschide un nou ecran cu datele introduse în formular.
Trebuie să validăm fiecare câmp și să verificăm întreaga corecție a formularului pentru a seta corect starea butonului. Apoi, datele colectate vor trebui stocate pentru următorul ecran.

Cum se creează un formular cu multe câmpuri cu Provider
În aplicația noastră, vom avea nevoie de un al doilea ChangeNotifier , dedicat ecranelor cu informații personale. Prin urmare, putem folosi MultiProvider , unde oferim o listă de obiecte ChangeNotifier . Acestea vor fi disponibile pentru toți descendenții MultiProvider .
clasa ArchitecturesSampleApp extinde StatelessWidget { depozitul final SimpleSocialMediaRepository; ArchitecturesSampleApp({Cheie, acest.repozitiv}): super(cheie: cheie); @trece peste Creare widget (context BuildContext) { returnează MultiProvider( furnizori: [ ChangeNotifierProvider<SocialMediaModel>( creați: (context) => SocialMediaModel (depozitar), ), ChangeNotifierProvider<PersonalDataNotifier>( creați: (context) => PersonalDataNotifier(), ) ], copil: MaterialApp( titlu: Strings.architecturesSampleApp, debugShowCheckedModeBanner: false, home: StartScreen(), rute: <String, WidgetBuilder>{ Routes.socialMedia: (context) => SocialMediaScreen(), Routes.favourites: (context) => FavouritesScreen(), Routes.personalDataForm: (context) => PersonalDataScreen(), Routes.personalDataInfo: (context) => PersonalDataInfoScreen() }, ), ); } }
În acest caz, PersonalDataNotifier va acționa ca un strat logic de business – va valida câmpuri, va avea acces la modelul de date pentru actualizarea acestuia și va actualiza câmpurile de care va depinde vizualizarea.
Formularul în sine este un API foarte drăguț de la Flutter, unde putem atașa automat validări folosind validatorul de proprietăți și putem salva datele din formular în model folosind callback-ul onSaved . Vom delega reguli de validare către PersonalDataNotifier și atunci când formularul este corect, îi vom transmite datele introduse.
Cel mai important lucru de pe acest ecran va fi ascultarea unei modificări în fiecare câmp și activarea sau dezactivarea butonului, în funcție de rezultatul validării. Vom folosi callback onChange din obiectul Form . În ea, vom verifica mai întâi starea de validare și apoi o vom transmite PersonalDataNotifier .
Formă( cheie: _formKey, autovalidare: adevărat, onChanged: () => _onFormChanged(personalDataNotifier), copil: void _onFormChanged(PersonalDataNotifier personalDataNotifier) { var isValid = _formKey.currentState.validate(); personalDataNotifier.onFormChanged(isValid); }
În PersonalDataNotifier , vom pregăti variabila isFormValid . O vom modifica (nu uitați să apelați notifyListeners() ) și în vizualizare, vom schimba starea butonului în funcție de valoarea acestuia. Nu uitați să obțineți instanța Notifier cu parametrul listen: true – în caz contrar, vizualizarea noastră nu se va reconstrui și starea butonului va rămâne neschimbată.
var personalDataNotifier = Provider.of<PersonalDataNotifier>(context, ascultare: adevărat);
De fapt, dat fiind faptul că folosim personalDataNotifier în alte locuri, unde reîncărcarea vizualizării nu este necesară, linia de mai sus nu este optimă și ar trebui să aibă parametrul listen setat la false . Singurul lucru pe care vrem să-l reîncărcăm este butonul, așa că îl putem împacheta într-un Consumer clasic :
Consumer<PersonalDataNotifier>( constructor: (context, notificator, copil) { returnează RaisedButton( copil: Text(Strings.addressNext), onPressed: notifier.isFormValid ? /* acțiune când butonul este activat */ : nul, culoare: Culori.albastru, disabledColor: Colors.grey, ); }, )
Datorită acestui fapt, nu forțăm alte componente să se reîncarce de fiecare dată când folosim un notificator.
În vizualizarea care afișează datele personale, nu vor mai fi probleme – avem acces la PersonalDataNotifier și de acolo, putem descărca modelul actualizat.
Cum se creează un formular cu multe câmpuri cu BLoC
Pentru ecranul anterior aveam nevoie de două obiecte BLoC . Deci, când adăugăm un alt „ecran dublu”, vom avea patru în total. Ca și în cazul Provider , ne putem descurca cu MultiBlocProvider , care funcționează aproape identic.
void main() => runApp( MultiBlocProvider(furnizori: [ BlocProvider( creați: (context) => SocialMediaBloc(SimpleSocialMediaRepository()), ), BlocProvider( creați: (context) => SocialMediaListBloc( mainBloc: BlocProvider.of<SocialMediaBloc>(context))), BlocProvider( creați: (context) => PersonalDataBloc(), ), BlocProvider( creați: (context) => PersonalDataInfoBloc( mainBloc: BlocProvider.of<PersonalDataBloc>(context)), ) ], copil: ArchitecturesSampleApp()), );
Ca și în modelul BLoC , cel mai bine este să începeți cu stările și acțiunile posibile.
clasa abstractă PersonalDataState {} clasa NextButtonDisabled extinde PersonalDataState {} clasa NextButtonEnabled extinde PersonalDataState {} clasa InputFormCorrect extinde PersonalDataState { modelul final de date personale; InputFormCorrect(this.model); }
Ceea ce se schimbă pe acest ecran este starea butonului. Prin urmare, avem nevoie de state separate pentru aceasta. În plus, starea InputFormCorrect ne va permite să trimitem datele colectate de formular.
clasa abstractă PersonalDataEvent {} clasa FormInputChanged extinde PersonalDataEvent { bool final este Valid; FormInputChanged(this.isValid); } clasa FormCorrect extinde PersonalDataEvent { formularul final de date cu caracter personal; FormCorrect(this.formData); }
Ascultarea modificărilor din formular este crucială, de unde evenimentul FormInputChanged . Când formularul este corect, evenimentul FormCorrect va fi trimis.
Când vine vorba de validări, există o mare diferență aici dacă o compari cu Provider. Dacă am dori să includem toată logica de validare în stratul BLoC , am avea o mulțime de evenimente pentru fiecare dintre câmpuri. În plus, multe state ar necesita vizualizarea pentru a afișa mesaje de validare.
Acest lucru este, desigur, posibil, dar ar fi ca o luptă împotriva API-ului TextFormField în loc să-și folosească beneficiile. Prin urmare, dacă nu există motive clare, puteți lăsa validări în stratul de vizualizare.
Starea butonului va depinde de starea trimisă la vizualizare de către BLoC :
BlocBuilder<PersonalDataBloc, PersonalDataState>( constructor: (context, stare) { returnează RaisedButton( copil: Text(Strings.addressNext), onPressed: starea este NextButtonEnabled ? /* acțiune când butonul este activat */ : nul, culoare: Culori.albastru, disabledColor: Colors.grey, ); })
Gestionarea evenimentelor și maparea stărilor în PersonalDataBloc va fi după cum urmează:
@trece peste Flux<PersonalDataState> mapEventToState(eveniment PersonalDataEvent) asincron* { if (evenimentul este FormCorrect) { randament InputFormCorrect(event.formData); } else if (evenimentul este FormInputChanged) { randament mapFormInputChangedToState(eveniment); } } PersonalDataState mapFormInputChangedToState(eveniment FormInputChanged) { if (event.isValid) { returnează NextButtonEnabled(); } altfel { returnează NextButtonDisabled(); } }
În ceea ce privește ecranul cu un rezumat al datelor personale, situația este similară cu exemplul anterior. BLoC atașat la acest ecran va prelua informații despre model din BLoC din ecranul formularului.
clasa PersonalDataInfoBloc extinde Bloc<PersonalDataInfoEvent, PersonalDataInfoState> { final PersonalDataBloc mainBloc; PersonalDataInfoBloc({@required this.mainBloc}) { mainBloc.listen((state) { if (starea este InputFormCorrect) { add(PersonalDataInfoScreenStart(state.model)); } }); } @trece peste PersonalDataInfoState get initialState => InfoEmpty(); @trece peste Flux<PersonalDataInfoState> mapEventToState(eveniment PersonalDataInfoEvent) asincron* { if (evenimentul este PersonalDataInfoScreenStart) { randament InfoLoaded(event.model); } } }
Arhitectura flutter: note de reținut
Exemplele de mai sus sunt suficiente pentru a arăta că există diferențe clare între cele două arhitecturi. BLoC separă foarte bine stratul de vizualizare de logica de afaceri. Acest lucru implică o mai bună reutilizare și testabilitate. Se pare că pentru a gestiona cazuri simple, trebuie să scrieți mai mult cod decât în Provider . După cum știți, în acest caz, această arhitectură Flutter va deveni mai utilă pe măsură ce crește complexitatea aplicației.
Doriți să construiți o aplicație orientată spre viitor pentru afacerea dvs.?
Să luăm legăturaDe asemenea, furnizorul separă bine interfața de utilizare de logică și nu forțează crearea de stări separate cu fiecare interacțiune a utilizatorului, ceea ce înseamnă că adesea nu trebuie să scrieți o cantitate mare de cod pentru a gestiona un caz simplu. Dar acest lucru poate cauza probleme în cazuri mai complexe.
Faceți clic aici pentru a vedea întregul proiect.