Flutter Arquitectura Limpia [14] – Interfaz del Usuario

flutter clean 14
Compartir

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.

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

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.

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.

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:

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

 

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

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.

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


Compartir

Entradas relacionadas

Deja tu comentario