Arquitetura Flutter: Provedor vs BLoC
Publicados: 2020-04-17Escrever aplicativos com Flutter cria grandes oportunidades para escolher arquitetura. Como é frequentemente o caso, a melhor resposta para a pergunta “Qual devo escolher?” é “Depende”. Quando você obtiver essa resposta, pode ter certeza de que encontrou um especialista em programação.
Neste artigo, percorreremos as telas mais populares em aplicativos móveis e as implementaremos nas duas arquiteturas Flutter mais populares: Provider e BLoC . Como resultado, aprenderemos os prós e contras de cada solução, o que nos ajudará a escolher a arquitetura Flutter certa para nosso próximo módulo ou aplicativo.
Breve introdução à arquitetura Flutter
A escolha da arquitetura para um projeto de desenvolvimento Flutter é de grande importância, principalmente devido ao fato de estarmos lidando com um paradigma de programação declarativa menos utilizado. Isso muda completamente a abordagem de gerenciamento do estado com o qual os desenvolvedores nativos de Android ou iOS estavam familiarizados, escrevendo o código de forma imperativa. Os dados disponíveis em um local do aplicativo não são tão fáceis de obter em outro. Não temos referências diretas a outras visualizações na árvore, das quais poderíamos obter seu estado atual.
O que é provedor em Flutter
Como o nome sugere, Provider é uma arquitetura Flutter que fornece o modelo de dados atual para o local onde atualmente precisamos dele. Ele contém alguns dados e notifica os observadores quando ocorre uma mudança. No Flutter SDK, esse tipo é chamado de ChangeNotifier . Para que o objeto do tipo ChangeNotifier esteja disponível para outros widgets, precisamos de ChangeNotifierProvider . Ele fornece objetos observados para todos os seus descendentes. O objeto que pode receber os dados atuais é Consumer , que possui uma instância ChangeNotifier no parâmetro de sua função de construção que pode ser usada para alimentar visualizações subsequentes com dados.
O que é BLoC em Flutter
O Business Logic Components é uma arquitetura Flutter muito mais semelhante a soluções populares em dispositivos móveis, como MVP ou MVVM. Ele fornece a separação da camada de apresentação das regras de lógica de negócios. Esta é uma aplicação direta da abordagem declarativa que o Flutter enfatiza fortemente, ou seja, UI = f (state) . BLoC é um lugar para onde vão os eventos da interface do usuário. Dentro desta camada, como resultado da aplicação de regras de negócio a um determinado evento, o BLoC responde com um estado específico, que então volta para a UI. Quando a camada de visualização recebe um novo estado, ela reconstrói sua visualização de acordo com o que o estado atual exige.
Curioso sobre o desenvolvimento do Flutter?
Veja nossas soluçõesComo criar uma lista no Flutter
Uma lista rolável é provavelmente uma das visualizações mais populares em aplicativos móveis. Portanto, escolher a arquitetura Flutter correta pode ser crucial aqui. Teoricamente, exibir a lista em si não é difícil. A situação fica mais complicada quando, por exemplo, adicionamos a capacidade de realizar uma determinada ação em cada elemento. Isso deve causar uma mudança em diferentes locais do aplicativo. Em nossa lista, poderemos selecionar cada um dos elementos, e cada um dos selecionados será exibido em uma lista separada em uma tela diferente.

Portanto, temos que armazenar os elementos que foram selecionados, para que possam ser exibidos em uma nova tela. Além disso, precisaremos reconstruir a visualização toda vez que a caixa de seleção for tocada, para realmente mostrar a seleção/desmarcação.
O modelo de item de lista parece muito simples:
classe Mídias Sociais { int id; Título da string; String iconAsset; bool éFavorito; Mídia social( {@required this.id, @required this.title, @required this.iconAsset, this.isFavorito = false}); void setFavourite(bool isFavourite) { this.isFavourite = isFavourite; } }
Como criar uma lista com o Provider
No padrão Provider, o modelo acima deve ser armazenado em um objeto. O objeto deve estender o ChangeNotifier para poder acessar o SocialMedia de outro local no aplicativo.
class SocialMediaModel estende ChangeNotifier { final List<SocialMedia> _socialMedia = [ /* alguns objetos de mídia social */ ]; UnmodifiableListView<SocialMedia> obter favoritos { return UnmodifiableListView(_socialMedia.where((item) => item.isFavourite)); } UnmodifiableListView<SocialMedia> obtém todos { return UnmodifiableListView(_socialMedia); } void setFavourite(int itemId, bool isChecked) { _mídia social .firstWhere((item) => item.id == itemId) .setFavourite(isChecked); notificarOuvintes(); }
Qualquer alteração neste objeto, que exigirá reconstrução na visão, deve ser sinalizada usando notifyListeners() . No caso do método setFavourite() para instruir o Flutter a renderizar novamente o fragmento da interface do usuário, isso observará a alteração neste objeto.
Agora podemos seguir para a criação da lista. Para preencher o ListView com elementos, precisaremos chegar ao objeto SocialMediaModel , que armazena uma lista de todos os elementos. Você pode fazer de duas maneiras:
- Provider.of<ModelType>(contexto, escuta: false)
- Consumidor
O primeiro fornece o objeto observado e nos permite decidir se a ação realizada no objeto deve reconstruir o widget atual, usando o parâmetro listen . Este comportamento será útil no nosso caso.
class SocialMediaListScreen estende StatelessWidget { SocialMediaListScreen(); @sobrepor Construção de widget (contexto BuildContext) { var socialMedia = Provider.of<SocialMediaModel>(context, listen: false); return ListView( crianças: socialMedia.all .map((item) => CheckboxSocialMediaItem(item: item)) .listar(), ); } }
Precisamos de uma lista de todas as mídias sociais, mas não há necessidade de reconstruir a lista inteira. Vamos dar uma olhada na aparência do widget de item de lista.
class CheckboxSocialMediaItem estende StatelessWidget { último item de mídia social; CheckboxSocialMediaItem({Key key, @required this.item}) : super(key: key); @sobrepor Construção de widget (contexto BuildContext) { return Preenchimento( preenchimento: const EdgeInsets.all(Dimens.paddingDefault), filho: linha( crianças: [ Consumidor<SocialMediaModel>( construtor: (contexto, modelo, filho) { retornar Caixa de seleção( valor: item.isFavorito, onChanged: (isChecked) => model.setFavourite(item.id, isChecked), ); }, ), SocialMediaItem( artigo: artigo, ) ], ), ); } }
Ouvimos a mudança no valor da caixa de seleção e atualizamos o modelo com base no estado da seleção. O próprio valor da caixa de seleção é definido usando a propriedade do modelo de dados. Isso significa que após a seleção, o modelo alterará o campo isFavourite para true . No entanto, a exibição não apresentará essa alteração até que reconstruamos a caixa de seleção. Aqui, um objeto Consumer vem com ajuda. Ele fornece o objeto observado e reconstrói todos os seus descendentes após receber informações sobre a mudança no modelo.
Vale a pena colocar Consumer apenas onde for necessário atualizar o widget para evitar visualizações de reconstrução desnecessárias. Observe que se, por exemplo, a seleção da caixa de seleção acionar alguma ação adicional, como alterar o título do item, o Consumidor teria que ser movido para cima na árvore do widget, para se tornar o pai do widget responsável por exibir o título . Caso contrário, a visualização do título não será atualizada.
Criar uma tela de mídia social favorita será semelhante. Obteremos uma lista de itens favoritos usando Provider .
class FavouritesListScreen estende StatelessWidget { FavoritosListaTela(); @sobrepor Construção de widget (contexto BuildContext) { var list = Provider.of<SocialMediaModel>(context, listen: false).favourites; return ListView( filhos: lista .map((item) => Preenchimento( preenchimento: const EdgeInsets.all(Dimens.paddingDefault), filho: SocialMediaItem(item: item))) .listar(), ); } }
Quando o método build for chamado, o Provider retornará a lista atual de mídias sociais favoritas.
Como criar uma lista com BLoC
Em nosso aplicativo simples, temos duas telas até o momento. Cada um deles terá seu próprio objeto BLoC . No entanto, lembre-se de que os itens selecionados na tela principal devem aparecer na lista de redes sociais favoritas. Portanto, devemos de alguma forma transferir eventos de seleção de checkbox para fora da tela. A solução é criar um objeto BLoC adicional que manipulará eventos que afetam o estado de muitas telas. Vamos chamá-lo de BLoC global. Em seguida, os objetos BLoC atribuídos a telas individuais ouvirão as alterações nos estados globais do BLoC e responderão de acordo.
Antes de criar um objeto BLoC , você deve primeiro pensar em quais eventos a visualização poderá enviar para a camada BLoC e em quais estados ela responderá. No caso de BLoC global, os eventos e estados serão os seguintes:
classe abstrata SocialMediaEvent {} class CheckboxChecked estende SocialMediaEvent { bool final isChecked; final int itemId; CheckboxChecked(this.isChecked, this.itemId); } classe abstrata SocialMediaState {} class ListPresented estende SocialMediaState { lista final List<SocialMedia>; ListPresented(this.list); }
O evento CheckboxChecked deve estar no BLoC global, pois afetará o estado de muitas telas – não apenas uma. Quando se trata de estados, temos um em que a lista está pronta para ser exibida. Do ponto de vista do BLoC global, não há necessidade de criar mais estados. Ambas as telas devem exibir a lista e os BLoCs individuais dedicados à tela específica devem cuidar disso. A implementação do próprio BLoC global ficará assim:
class SocialMediaBloc estende Bloc<SocialMediaEvent, SocialMediaState> { repositório final SimpleSocialMediaRepository; SocialMediaBloc(este.repositório); @sobrepor SocialMediaState get initialState => ListPresented(repository.getSocialMedia); @sobrepor Stream<SocialMediaState> mapEventToState(SocialMediaEvent event) assíncrono* { if (o evento é CheckboxChecked) { yield _mapCheckboxCheckedToState(evento); } } SocialMediaState _mapCheckboxCheckedToState(evento CheckboxChecked) { final updatedList = (estado como ListPresented).list; Lista atualizada .firstWhere((item) => item.id == event.itemId) .setFavourite(event.isChecked); return ListPresented(updatedList); } }
O estado inicial é ListPresented – assumimos que já recebemos dados do repositório. Só precisamos responder a um evento – CheckboxChecked . Portanto, atualizaremos o elemento selecionado usando o método setFavourite e enviaremos a nova lista envolvida no estado ListPresented .
Agora precisamos enviar o evento CheckboxChecked ao tocar na caixa de seleção. Para fazer isso, precisaremos de uma instância de SocialMediaBloc em um local onde possamos anexar o callback onChanged . Podemos obter esta instância usando BlocProvider - parece semelhante ao Provider do padrão discutido acima. Para que tal BlocProvider funcione, mais alto na árvore de widgets, você deve inicializar o objeto BLoC desejado. No nosso exemplo, isso será feito no método main:
void main() => runApp(BlocProvider( criar: (contexto) { return SocialMediaBloc(SimpleSocialMediaRepository()); }, filho: ArchitecturesSampleApp()));
Graças a isso, no código da lista principal, podemos facilmente chamar BLoC usando BlocProvider.of() e enviar um evento para ele usando o método add :
class SocialMediaListScreen estende StatefulWidget { _SocialMediaListState createState() => _SocialMediaListState(); } class _SocialMediaListState estende State<SocialMediaListScreen> { @sobrepor Construção de widget (contexto BuildContext) { return BlocBuilder<SocialMediaListBloc, SocialMediaListState>( construtor: (contexto, estado) { if (estado é MainListLoaded) { return ListView( filhos: estado.socialMedia .map((item) => CheckboxSocialMediaItem( artigo: artigo, onCheckboxChanged: (isChecked) => BlocProvider.of<SocialMediaBloc>(contexto) .add(CheckboxChecked(isChecked, item.id)), )) .listar(), ); } senão { return Center(filho: Text(Strings.emptyList)); } }, ); } }
Já temos a propagação do evento CheckboxChecked para BLoC , também sabemos como BLoC responderá a tal evento. Mas na verdade… o que fará com que a lista seja reconstruída com a caixa de seleção já marcada? O Global BLoC não oferece suporte à alteração de estados de lista, pois é tratado por objetos BLoC individuais atribuídos a telas. A solução é ouvir um BLoC global para alterar o estado e responder de acordo com esse estado. Abaixo, o BLoC dedicado à lista das principais redes sociais com checkbox:

class SocialMediaListBloc extends Bloc<SocialMediaListEvent, SocialMediaListState> { final SocialMediaBloc mainBloc; SocialMediaListBloc({@required this.mainBloc}) { mainBloc.listen((estado) { if (estado é ListPresented) { add(ScreenStart(state.list)); } }); } @sobrepor SocialMediaListState get initialState => MainListEmpty(); @sobrepor Stream<SocialMediaListState> mapEventToState( evento SocialMediaListEvent) assíncrono* { switch (evento.runtimeType) { caso ScreenStart: yield MainListLoaded((evento como ScreenStart).list); parar; } } }
Quando o SocialMediaBloc retornar o estado ListPresented , o SocialMediaListBloc será notificado. Observe que ListPresented transmite uma lista. É aquele que contém informações atualizadas sobre como marcar o item com a caixa de seleção.
Da mesma forma, podemos criar um BLoC dedicado à tela de mídia social favorita:
class FavouritesListBloc estende Bloc<FavouritesListEvent, FavouritesListSate> { final SocialMediaBloc mainBloc; FavoritesListBloc({@required this.mainBloc}) { mainBloc.listen((estado) { if (estado é ListPresented) { add(FavoritesScreenStart(state.list)); } }); } @sobrepor FavouritesListSate get initialState => FavouritesListEmpty(); @sobrepor Stream<FavouritesListSate> mapEventToState(FavouritesListEvent event) assíncrono* { if (evento é FavoritosScreenStart) { var lista de favoritos = event.list.where((item) => item.isFavourite).toList(); yieldLista de FavoritosCarregado(Lista de Favoritos); } } }
Alterar o estado no BLoC global resulta no disparo do evento FavoritesScreenStart com a lista atual. Em seguida, os itens marcados como favoritos são filtrados e essa lista é exibida na tela.
Como criar um formulário com muitos campos no Flutter
Formulários longos podem ser complicados, especialmente quando os requisitos assumem diferentes variantes de validação ou algumas alterações na tela após a inserção do texto. Na tela de exemplo, temos um formulário composto por vários campos e o botão “PRÓXIMO”. Os campos serão validados automaticamente e o botão desativado até que o formulário seja totalmente válido. Após clicar no botão, uma nova tela será aberta com os dados inseridos no formulário.
Temos que validar cada campo e verificar toda a correção do formulário para definir corretamente o estado do botão. Em seguida, os dados coletados precisarão ser armazenados para a próxima tela.

Como criar um formulário com muitos campos com o Provider
Em nosso aplicativo, precisaremos de um segundo ChangeNotifier , dedicado às telas de informações pessoais. Podemos, portanto, usar o MultiProvider , onde fornecemos uma lista de objetos ChangeNotifier . Eles estarão disponíveis para todos os descendentes de MultiProvider .
class ArchitecturesSampleApp estende StatelessWidget { repositório final SimpleSocialMediaRepository; ArchitecturesSampleApp({Key key, this.repository}) : super(key: key); @sobrepor Construção de widget (contexto BuildContext) { return MultiProvedor( fornecedores: [ ChangeNotifierProvider<SocialMediaModel>( criar: (contexto) => SocialMediaModel(repositório), ), ChangeNotifierProvider<PersonalDataNotifier>( criar: (contexto) => PersonalDataNotifier(), ) ], filho: MaterialApp( título: Strings.architecturesSampleApp, debugShowCheckedModeBanner: false, home: TelaInicial(), rotas: <String, WidgetBuilder>{ Routes.socialMedia: (contexto) => SocialMediaScreen(), Rotas.favoritos: (contexto) => Tela de Favoritos(), Routes.personalDataForm: (contexto) => PersonalDataScreen(), Routes.personalDataInfo: (contexto) => PersonalDataInfoScreen() }, ), ); } }
Nesse caso, PersonalDataNotifier estará atuando como uma camada de lógica de negócios – ele estará validando campos, tendo acesso ao modelo de dados para sua atualização e atualizando os campos dos quais a visualização dependerá.
O formulário em si é uma API muito legal do Flutter, onde podemos anexar validações automaticamente usando o validador de propriedades e salvar os dados do formulário no modelo usando o callback onSaved . Delegaremos regras de validação ao PersonalDataNotifier e quando o formulário estiver correto, passaremos os dados inseridos para ele.
O mais importante nesta tela será escutar uma mudança em cada campo e habilitar ou desabilitar o botão, dependendo do resultado da validação. Usaremos o callback onChange do objeto Form . Nele, primeiro verificaremos o status de validação e depois o passaremos para PersonalDataNotifier .
Forma( chave: _formKey, autovalidate: verdadeiro, onChanged: () => _onFormChanged(personalDataNotifier), filho: void _onFormChanged(PersonalDataNotifier personalDataNotifier) { var éValido = _formKey.currentState.validate(); personalDataNotifier.onFormChanged(isValid); }
Em PersonalDataNotifier , vamos preparar a variável isFormValid . Vamos modificá-lo (não esqueça de chamar notifyListeners() ) e na view, vamos mudar o estado do botão dependendo do seu valor. Lembre-se de obter a instância do Notifier com o parâmetro listen: true – caso contrário, nossa view não será reconstruída e o estado do botão permanecerá inalterado.
var personalDataNotifier = Provider.of<PersonalDataNotifier>(context, listen: true);
Na verdade, dado o fato de usarmos personalDataNotifier em outros lugares, onde não é necessário recarregar a view, a linha acima não é a ideal e deve ter o parâmetro listen definido como false . A única coisa que queremos recarregar é o botão, para que possamos envolvê-lo em um Consumer clássico:
Consumidor<PersonalDataNotifier>( construtor: (contexto, notificador, filho) { return Botão Levantado( filho: Text(Strings.addressNext), onPressed: notifier.isFormValid ? /* ação quando o botão está habilitado */ : nulo, cor: Colors.blue, disabledColor: Colors.grey, ); }, )
Graças a isso, não forçamos outros componentes a recarregar cada vez que usamos um notificador.
Na visualização de dados pessoais, não haverá mais problemas – temos acesso ao PersonalDataNotifier e, a partir daí, podemos baixar o modelo atualizado.
Como criar um formulário com muitos campos com BLoC
Para a tela anterior precisávamos de dois objetos BLoC . Então, quando adicionarmos outra “tela dupla”, teremos quatro ao todo. Como no caso de Provider , podemos lidar com isso com MultiBlocProvider , que funciona de forma quase idêntica.
void main() => runApp( MultiBlocProvider(provedores: [ BlocProvedor( criar: (contexto) => SocialMediaBloc(SimpleSocialMediaRepository()), ), BlocProvedor( criar: (contexto) => SocialMediaListBloc( mainBloc: BlocProvider.of<SocialMediaBloc>(contexto))), BlocProvedor( criar: (contexto) => PersonalDataBloc(), ), BlocProvedor( criar: (contexto) => PersonalDataInfoBloc( mainBloc: BlocProvider.of<PersonalDataBloc>(contexto)), ) ], filho: ArchitecturesSampleApp()), );
Como no padrão BLoC , é melhor começar com os possíveis estados e ações.
classe abstrata PersonalDataState {} classe NextButtonDisabled estende PersonalDataState {} classe NextButtonEnabled estende PersonalDataState {} class InputFormCorrect estende PersonalDataState { modelo final de dados pessoais; InputFormCorrect(this.model); }
O que está mudando nesta tela é o estado do botão. Precisamos, portanto, de estados separados para isso. Além disso, o estado InputFormCorrect nos permitirá enviar os dados coletados pelo formulário.
classe abstrata PersonalDataEvent {} class FormInputChanged estende PersonalDataEvent { bool final éVálido; FormInputChanged(this.isValid); } class FormCorrect estende PersonalDataEvent { formulário final de Dados PessoaisDados; FormCorrect(this.formData); }
Ouvir as alterações no formulário é crucial, daí o evento FormInputChanged . Quando o formulário estiver correto, o evento FormCorrect será enviado.
Quando se trata de validações, há uma grande diferença aqui se você comparar com o Provider. Se quisermos incluir toda a lógica de validação na camada BLoC , teríamos muitos eventos para cada um dos campos. Além disso, muitos estados exigiriam que a exibição mostrasse mensagens de validação.
Claro que isso é possível, mas seria como uma luta contra a API TextFormField em vez de usar seus benefícios. Portanto, se não houver motivos claros, você pode deixar as validações na camada de visualização.
O estado do botão dependerá do estado enviado para a visualização por BLoC :
BlocBuilder<PersonalDataBloc, PersonalDataState>( construtor: (contexto, estado) { return Botão Levantado( filho: Text(Strings.addressNext), onPressed: o estado é NextButtonEnabled ? /* ação quando o botão está habilitado */ : nulo, cor: Colors.blue, disabledColor: Colors.grey, ); })
A manipulação de eventos e o mapeamento para estados em PersonalDataBloc serão os seguintes:
@sobrepor Stream<PersonalDataState> mapEventToState(PersonalDataEvent event) assíncrono* { if (o evento é FormCorrect) { yield InputFormCorrect(event.formData); } else if (o evento é FormInputChanged) { rendimento mapFormInputChangedToState(evento); } } PersonalDataState mapFormInputChangedToState(evento FormInputChanged) { if (evento.éválido) { return NextButtonEnabled(); } senão { return PróximoBotãoDesativado(); } }
Quanto à tela com resumo dos dados pessoais, a situação é semelhante ao exemplo anterior. O BLoC anexado a esta tela recuperará as informações do modelo do BLoC da tela do formulário.
classe PersonalDataInfoBloc extends Bloc<PersonalDataInfoEvent, PersonalDataInfoState> { final PersonalDataBloc mainBloc; PersonalDataInfoBloc({@required this.mainBloc}) { mainBloc.listen((estado) { if (estado é InputFormCorrect) { add(PersonalDataInfoScreenStart(estado.modelo)); } }); } @sobrepor PersonalDataInfoState get initialState => InfoEmpty(); @sobrepor Stream<PersonalDataInfoState> mapEventToState(PersonalDataInfoEvent event) assíncrono* { if (o evento é PersonalDataInfoScreenStart) { rendimento InfoCarregado(evento.modelo); } } }
Arquitetura Flutter: notas para lembrar
Os exemplos acima são suficientes para mostrar que existem diferenças claras entre as duas arquiteturas. BLoC separa muito bem a camada de visualização da lógica de negócios. Isso implica uma melhor reutilização e testabilidade. Parece que para lidar com casos simples, você precisa escrever mais código do que em Provider . Como você sabe, nesse caso, essa arquitetura Flutter se tornará mais útil à medida que a complexidade do aplicativo aumentar.
Quer construir um aplicativo orientado para o futuro para o seu negócio?
Vamos entrar em contatoO provedor também separa bem a interface do usuário da lógica e não força a criação de estados separados com cada interação do usuário, o que significa que muitas vezes você não precisa escrever uma grande quantidade de código para lidar com um caso simples. Mas isso pode causar problemas em casos mais complexos.
Clique aqui para conferir todo o projeto.