Arquitectura Flutter: Proveedor vs BLoC
Publicado: 2020-04-17Escribir aplicaciones con Flutter crea grandes oportunidades para elegir la arquitectura. Como suele ser el caso, la mejor respuesta a la pregunta "¿Cuál debo elegir?" es "depende". Cuando obtenga esa respuesta, puede estar seguro de que encontró a un experto en programación.
En este artículo, repasaremos las pantallas más populares en las aplicaciones móviles y las implementaremos en las dos arquitecturas de Flutter más populares: Provider y BLoC . Como resultado, aprenderemos los pros y los contras de cada solución, lo que nos ayudará a elegir la arquitectura de Flutter adecuada para nuestro próximo módulo o aplicación.
Breve introducción a la arquitectura Flutter
Elegir la arquitectura para un proyecto de desarrollo de Flutter es de gran importancia, principalmente debido al hecho de que estamos tratando con un paradigma de programación declarativa menos utilizado. Esto cambia por completo el enfoque para administrar el estado con el que estaban familiarizados los desarrolladores nativos de Android o iOS, escribiendo el código de manera imperativa. Los datos disponibles en un lugar de la aplicación no son tan fáciles de obtener en otro. No tenemos referencias directas a otras vistas en el árbol, de las cuales podríamos obtener su estado actual.
¿Qué es el proveedor en Flutter?
Como sugiere el nombre, Provider es una arquitectura de Flutter que proporciona el modelo de datos actual al lugar donde lo necesitamos actualmente. Contiene algunos datos y notifica a los observadores cuando ocurre un cambio. En Flutter SDK, este tipo se denomina ChangeNotifier . Para que el objeto de tipo ChangeNotifier esté disponible para otros widgets, necesitamos ChangeNotifierProvider . Proporciona objetos observados para todos sus descendientes. El objeto que puede recibir datos actuales es Consumer , que tiene una instancia de ChangeNotifier en el parámetro de su función de compilación que se puede usar para alimentar vistas posteriores con datos.
¿Qué es BLoC en Flutter?
Business Logic Components es una arquitectura de Flutter mucho más similar a las soluciones populares en dispositivos móviles como MVP o MVVM. Proporciona separación de la capa de presentación de las reglas de lógica empresarial. Esta es una aplicación directa del enfoque declarativo que Flutter enfatiza fuertemente, es decir, UI = f (state) . BLoC es un lugar donde van los eventos de la interfaz de usuario. Dentro de esta capa, como resultado de la aplicación de reglas comerciales a un evento determinado, BLoC responde con un estado específico, que luego regresa a la interfaz de usuario. Cuando la capa de vista recibe un nuevo estado, reconstruye su vista de acuerdo con lo que requiere el estado actual.
¿Tienes curiosidad por el desarrollo de Flutter?
Vea nuestras solucionesCómo crear una lista en Flutter
Una lista desplazable es probablemente una de las vistas más populares en las aplicaciones móviles. Por lo tanto, elegir la arquitectura de Flutter correcta puede ser crucial aquí. Teóricamente, mostrar la lista en sí no es difícil. La situación se vuelve más complicada cuando, por ejemplo, agregamos la capacidad de realizar una determinada acción en cada elemento. Eso debería causar un cambio en diferentes lugares de la aplicación. En nuestra lista, podremos seleccionar cada uno de los elementos, y cada uno de los seleccionados se mostrará en una lista separada en una pantalla diferente.

Por lo tanto, tenemos que almacenar elementos que han sido seleccionados, para que puedan mostrarse en una nueva pantalla. Además, tendremos que reconstruir la vista cada vez que se toque la casilla de verificación, para mostrar realmente marcar/desmarcar.
El modelo de elemento de lista parece muy simple:
clase redes sociales { identificación interna; Título de cadena; Cadena iconAsset; bool es Favorito; Redes sociales( {@requerido este.id, @requerido este.título, @requirió este.iconAsset, this.isFavourite = false}); void setFavorito(bool esFavorito) { this.isFavourite = isFavourite; } }
Cómo crear una lista con Provider
En el patrón de proveedor, el modelo anterior debe almacenarse en un objeto. El objeto debe extender ChangeNotifier para poder acceder a SocialMedia desde otro lugar en la aplicación.
clase SocialMediaModel extiende ChangeNotifier { final List<SocialMedia> _socialMedia = [ /* algunos objetos de redes sociales */ ]; UnmodifiableListView<SocialMedia> obtener favoritos { return UnmodifiableListView(_socialMedia.where((item) => item.isFavourite)); } UnmodifiableListView<SocialMedia> obtener todos { volver UnmodificableListView(_socialMedia); } void setFavourite(int itemId, bool isChecked) { _redes sociales .firstwhere((elemento) => elemento.id == elementoId) .setFavourite(isChecked); notificar a los oyentes (); }
Cualquier cambio en este objeto, que requerirá la reconstrucción en la vista, debe señalarse mediante notificarListeners() . En el caso del método setFavourite() para indicarle a Flutter que vuelva a renderizar el fragmento de la interfaz de usuario, eso observará el cambio en este objeto.
Ahora podemos pasar a crear la lista. Para llenar ListView con elementos, necesitaremos acceder al objeto SocialMediaModel , que almacena una lista de todos los elementos. Puedes hacerlo de dos formas:
- Provider.of<ModelType>(contexto, escuchar: falso)
- Consumidor
El primero proporciona el objeto observado y nos permite decidir si la acción realizada en el objeto debe reconstruir el widget actual, utilizando el parámetro de escucha . Este comportamiento será útil en nuestro caso.
class SocialMediaListScreen extiende StatelessWidget { Pantalla de lista de medios sociales (); @anular Compilación del widget (contexto BuildContext) { var socialMedia = Provider.of<SocialMediaModel>(context, listen: false); devolver Vista de lista ( niños: socialMedia.all .map((elemento) => CheckboxSocialMediaItem(elemento: elemento)) .Listar(), ); } }
Necesitamos una lista de todas las redes sociales, pero no es necesario reconstruir la lista completa. Echemos un vistazo a cómo se ve el widget de elemento de lista.
clase CheckboxSocialMediaItem extiende StatelessWidget { elemento final de SocialMedia; CheckboxSocialMediaItem({Clave clave, @requerido este.elemento}) : super(clave: clave); @anular Compilación del widget (contexto BuildContext) { Relleno de retorno ( relleno: const EdgeInsets.all(Dimens.paddingDefault), hijo: Fila( niños: [ Consumidor<modelo de redes sociales>( constructor: (contexto, modelo, niño) { casilla de verificación de retorno ( valor: item.isFavourite, onChanged: (está marcado) => modelo.setFavourite(item.id, isChecked), ); }, ), Elemento de redes sociales( artículo: artículo, ) ], ), ); } }
Escuchamos el cambio en el valor de la casilla de verificación y actualizamos el modelo según el estado de verificación. El valor de la casilla de verificación en sí se establece mediante la propiedad del modelo de datos. Esto significa que después de la selección, el modelo cambiará el campo isFavourite a true . Sin embargo, la vista no presentará este cambio hasta que reconstruyamos la casilla de verificación. Aquí, un objeto Consumer viene con ayuda. Proporciona el objeto observado y reconstruye todos sus descendientes después de recibir información sobre el cambio en el modelo.
Vale la pena colocar Consumer solo donde sea necesario actualizar el widget para evitar vistas de reconstrucción innecesarias. Tenga en cuenta que si, por ejemplo, la selección de la casilla activará alguna acción adicional, como cambiar el título del artículo, el Consumidor deberá moverse más arriba en el árbol de widgets, para convertirse en el padre del widget responsable de mostrar el título. . De lo contrario, la vista del título no se actualizará.
Crear una pantalla de redes sociales favorita se verá similar. Obtendremos una lista de artículos favoritos usando Provider .
class FavouritesListScreen extiende StatelessWidget { Pantalla de lista de favoritos (); @anular Compilación del widget (contexto BuildContext) { var list = Provider.of<SocialMediaModel>(context, listen: false).favoritos; devolver Vista de lista ( niños: lista .map((elemento) => Relleno( relleno: const EdgeInsets.all(Dimens.paddingDefault), niño: SocialMediaItem (elemento: elemento))) .Listar(), ); } }
Cuando se llama al método de compilación , el proveedor devolverá la lista actual de redes sociales favoritas.
Cómo crear una lista con BLoC
En nuestra aplicación simple, tenemos dos pantallas hasta ahora. Cada uno de ellos tendrá su propio objeto BLoC . Sin embargo, tenga en cuenta que los elementos seleccionados en la pantalla principal aparecerán en la lista de redes sociales favoritas. Por lo tanto, de alguna manera debemos transferir los eventos de selección de casillas de verificación fuera de la pantalla. La solución es crear un objeto BLoC adicional que maneje los eventos que afectan el estado de muchas pantallas. Llamémoslo BLoC global. Luego, los objetos BLoC asignados a pantallas individuales escucharán los cambios en los estados globales de BLoC y responderán en consecuencia.
Antes de crear un objeto BLoC , primero debe pensar qué eventos podrá enviar la vista a la capa BLoC y a qué estados responderá. En el caso de BLoC global, los eventos y estados serán los siguientes:
clase abstracta SocialMediaEvent {} clase CheckboxChecked extiende SocialMediaEvent { bool final está marcado; ID de elemento int final; CheckboxChecked(this.isChecked, this.itemId); } clase abstracta SocialMediaState {} clase ListPresented extiende SocialMediaState { Lista final<SocialMedia> lista; ListaPresentada(esta.lista); }
El evento CheckboxChecked debe estar en el BLoC global, porque afectará el estado de muchas pantallas, no solo una. Cuando se trata de estados, tenemos uno en el que la lista está lista para mostrarse. Desde el punto de vista global de BLoC , no hay necesidad de crear más estados. Ambas pantallas deben mostrar la lista y los BLoC individuales dedicados a la pantalla específica deben encargarse de ello. La implementación del propio BLoC global se verá así:
clase SocialMediaBloc extiende Bloc<SocialMediaEvent, SocialMediaState> { repositorio final de SimpleSocialMediaRepository; SocialMediaBloc(este.repositorio); @anular SocialMediaState get initialState => ListPresented(repository.getSocialMedia); @anular Stream<SocialMediaState> mapEventToState(SocialMediaEvent event) async* { si (el evento es CheckboxChecked) { rendimiento _mapCheckboxCheckedToState(evento); } } SocialMediaState _mapCheckboxCheckedToState(evento CheckboxChecked) { final updatedList = (estado como ListPresented).list; lista actualizada .firstwhere((elemento) => elemento.id == evento.itemId) .setFavourite(event.isChecked); return ListPresented(actualizadoLista); } }
El estado inicial es ListPresented : asumimos que ya hemos recibido datos del repositorio. Solo necesitamos responder a un evento: CheckboxChecked . Así que actualizaremos el elemento seleccionado usando el método setFavourite y enviaremos la nueva lista envuelta en el estado ListPresented .
Ahora necesitamos enviar el evento CheckboxChecked al tocar la casilla de verificación. Para hacer esto, necesitaremos una instancia de SocialMediaBloc en un lugar donde podamos adjuntar la devolución de llamada onChanged . Podemos obtener esta instancia usando BlocProvider : se parece a Provider del patrón discutido anteriormente. Para que tal BlocProvider funcione, más arriba en el árbol de widgets, debe inicializar el objeto BLoC deseado. En nuestro ejemplo, se hará en el método principal:
void principal() => ejecutarAplicación(BlocProvider( crear: (contexto) { return SocialMediaBloc(SimpleSocialMediaRepository()); }, hijo: ArchitecturesSampleApp()));
Gracias a esto, en el código de la lista principal, podemos llamar fácilmente a BLoC usando BlocProvider.of () y enviarle un evento usando el método de agregar :
clase SocialMediaListScreen extiende StatefulWidget { _SocialMediaListState createState() => _SocialMediaListState(); } class _SocialMediaListState extiende State<SocialMediaListScreen> { @anular Compilación del widget (contexto BuildContext) { return BlocBuilder<SocialMediaListBloc, SocialMediaListState>( constructor: (contexto, estado) { if (el estado es MainListLoaded) { devolver Vista de lista ( niños: state.socialMedia .map((elemento) => CheckboxSocialMediaItem( artículo: artículo, onCheckboxChanged: (está marcado) => BlocProvider.of<SocialMediaBloc>(contexto) .add(CheckboxChecked(isChecked, item.id)), )) .Listar(), ); } más { Centro de retorno (hijo: Texto (Strings.emptyList)); } }, ); } }
Ya tenemos la propagación de eventos CheckboxChecked a BLoC , también sabemos cómo responderá BLoC a tal evento. Pero en realidad... ¿qué hará que la lista se reconstruya con la casilla de verificación ya seleccionada? Global BLoC no admite el cambio de estado de la lista, porque lo manejan los objetos BLoC individuales asignados a las pantallas. La solución es lo mencionado anteriormente escuchando un BLoC global para cambiar el estado y responder de acuerdo a este estado. A continuación, el BLoC dedicado a la lista principal de redes sociales con una casilla de verificación:

clase SocialMediaListBloc extiende Bloc<SocialMediaListEvent, SocialMediaListState> { final SocialMediaBloc mainBloc; SocialMediaListBloc({@requerido this.mainBloc}) { mainBloc.listen((estado) { si (el estado es ListPresented) { add(ScreenStart(estado.lista)); } }); } @anular SocialMediaListState get initialState => MainListEmpty(); @anular Stream<SocialMediaListState> mapEventToState( evento SocialMediaListEvent) asíncrono* { interruptor (evento. tipo de tiempo de ejecución) { caso ScreenStart: yield MainListLoaded((evento como ScreenStart).list); descanso; } } }
Cuando SocialMediaBloc devuelve el estado ListPresented , SocialMediaListBloc será notificado. Tenga en cuenta que ListPresented transmite una lista. Es el que contiene información actualizada sobre cómo marcar el elemento con la casilla de verificación.
Del mismo modo, podemos crear un BLoC dedicado a la pantalla de redes sociales favoritas:
class BlocListaFavoritos extends Bloc<EventoListaFavoritos, EstadoListaFavoritos> { final SocialMediaBloc mainBloc; FavoritosListBloc({@requerido this.mainBloc}) { mainBloc.listen((estado) { si (el estado es ListPresented) { add(FavouritesScreenStart(estado.lista)); } }); } @anular FavoritosListaEstado get estadoInicial => FavoritosListaVacío(); @anular Stream<FavoritesListSate> mapEventToState(FavouritesListEvent event) async* { if (el evento es FavoritesScreenStart) { var favouritesList = event.list.where((item) => item.isFavourite).toList(); yield Lista de favoritos cargada (lista de favoritos); } } }
Cambiar el estado en el BLoC global da como resultado que se active el evento FavouritesScreenStart con la lista actual. Luego, los elementos que uno marca como favoritos se filtran y dicha lista se muestra en la pantalla.
Cómo crear un formulario con muchos campos en Flutter
Los formularios largos pueden ser complicados, especialmente cuando los requisitos asumen diferentes variantes de validación, o algunos cambios en la pantalla después de ingresar el texto. En la pantalla de ejemplo, tenemos un formulario que consta de varios campos y el botón "SIGUIENTE". Los campos se validarán automáticamente y el botón se desactivará hasta que el formulario sea completamente válido. Después de hacer clic en el botón, se abrirá una nueva pantalla con los datos ingresados en el formulario.
Tenemos que validar cada campo y verificar la corrección completa del formulario para configurar correctamente el estado del botón. Luego, los datos recopilados deberán almacenarse para la siguiente pantalla.

Cómo crear un formulario con muchos campos con Provider
En nuestra aplicación, necesitaremos un segundo ChangeNotifier , dedicado a las pantallas de información personal. Por lo tanto, podemos usar MultiProvider , donde proporcionamos una lista de objetos ChangeNotifier . Estarán disponibles para todos los descendientes de MultiProvider .
clase ArchitecturesSampleApp extiende StatelessWidget { repositorio final de SimpleSocialMediaRepository; ArchitecturesSampleApp({Clave clave, este.repositorio}) : super(clave: clave); @anular Compilación del widget (contexto BuildContext) { volver multiproveedor( proveedores: [ ChangeNotifierProvider<Modelo de redes sociales>( crear: (contexto) => SocialMediaModel(repositorio), ), ChangeNotifierProvider<PersonalDataNotifier>( crear: (contexto) => PersonalDataNotifier(), ) ], hijo: MaterialApp( título: Strings.architecturesSampleApp, debugShowCheckedModeBanner: falso, inicio: pantalla de inicio(), rutas: <String, WidgetBuilder>{ Rutas.socialMedia: (contexto) => SocialMediaScreen(), Rutas.favoritos: (contexto) => FavoritosPantalla(), Rutas.personalDataForm: (contexto) => PersonalDataScreen(), Rutas.personalDataInfo: (contexto) => PersonalDataInfoScreen() }, ), ); } }
En este caso, PersonalDataNotifier estará actuando como una capa de lógica de negocios: validará campos, tendrá acceso al modelo de datos para su actualización y actualizará los campos de los que dependerá la vista.
El formulario en sí es una API muy agradable de Flutter, donde podemos adjuntar validaciones automáticamente usando el validador de propiedades y guardar los datos del formulario en el modelo usando la devolución de llamada onSaved . Delegaremos las reglas de validación a PersonalDataNotifier y cuando el formulario sea correcto, le pasaremos los datos introducidos.
Lo más importante en esta pantalla será escuchar un cambio en cada campo y habilitar o deshabilitar el botón, dependiendo del resultado de la validación. Usaremos callback onChange desde el objeto Form . En él, primero comprobaremos el estado de validación y luego lo pasaremos a PersonalDataNotifier .
Forma( clave: _formKey, autovalidar: verdadero, onChanged: () => _onFormChanged(personalDataNotifier), niño: void _onFormChanged(PersonalDataNotifier personalDataNotifier) { var isValid = _formKey.currentState.validate(); notificador de datos personales. en formulario cambiado (es válido); }
En PersonalDataNotifier , prepararemos la variable isFormValid . Lo modificaremos (no olvides llamar a notificarListeners() ) y en la vista, cambiaremos el estado del botón dependiendo de su valor. Recuerde obtener la instancia de Notifier con el parámetro listen: true ; de lo contrario, nuestra vista no se reconstruirá y el estado del botón permanecerá sin cambios.
var personalDataNotifier = Provider.of<PersonalDataNotifier>(context, listen: true);
En realidad, dado el hecho de que usamos personalDataNotifier en otros lugares, donde no es necesario volver a cargar la vista, la línea anterior no es óptima y debe tener el parámetro de escucha establecido en falso . Lo único que queremos recargar es el botón, por lo que podemos envolverlo en un consumidor clásico:
Consumidor<Notificador de datos personales>( constructor: (contexto, notificador, niño) { return Botón Elevado( hijo: Texto(Strings.addressNext), onPressed: notificador.isFormValid ? /* acción cuando el botón está habilitado */ : nulo, color: colores.azul, disabledColor: Colores.gris, ); }, )
Gracias a esto, no obligamos a otros componentes a recargarse cada vez que usamos un notificador.
En la vista que muestra los datos personales, no habrá más problemas: tenemos acceso a PersonalDataNotifier y desde allí, podemos descargar el modelo actualizado.
Cómo crear un formulario con muchos campos con BLoC
Para la pantalla anterior necesitábamos dos objetos BLoC . Entonces, cuando añadamos otra “doble pantalla”, tendremos cuatro en total. Como en el caso de Provider , podemos manejarlo con MultiBlocProvider , que funciona casi de manera idéntica.
void principal() => ejecutarAplicación( MultiBlocProvider(proveedores: [ ProveedorBloque( crear: (contexto) => SocialMediaBloc(SimpleSocialMediaRepository()), ), ProveedorBloque( crear: (contexto) => SocialMediaListBloc( mainBloc: BlocProvider.of<SocialMediaBloc>(contexto))), ProveedorBloque( crear: (contexto) => PersonalDataBloc(), ), ProveedorBloque( crear: (contexto) => PersonalDataInfoBloc( mainBloc: BlocProvider.of<PersonalDataBloc>(contexto)), ) ], niño: ArquitecturasSampleApp()), );
Como en el patrón BLoC , es mejor comenzar con los posibles estados y acciones.
clase abstracta PersonalDataState {} clase NextButtonDisabled extiende PersonalDataState {} clase NextButtonEnabled extiende PersonalDataState {} clase InputFormCorrect extiende PersonalDataState { modelo final de datos personales; InputFormCorrect(este.modelo); }
Lo que está cambiando en esta pantalla es el estado del botón. Por lo tanto, necesitamos estados separados para ello. Además, el estado InputFormCorrect nos permitirá enviar los datos que ha recogido el formulario.
clase abstracta PersonalDataEvent {} clase FormInputChanged extiende PersonalDataEvent { bool final es válido; FormInputChanged(this.isValid); } clase FormCorrect extiende PersonalDataEvent { datos de formulario de datos personales finales; FormCorrect(esto.formData); }
Escuchar los cambios en el formulario es crucial, de ahí el evento FormInputChanged . Cuando el formulario es correcto, se enviará el evento FormCorrect .
Cuando se trata de validaciones, hay una gran diferencia aquí si lo compara con Provider. Si quisiéramos encerrar toda la lógica de validación en la capa BLoC , tendríamos muchos eventos para cada uno de los campos. Además, muchos estados requerirían que la vista mostrara mensajes de validación.
Esto es, por supuesto, posible, pero sería como una lucha contra la API TextFormField en lugar de utilizar sus ventajas. Por lo tanto, si no hay razones claras, puede dejar las validaciones en la capa de vista.
El estado del botón dependerá del estado enviado a la vista por BLoC :
BlocBuilder<Bloque de datos personales, Estado de datos personales>( constructor: (contexto, estado) { return Botón Elevado( hijo: Texto(Strings.addressNext), onPressed: el estado es NextButtonEnabled ? /* acción cuando el botón está habilitado */ : nulo, color: colores.azul, disabledColor: Colores.gris, ); })
El manejo de eventos y la asignación a estados en PersonalDataBloc serán los siguientes:
@anular Stream<EstadoDatosPersonales> mapEventToState(evento PersonalDataEvent) asíncrono* { si (el evento es FormCorrect) { producir InputFormCorrect(event.formData); } else if (el evento es FormInputChanged) { rendimiento mapFormInputChangedToState (evento); } } PersonalDataState mapFormInputChangedToState (evento FormInputChanged) { si (evento.esVálido) { return BotónSiguienteHabilitado(); } más { return SiguienteBotónDesactivado(); } }
En cuanto a la pantalla con un resumen de datos personales, la situación es similar al ejemplo anterior. El BLoC adjunto a esta pantalla recuperará la información del modelo del BLoC de la pantalla del formulario.
clase PersonalDataInfoBloc extiende Bloc<PersonalDataInfoEvent, PersonalDataInfoState> { final PersonalDataBloc mainBloc; PersonalDataInfoBloc({@requerido this.mainBloc}) { mainBloc.listen((estado) { si (el estado es InputFormCorrect) { add(PersonalDataInfoScreenStart(estado.modelo)); } }); } @anular PersonalDataInfoState get initialState => InfoEmpty(); @anular Stream<PersonalDataInfoState> mapEventToState(PersonalDataInfoEvent event) async* { si (el evento es PersonalDataInfoScreenStart) { rendimiento InfoLoaded (evento.modelo); } } }
Arquitectura Flutter: notas para recordar
Los ejemplos anteriores son suficientes para mostrar que existen claras diferencias entre las dos arquitecturas. BLoC separa muy bien la capa de vista de la lógica empresarial. Esto implica una mejor reutilización y capacidad de prueba. Parece que para manejar casos simples, necesita escribir más código que en Provider . Como sabes, en ese caso, esta arquitectura de Flutter se volverá más útil a medida que aumente la complejidad de la aplicación.
¿Quiere crear una aplicación orientada al futuro para su empresa?
Mantengámonos en contactoEl proveedor también separa bien la interfaz de usuario de la lógica y no fuerza la creación de estados separados con cada interacción del usuario, lo que significa que a menudo no es necesario escribir una gran cantidad de código para manejar un caso simple. Pero esto puede causar problemas en casos más complejos.
Haga clic aquí para ver todo el proyecto.