Arquitectura de la aplicaci贸n Flutter 101: Vanilla, Scoped Model, BLoC


(publicado originalmente en Medium )


Flutter proporciona un marco moderno de estilo de reacci贸n, una rica colecci贸n de widgets y herramientas, pero no hay nada similar a la gu铆a de Android para la arquitectura de aplicaciones .


De hecho, no existe una arquitectura definitiva que cumpla con todos los requisitos posibles, sin embargo, admitamos que la mayor铆a de las aplicaciones m贸viles en las que estamos trabajando tienen al menos algunas de las siguientes funciones:


  1. Solicitar / cargar datos desde / a la red.
  2. Mapear, transformar, preparar datos y presentarlos al usuario.
  3. Poner / obtener datos a / de la base de datos.

Teniendo esto en cuenta, he creado una aplicaci贸n de muestra que est谩 resolviendo exactamente el mismo problema utilizando tres enfoques diferentes de la arquitectura.


Al usuario se le presenta un bot贸n "Cargar datos del usuario" en el centro de la pantalla. Cuando el usuario hace clic en el bot贸n, se activa la carga as铆ncrona de datos y el bot贸n se reemplaza con un indicador de carga. Despu茅s de cargar los datos, el indicador de carga se reemplaza con los datos.


Empecemos



Datos


Para simplificar, he creado la clase Repository que contiene el m茅todo getUser() que emula una llamada de red as铆ncrona y devuelve el objeto Future<User> con valores codificados.


Si no est谩 familiarizado con Futures y la programaci贸n asincr贸nica en Dart, puede obtener m谩s informaci贸n al respecto siguiendo este tutorial y leyendo un documento .


 class Repository { Future<User> getUser() async { await Future.delayed(Duration(seconds: 2)); return User(name: 'John', surname: 'Smith'); } } 

 class User { User({ @required this.name, @required this.surname, }); final String name; final String surname; } 

Vainilla


Construyamos la aplicaci贸n de la forma en que lo har铆an la mayor铆a de los desarrolladores despu茅s de leer la documentaci贸n oficial de Flutter.


Navegando a la pantalla VanillaScreen usando Navigator


 Navigator.push( context, MaterialPageRoute( builder: (context) => VanillaScreen(_repository), ), ); 

Como el estado del widget podr铆a cambiar varias veces durante la vida 煤til del widget, deber铆amos extender StatefulWidget . Implementar un widget con estado tambi茅n requiere tener una clase de State . Los campos bool _isLoading y User _user en la clase _VanillaScreenState representan el estado del widget. Ambos campos se inicializan antes de que se build(BuildContext context) m茅todo build(BuildContext context) .


 class VanillaScreen extends StatefulWidget { VanillaScreen(this._repository); final Repository _repository; @override State<StatefulWidget> createState() => _VanillaScreenState(); } class _VanillaScreenState extends State<VanillaScreen> { bool _isLoading = false; User _user; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Vanilla'), ), body: SafeArea( child: _isLoading ? _buildLoading() : _buildBody(), ), ); } Widget _buildBody() { if (_user != null) { return _buildContent(); } else { return _buildInit(); } } Widget _buildInit() { return Center( child: RaisedButton( child: const Text('Load user data'), onPressed: () { setState(() { _isLoading = true; }); widget._repository.getUser().then((user) { setState(() { _user = user; _isLoading = false; }); }); }, ), ); } Widget _buildContent() { return Center( child: Text('Hello ${_user.name} ${_user.surname}'), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator(), ); } } 

Cuando se crea el objeto de estado del widget, se llama al m茅todo build(BuildContext context) para construir la IU. Todas las decisiones sobre los widgets que deben construirse para representar el estado actual se toman en el c贸digo de declaraci贸n de UI.


 body: SafeArea( child: _isLoading ? _buildLoading() : _buildBody(), ) 

Para mostrar el indicador de progreso cuando el usuario hace clic en el bot贸n "Cargar detalles del usuario" que hacemos a continuaci贸n.


 setState(() { _isLoading = true; }); 

Llamar a setState () notifica al marco que el estado interno de este objeto ha cambiado de una manera que podr铆a afectar la interfaz de usuario en este sub谩rbol, lo que hace que el marco programe una compilaci贸n para este objeto Estado.

Eso significa que despu茅s de llamar setState() m茅todo setState() el marco vuelve a llamar setState() m茅todo build(BuildContext context) y se reconstruye todo el 谩rbol de widgets . Como _isLoading ahora est谩 establecido en el m茅todo true , se llama a _buildBody() lugar de _buildBody() y el indicador de carga se muestra en la pantalla. Exactamente lo mismo sucede cuando manejamos la devoluci贸n de llamada desde getUser() y llamamos a setState() para reasignar los campos _isLoading y _user .


 widget._repository.getUser().then((user) { setState(() { _user = user; _isLoading = false; }); }); 

Pros


  1. F谩cil de aprender y entender.
  2. No se requieren bibliotecas de terceros.

Contras


  1. Todo el 谩rbol de widgets se reconstruye cada vez que cambia el estado del widget.
  2. Est谩 rompiendo el principio de responsabilidad 煤nica. Widget no solo es responsable de construir la interfaz de usuario, tambi茅n es responsable de la carga de datos, la l贸gica empresarial y la gesti贸n del estado.
  3. Las decisiones sobre c贸mo se debe representar el estado actual se toman en el c贸digo de declaraci贸n de UI. Si tuvi茅ramos un poco m谩s complejo, la legibilidad del c贸digo de estado disminuir铆a.

Modelo de alcance


Scoped Model es un paquete de terceros que no est谩 incluido en el marco de Flutter. As铆 es como lo describen los desarrolladores de Scoped Model:


Un conjunto de utilidades que le permiten pasar f谩cilmente un modelo de datos desde un widget principal a sus descendientes. Adem谩s, tambi茅n reconstruye todos los elementos secundarios que usan el modelo cuando se actualiza el modelo. Esta biblioteca se extrajo originalmente de la base de c贸digo Fuchsia.

Construyamos la misma pantalla usando el modelo con alcance. Primero, necesitamos instalar el paquete Scoped Model agregando la dependencia pubspec.yaml a pubspec.yaml en dependencies secci贸n de dependencies .


 scoped_model: ^1.0.1 

Echemos un vistazo al widget UserModelScreen y comp谩relo con el ejemplo anterior que se cre贸 sin usar el Modelo con 谩mbito. Echemos un vistazo al widget UserModelScreen y comp谩relo con el ejemplo anterior que se cre贸 sin usar el Modelo con 谩mbito. Como queremos que nuestro modelo est茅 disponible para todos los descendientes del widget, debemos envolverlo con ScopedModel gen茅rico y proporcionar un widget y un modelo.


 class UserModelScreen extends StatefulWidget { UserModelScreen(this._repository); final Repository _repository; @override State<StatefulWidget> createState() => _UserModelScreenState(); } class _UserModelScreenState extends State<UserModelScreen> { UserModel _userModel; @override void initState() { _userModel = UserModel(widget._repository); super.initState(); } @override Widget build(BuildContext context) { return ScopedModel( model: _userModel, child: Scaffold( appBar: AppBar( title: const Text('Scoped model'), ), body: SafeArea( child: ScopedModelDescendant<UserModel>( builder: (context, child, model) { if (model.isLoading) { return _buildLoading(); } else { if (model.user != null) { return _buildContent(model); } else { return _buildInit(model); } } }, ), ), ), ); } Widget _buildInit(UserModel userModel) { return Center( child: RaisedButton( child: const Text('Load user data'), onPressed: () { userModel.loadUserData(); }, ), ); } Widget _buildContent(UserModel userModel) { return Center( child: Text('Hello ${userModel.user.name} ${userModel.user.surname}'), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator(), ); } } 

En el ejemplo anterior, todo el 谩rbol de widgets se reconstruy贸 cuando cambi贸 el estado del widget. Pero, 驴realmente necesitamos reconstruir toda la pantalla? Por ejemplo, AppBar no deber铆a cambiar en absoluto, por lo que no tiene sentido reconstruirlo. Idealmente, deber铆amos reconstruir solo aquellos widgets que se actualizan. Scoped Model puede ayudarnos a resolver eso.


ScopedModelDescendant<UserModel> se utiliza para encontrar UserModel en el 谩rbol de widgets. Se reconstruir谩 autom谩ticamente cada vez que UserModel notifique que se ha producido un cambio.


Otra mejora es que UserModelScreen ya no es responsable de la administraci贸n del estado y la l贸gica comercial.


Echemos un vistazo al c贸digo de UserModel .


 class UserModel extends Model { UserModel(this._repository); final Repository _repository; bool _isLoading = false; User _user; User get user => _user; bool get isLoading => _isLoading; void loadUserData() { _isLoading = true; notifyListeners(); _repository.getUser().then((user) { _user = user; _isLoading = false; notifyListeners(); }); } static UserModel of(BuildContext context) => ScopedModel.of<UserModel>(context); } 

Ahora UserModel retiene y administra el estado. Para notificar a los oyentes (y reconstruir descendientes) que el cambio tuvo lugar, se debe llamar al m茅todo notifyListeners() .


Pros


  1. L贸gica empresarial, gesti贸n de estado y separaci贸n de c贸digo de UI.
  2. F谩cil de aprender

    Contras

  3. Requiere una biblioteca de terceros.
  4. A medida que el modelo se vuelve m谩s y m谩s complejo, es dif铆cil hacer un seguimiento de cu谩ndo debe llamar a notifyListeners() .

BLoC


BLoC (componentes de l贸gica de negocio) es un patr贸n recomendado por los desarrolladores de Google. Aprovecha la funcionalidad de las transmisiones para administrar y propagar los cambios de estado.


Para desarrolladores de Android: puede pensar en el objeto Bloc como ViewModel y en StreamController como LiveData . Esto har谩 que el siguiente c贸digo sea muy sencillo ya que ya est谩 familiarizado con los conceptos.


 class UserBloc { UserBloc(this._repository); final Repository _repository; final _userStreamController = StreamController<UserState>(); Stream<UserState> get user => _userStreamController.stream; void loadUserData() { _userStreamController.sink.add(UserState._userLoading()); _repository.getUser().then((user) { _userStreamController.sink.add(UserState._userData(user)); }); } void dispose() { _userStreamController.close(); } } class UserState { UserState(); factory UserState._userData(User user) = UserDataState; factory UserState._userLoading() = UserLoadingState; } class UserInitState extends UserState {} class UserLoadingState extends UserState {} class UserDataState extends UserState { UserDataState(this.user); final User user; } 

No se requieren llamadas a m茅todos adicionales para notificar a los suscriptores cuando cambia el estado.


He creado 3 clases para representar posibles estados de la pantalla:


UserInitState para el estado, cuando el usuario abre una pantalla con un bot贸n en el centro.


UserLoadingState para el estado, cuando se muestra el indicador de carga mientras se cargan los datos.


UserDataState para el estado, cuando los datos se cargan y se muestran en la pantalla.


Propagar cambios de estado de esta manera nos permite deshacernos de toda la l贸gica en el c贸digo de declaraci贸n de UI. En el ejemplo con Scoped Model, todav铆a est谩bamos verificando si _isLoading es true en el c贸digo de declaraci贸n de UI para decidir qu茅 widget deber铆amos renderizar. En el caso de BLoC, estamos propagando el estado de la pantalla y la 煤nica responsabilidad del widget UserBlocScreen es representar la interfaz de usuario para este estado.


 class UserBlocScreen extends StatefulWidget { UserBlocScreen(this._repository); final Repository _repository; @override State<StatefulWidget> createState() => _UserBlocScreenState(); } class _UserBlocScreenState extends State<UserBlocScreen> { UserBloc _userBloc; @override void initState() { _userBloc = UserBloc(widget._repository); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Bloc'), ), body: SafeArea( child: StreamBuilder<UserState>( stream: _userBloc.user, initialData: UserInitState(), builder: (context, snapshot) { if (snapshot.data is UserInitState) { return _buildInit(); } if (snapshot.data is UserDataState) { UserDataState state = snapshot.data; return _buildContent(state.user); } if (snapshot.data is UserLoadingState) { return _buildLoading(); } }, ), ), ); } Widget _buildInit() { return Center( child: RaisedButton( child: const Text('Load user data'), onPressed: () { _userBloc.loadUserData(); }, ), ); } Widget _buildContent(User user) { return Center( child: Text('Hello ${user.name} ${user.surname}'), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator(), ); } @override void dispose() { _userBloc.dispose(); super.dispose(); } } 

UserBlocScreen c贸digo de UserBlocScreen volvi贸 a煤n m谩s simple en comparaci贸n con los ejemplos anteriores. Para escuchar los cambios de estado, estamos utilizando StreamBuilder . StreamBuilder es un StatefulWidget que se basa en la 煤ltima instant谩nea de interacci贸n con un Stream .


Pros


No se necesitan bibliotecas de terceros.
L贸gica empresarial, gesti贸n del estado y separaci贸n l贸gica de la interfaz de usuario.
Es reactivo No se necesitan llamadas adicionales como en el caso de notifyListeners() Scoped Model notifyListeners() .


Contras


Se requiere experiencia trabajando con streams o rxdart.



Puede verificar el c贸digo fuente de los ejemplos anteriores de este repositorio de github.


Originalmente el art铆culo se publica en Medium

Source: https://habr.com/ru/post/438524/


All Articles