Flutter Arquitectura Limpia [14] – Interfaz del Usuario
![Flutter Arquitectura Limpia [14] – Interfaz del Usuario 1 flutter clean 14](https://rubenjromo.com/wp-content/uploads/2019/12/flutterclean14.jpg.webp)
Flutter Arquitectura Limpia [14] – Interfaz del Usuario
¡Finalmente ha llegado el momento de poner en práctica todo nuestro trabajo anterior! Esta parte se tratará de construir la interfaz de usuario y dividirla en múltiples widgets legibles y mantenibles.
[wp_ad_camp_1]
Curso TDD Arquitectura Limpia
Esta publicación es solo una parte de una serie de tutoriales. ¡Todas las demás partes las puede ver aquí y aprenda a diseñar sus aplicaciones Flutter!
Creando una Página
La función number trivia (y, a su vez, toda la aplicación) tendrá una sola página, que será un StatelessWidget llamado NumberTriviaPage. Vamos a crear una versión en blanco por ahora.
.../presentation/pages/number_trivia_page.dart
class NumberTriviaPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Number Trivia'),
),
);
}
}
Todo este tiempo el archivo main.dart contenía la aplicación de contador de ejemplo. ¡Cambiemos eso! Vamos a eliminar todo y crear MaterialApp. Por supuesto, el widget de inicio será el NumberTriviaPage.
main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Number Trivia',
theme: ThemeData(
primaryColor: Colors.green.shade800,
accentColor: Colors.green.shade600,
),
home: NumberTriviaPage(),
);
}
}
Obtención del titular de la lógica de presentación
En Clean Architecture, la única vía de comunicación entre los widgets de IU y el resto de la aplicación es el titular de la lógica de presentación. La aplicación Number Trivia usa Bloc, pero como digo probablemente por enésima vez, puedes usar cualquier cosa, desde Change Notifier hasta MobX.
Independientemente de su método de administración de estado preferido, aún debe proporcionar su titular de lógica de presentación en todo el árbol de widgets. Para eso, ¡obviamente puedes usar el paquete del proveedor! Como estamos usando Bloc, que está integrado con el proveedor, vamos a usar un BlocProvider especial que tiene algunas características específicas de Bloc.
NumberTriviaBloc debe estar disponible para todo el body del Scaffold de la página. Aquí es donde obtendremos la instancia registrada de NumberTriviaBloc del localizador de servicios, que a su vez iniciará todos los lazy singletons registrados en la parte anterior.
[wp_ad_camp_2]
number_trivia_page.dart
...
import '../../../../injection_container.dart';
import '../bloc/bloc.dart';
class NumberTriviaPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Number Trivia'),
),
body: BlocProvider(
builder: (_) => sl<NumberTriviaBloc>(),
child: Container(),
),
);
}
}
UI Terminada
Construyendo el contenido
Antes de reaccionar a los States emitidos por NumberTriviaBloc, primero construyamos un esquema básico de la interfaz de usuario utilizando widgets Placeholder.
La raíz de la interfaz de usuario será una columna centrada que consta de dos partes básicas:
- La mitad superior se ocupará de la producción. Habrá un mensaje que contiene la trivia misma, algún tipo de error o incluso la llamada inicial a "¡Start Searching!". CircularLoadingIndicator también se mostrará en la parte superior. Esta parte de la interfaz de usuario se reconstruirá cada vez que cambie el estado.
- La parte inferior se ocupará de la entrada. Tendrá un TextField y dos RaisedButtons.
Delimitando con Placeholders
Diseñar la interfaz de usuario con placeholders es una manera perfecta de establecer adecuadamente el espacio y los tamaños de los widgets sin tener que pensar en su implementación. En este punto, también será lo mejor extraer el body del Scaffold en su propio método de ayuda de buildBody.
number_trivia_page.dart
class NumberTriviaPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Number Trivia'),
),
body: buildBody(context),
);
}
BlocProvider<NumberTriviaBloc> buildBody(BuildContext context) {
return BlocProvider(
builder: (_) => sl<NumberTriviaBloc>(),
child: Center(
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: <Widget>[
SizedBox(height: 10),
// Top half
Container(
// Third of the size of the screen
height: MediaQuery.of(context).size.height / 3,
// Message Text widgets / CircularLoadingIndicator
child: Placeholder(),
),
SizedBox(height: 20),
// Bottom half
Column(
children: <Widget>[
// TextField
Placeholder(fallbackHeight: 40),
SizedBox(height: 10),
Row(
children: <Widget>[
Expanded(
// Search concrete button
child: Placeholder(fallbackHeight: 30),
),
SizedBox(width: 10),
Expanded(
// Random button
child: Placeholder(fallbackHeight: 30),
)
],
)
],
)
],
),
),
),
);
}
}
Mitad Superior - Mostrando los datos
La mitad superior de la pantalla tiene que mostrar diferentes widgets dependiendo del estado generado por NumberTriviaBloc. Loading mostrará un indicador de progreso, el error, por supuesto, mostrará un mensaje de error, etc. Es posible construir diferentes widgets de acuerdo con el estado actual del Bloque con un BlocBuilder.
[wp_ad_camp_3]
El estado inicial de NumberTriviaBloc está vacío, así que primero manejemos eso devolviendo un texto simple dentro de un contenedor para asegurarnos de que ocupa un tercio de la altura de la pantalla.
number_trivia_page.dart
...
// Top half
BlocBuilder<NumberTriviaBloc, NumberTriviaState>(
builder: (context, state) {
if (state is Empty) {
return Container(
// Third of the size of the screen
height: MediaQuery.of(context).size.height / 3,
child: Center(
child: Text('Start searching!'),
),
);
}
// We're going to also check for the other states
},
),
...
Display del Mensaje
Además de que el texto es pequeño, se maneja el estado Vacío. Sin embargo, primero limpiemos el desorden que creamos extrayendo el Contenedor en su propio widget MessageDisplay. Usaremos este widget para mostrar también los mensajes de error cuando se emita el estado de error, por lo que debemos asegurarnos de que un string se pueda pasar a través del constructor.
Además, agregaremos un poco de estilo para hacer que el texto sea más grande y también desplazable usando SingleChildScrollView. De lo contrario, los mensajes largos se cortarían debido al Contenedor que tiene una altura limitada.
number_trivia_page.dart
...
BlocBuilder<NumberTriviaBloc, NumberTriviaState>(
builder: (context, state) {
if (state is Empty) {
return MessageDisplay(
message: 'Start searching!',
);
}
},
),
...
class MessageDisplay extends StatelessWidget{
final String message;
const MessageDisplay({
Key key,
@required this.message,
}) : assert(message != null),
super(key: key);
@override
Widget build(BuildContext context) {
return Container(
// Third of the size of the screen
height: MediaQuery.of(context).size.height / 3,
child: Center(
child: SingleChildScrollView(
child: Text(
message,
style: TextStyle(fontSize: 25),
textAlign: TextAlign.center,
),
),
),
);
}
}

Es natural reutilizar este MessageDisplay para mensajes que también provienen del estado Error. Ampliaremos el BlocBuilder con una cláusula else if:
[wp_ad_camp_4]
number_trivia_page.dart
BlocBuilder<NumberTriviaBloc, NumberTriviaState>(
builder: (context, state) {
if (state is Empty) {
return MessageDisplay(
message: 'Start searching!',
);
} else if (state is Error) {
return MessageDisplay(
message: state.message,
);
}
},
),
LoadingWidget
Obtener datos de la API remota lleva algún tiempo. Es por eso que el Bloc emite un estado de carga Loading y es responsabilidad de la interfaz de usuario mostrar un indicador de carga. Ya sabemos que poner widgets de largo aliento directamente en el BlocBuilder dificulta la legibilidad, por lo que inmediatamente vamos a extraer el Contenedor en un LoadingWidget.
number_trivia_page.dart
...
BlocBuilder<NumberTriviaBloc, NumberTriviaState>(
builder: (context, state) {
if (state is Empty) {
return MessageDisplay(
message: 'Start searching!',
);
} else if (state is Loading) {
return LoadingWidget();
}else if (state is Error) {
return MessageDisplay(
message: state.message,
);
}
},
),
...
class LoadingWidget extends StatelessWidget{
const LoadingWidget({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height / 3,
child: Center(
child: CircularProgressIndicator(),
),
);
}
}
TriviaDisplay
Nos falta una implementación de "reacción de estado" para el estado más importante de todos: el estado Loaded que contiene la entidad NumberTrivia en la que está interesado el usuario.
El widget TriviaDisplay será extremadamente similar a MessageDisplay, pero, por supuesto, tomará un objeto NumberTrivia a través del constructor. Además, TriviaDisplay estará compuesto por una columna que muestra dos widgets de texto. Uno mostrará el número en una fuente grande, el otro mostrará la trivia real.
number_trivia_page.dart
...
BlocBuilder<NumberTriviaBloc, NumberTriviaState>(
builder: (context, state) {
if (state is Empty) {
return MessageDisplay(
message: 'Start searching!',
);
} else if (state is Loading) {
return LoadingWidget();
} else if (state is Loaded) {
return TriviaDisplay(
numberTrivia: state.trivia,
);
} else if (state is Error) {
return MessageDisplay(
message: state.message,
);
}
},
),
...
class TriviaDisplay extends StatelessWidget {
final NumberTrivia numberTrivia;
const TriviaDisplay({
Key key,
this.numberTrivia,
}) : assert(numberTrivia != null),
super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height / 3,
child: Column(
children: <Widget>[
// Fixed size, doesn't scroll
Text(
numberTrivia.number.toString(),
style: TextStyle(
fontSize: 50,
fontWeight: FontWeight.bold,
),
),
// Expanded makes it fill in all the remaining space
Expanded(
child: Center(
// Only the trivia "message" part will be scrollable
child: SingleChildScrollView(
child: Text(
numberTrivia.text,
style: TextStyle(fontSize: 25),
textAlign: TextAlign.center,
),
),
),
)
],
),
);
}
}
Pequeñas Modificaciones
Ya hemos hecho mucho para mantener el código mantenible mediante la creación de widgets personalizados. Sin embargo, actualmente están todos dentro del archivo number_trivia_page.dart, así que simplemente muévalos a sus propios archivos en la carpeta de widgets.
Number Trivia -carpeta de presentación
El archivo widgets.dart es algo llamado archivo de barril. Dado que Dart no admite "package imports" como Kotlin o Java, debemos ayudarnos a deshacernos de muchas importaciones individuales con archivos de barril. Simplemente exporta todos los otros archivos presentes dentro de la carpeta.
widgets.dart
export 'loading_widget.dart';
export 'message_display.dart';
export 'trivia_display.dart';
Y luego, dentro de number_trivia_page.dart es suficiente para importar solo el archivo barril:
import '../widgets/widgets.dart';
Mitad Inferior - Recibiendo el input
Todos los widgets que hemos creado hasta este punto no serán de utilidad a menos que el usuario pueda iniciar la extracción de NumberTrivia aleatorio u concreto. Debido a que estamos usando Bloc, esto sucederá enviando eventos.
[wp_ad_camp_5]
Actualmente, la mitad inferior de la IU está llena de placeholders, pero incluso antes de reemplazarlos con widgets reales, separemos toda la mitad inferior de la columna en un Stateful widget TriviaControls personalizado.
All of the widgets we've made up to this point would be of no use unless the user could initiate fetching of random or concrete NumberTrivia
. Because we're using Bloc, this will happen by dispatching events.
number_trivia_page.dart
class TriviaControls extends StatefulWidget{
const TriviaControls({
Key key,
}) : super(key: key);
@override
_TriviaControlsState createState() => _TriviaControlsState();
}
class _TriviaControlsState extends State<TriviaControls> {
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
// Placeholders here...
],
);
}
}
¿Por qué Stateful? TriviaControls tendrá un TextField y para entregar el string ingresada al Bloque cada vez que se presiona un botón, este widget deberá mantener ese string como estado local.
Además de enviar el evento GetTriviaForConcreteNumber cuando se presiona el botón concreto, este evento se enviará también cuando se envíe TextField presionando un botón en el teclado
number_trivia_page.dart
class _TriviaControlsState extends State<TriviaControls> {
final controller = TextEditingController();
String inputStr;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
TextField(
controller: controller,
keyboardType: TextInputType.number,
decoration: InputDecoration(
border: OutlineInputBorder(),
hintText: 'Input a number',
),
onChanged: (value) {
inputStr = value;
},
onSubmitted: (_) {
dispatchConcrete();
},
),
SizedBox(height: 10),
Row(
children: <Widget>[
Expanded(
child: RaisedButton(
child: Text('Search'),
color: Theme.of(context).accentColor,
textTheme: ButtonTextTheme.primary,
onPressed: dispatchConcrete,
),
),
SizedBox(width: 10),
Expanded(
child: RaisedButton(
child: Text('Get random trivia'),
onPressed: dispatchRandom,
),
),
],
)
],
);
}
void dispatchConcrete() {
// Clearing the TextField to prepare it for the next inputted number
controller.clear();
BlocProvider.of<NumberTriviaBloc>(context)
.dispatch(GetTriviaForConcreteNumber(inputStr));
}
void dispatchRandom() {
controller.clear();
BlocProvider.of<NumberTriviaBloc>(context)
.dispatch(GetTriviaForRandomNumber());
}
}
Por supuesto, volvamos a colocar este widget en su propio archivo trivia_controls.dart y luego lo agreguemos como una exportación al archivo barril Por supuesto, volvamos a colocar este widget en su propio archivo trivia_controls.dart y luego lo agreguemos como una exportación al archivo barril widgets.dart.
Últimos Arreglos
¡Funciona! 🎉 ¡Funciona! 🎈 ¡Funciona! 🎊 ¡Hurra! El usuario finalmente puede ingresar un número y obtener una trivia concreta o simplemente puede presionar el botón aleatorio y obtener algunas trivias aleatorias. Sin embargo, hay un error en la interfaz de usuario que se hace evidente de inmediato cuando abrimos un teclado:
Esto se puede solucionar muy fácilmente con un SingleChildScrollView que envuelve todo el cuerpo del Scaffold. Con eso, cada vez que aparezca el teclado y el cuerpo se encoja en altura, se desplazará y no se producirá un desbordamiento.
number_trivia_page.dart
class NumberTriviaPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Number Trivia'),
),
body: SingleChildScrollView(
child: buildBody(context),
),
);
}
...
}
¿Terminamos?
En estas 14 partes del curso de Arquitectura limpia, pasamos de una idea a una aplicación funcional. Aprendió a diseñar sus aplicaciones en capas independientes, cómo hacer TDD, descubrió el poder de los contratos, aprendió la inyección de dependencia y mucho más.
La aplicación Number Trivia que hemos creado puede ser más simple cuando se trata de su funcionalidad, aunque lo importante son los principios. Lo que aprendió en este curso es aplicable a todo tipo de circunstancias y creo que ahora es un mejor desarrollador en general. ¡Practica los principios que aprendiste aquí y construye algo increíble!
Aquí les comparto el video
Deja una respuesta
Lo siento, debes estar conectado para publicar un comentario.