Flutter Arquitectura Limpia [14] – Interfaz del Usuario

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]

Debo aclarar que el contenido original es de Resocoder,  lo que he hecho  es una traducción al español del contenido. Al final de este artículo está el video en inglés para que vayan ilustrándose mejor y ya tengan una claridad de lo expuesto

Curso TDD Arquitectura Limpia

001 draw

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!

Índice

    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

    001 seo and web

    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(),
          ),
        );
      }
    }

    app ui

    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.

    Te puede interesar:  Flutter Arquitectura Limpia [10] – Bloc Scaffolding y Conversión de Entrada

    ui with placeholders

    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

    002 touch screen

    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

    003 mails

    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,
              ),
            ),
          ),
        );
      }
    }
    message display widget

     

    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,
          );
        }
      },
    ),
    No vamos a utilizar el mensaje de visualización para el estado cargado para mostrar el NumberTrivia, porque queremos que el número se muestre bien en la parte superior.

    LoadingWidget

    004 reload

     

    Te puede interesar:  Flutter Arquitectura Limpia [4] – Descripción general de la capa de datos y modelos

    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(),
          ),
        );
      }
    }
    Cada widget personalizado en la mitad superior de la pantalla ocupará exactamente un tercio de la altura de toda la pantalla usando MediaQuery. Fijar la altura es útil para evitar que la mitad inferior "salte" cuando cambia la longitud del mensaje / trivia que se muestra.

    TriviaDisplay

    006 education

    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.

    widgets folder

    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

    007

    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.

    Te puede interesar:  Flutter Arquitectura Limpia [6] – Implementación del Repositorio

    [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:

    keyboard overflow

    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?

    005 confetti

    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

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

    Subir