Flutter Arquitectura Limpia [10] – Bloc Scaffolding y Conversión de Entrada

Flutter Arquitectura Limpia [10] – Bloc Scaffolding y Conversión de Entrada

La capa de presentación contiene la interfaz de usuario en forma de widgets y también los titulares de lógica de presentación, que se pueden implementar como ChangeNotifier, Bloc, Reducer, ViewModel, MobX Store … ¡Lo que sea! Sin embargo, en el caso de nuestra aplicación Number Trivia, vamos a usar el paquete flutter_bloc para ayudarnos a implementar el patrón BLoC.

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!

Configurando el IDE

Antes de crear los archivos y clases necesarios para el Bloc, es aconsejable entregar el trabajo repetitivo a una extensión del VS Code o un complemento IntelliJ (¡haga clic en los enlaces!). Si bien puede crear todos los archivos usted mismo, es mucho mejor hacer clic en un botón y dejar que la extensión haga el trabajo por usted.

Eventos, Estados, Bloc y más

Bloc, también escrito como BLoC, es una abreviatura de Business Logic Component. Siguiendo la Arquitectura limpia, debería llamarse PLoC (Presentation Logic Component), pero creo que nos atendremos a la convención de nomenclatura original 😬. Toda la lógica de negocios está en la capa de dominio, después de todo.

 

 

En pocas palabras, Bloc es un patrón de gestión de estado reactivo donde los datos fluyen solo en una dirección. Todo se puede separar en tres pasos principales:

  1. Los eventos (como “obtener trivia de números concretos”) se envían desde los widgets de la interfaz de usuario
  2. Bloc recibe eventos y ejecuta la lógica de negocios apropiada (llamando a los casos de uso, en el caso de la arquitectura limpia).
  3. Los estados (que contienen una instancia de NumberTrivia, por ejemplo) se emiten desde el Bloque de regreso a los Widgets de la interfaz de usuario, que muestran los datos recién llegados.

Datos fluyendo unidireccionalmente

 

Creando los archivos

En VS Code y con la extensión instalada, haga clic derecho en la carpeta del bloc y seleccione “Bloc: New Bloc” en el menú

 

De a los archivos el prefijo “number_trivia”.

Finalmente, elija usar Equatable para que los eventos y estados generados tengan igualdad de valor desde el principio.

La extensión ahora generará 3 archivos regulares, de los cuales cada uno contiene una clase básica para el Bloc, Evento y Estado, respectivamente. El cuarto archivo llamado simplemente bloc.dart es el llamado “barrel file” que simplemente exporta todos los demás. Esto facilita las importaciones en otras partes de la capa de presentación.

 

Eventos

El archivo number_trivia_event.dart actualmente solo contiene una clase abstracta base, de la que heredarán todos nuestros eventos personalizados. ¿Qué tipo de eventos deberían poder enviar los widgets al bloc? Bueno, al mirar la interfaz de usuario, solo hay 2 botones: uno para mostrar trivia para un número concreto, otro para un número aleatorio.

 

Por lo tanto, es una buena idea tener dos eventos.

Los has adivinado correctamente: GetTriviaForConcreteNumber y GetTriviaForRandomNumber. Sin embargo, no se preocupe, ya que ahora habrá una gran diferencia en cómo manejaremos esos eventos dentro del Bloc. Verás por qué en solo un segundo.

El evento aleatorio será solo una clase vacía. Sin embargo, el evento concreto debe contener un campo para el número. ¿Cuál debería ser el tipo de campo? Puede ser impactante para algunos de ustedes, pero el tipo del campo de número será un String

number_trivia_event.dart
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

@immutable
abstract class NumberTriviaEvent extends Equatable {
  NumberTriviaEvent([List props = const <dynamic>[]]) : super(props);
}

class GetTriviaForConcreteNumber extends NumberTriviaEvent {
  final String numberString;

  GetTriviaForConcreteNumber(this.numberString) : super([numberString]);
}

class GetTriviaForRandomNumber extends NumberTriviaEvent {}

Los eventos se envían desde los widgets. El widget en el que el usuario escribe un número será un TextField. Un valor contenido dentro de un TextField siempre es un String.

Convertir un string en un int directamente en la interfaz de usuario o incluso dentro de la clase Event iría en contra de lo que hemos estado tratando de lograr con la Arquitectura Limpia todo el tiempo: mantenibilidad, legibilidad y comprobabilidad. Ah, y también violaríamos el primer principio SÓLIDO de separación de preocupaciones.

Nunca coloque ninguna lógica comercial o de presentación en la interfaz de usuario. Las aplicaciones Flutter son especialmente susceptibles a esto ya que el código de la interfaz de usuario también está escrito en Dart.

InputConverter

Romperemos un poco nuestra tradición y crearemos una clase para hacer la conversión, un InputConverter, sin crear primero su contrato de clase abstracta. Personalmente, creo que no es necesario crear contratos para clases de utilidad simples como esta. Además, dado que cada clase en Dart se puede implementar como una interfaz, hacer mocking del convertidor de entrada mientras se prueba el bloc en la siguiente parte seguirá siendo tan fácil como hacer mocking de una clase abstracta.

Vivirá dentro de la capa de presentación de forma muy parecida a NumberTriviaModel dentro de la capa de datos. El propósito del convertidor será el mismo que el del modelo: no permitir que la capa de dominio se enrede en el mundo exterior. ¡Los números no son strings, al igual que NumberTrivia no es JSON, después de todo!

Vamos a crear un archivo para esto en una nueva carpeta core/util. Si quisieras ser muy estricto en la separación del código en capas, podrías, por supuesto, ponerlo en core/presentation/util.

Ubicación del archivo

Tendrá un único método llamado stringToUnsignedInteger. Esto se debe a que, además de analizar las cadenas, también se asegurará de que el número ingresado no sea negativo.

Para facilitar las pruebas, creemos un método vacío junto con Failure que se devolverá si el número no es válido.

 

input_converter.dart

import 'package:dartz/dartz.dart';

import '../error/failure.dart';

class InputConverter {
  Either<Failure, int> stringToUnsignedInteger(String str) {
    // TODO: Implement
  }
}

class InvalidInputFailure extends Failure {}

Dentro del archivo de prueba que se encuentra en la ubicación reflejada habitual, no habrá nada de que usar mock, ya que InputConverter no tiene dependencias. La primera prueba manejará el caso cuando todo salga bien y el string de entrada sea, de hecho, un entero sin signo (también conocido como positivo).

input_converter_test.dart

import 'package:clean_architecture_tdd_prep/core/util/input_converter.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  InputConverter inputConverter;

  setUp(() {
    inputConverter = InputConverter();
  });

  group('stringToUnsignedInt', () {
    test(
      'should return an integer when the string represents an unsigned integer',
      () async {
        // arrange
        final str = '123';
        // act
        final result = inputConverter.stringToUnsignedInteger(str);
        // assert
        expect(result, Right(123));
      },
    );
  });
}

Al escribir la menor cantidad de código posible, simplemente devolveremos la cadena analizada envuelta en el lado derecho de Either.

 

input_converter.dart

Either<Failure, int> stringToUnsignedInteger(String str) {
  return Right(int.parse(str));
}

Por supuesto, cuando el string no es un número en absoluto, sino que contiene caracteres como ‘abc’ o incluso si contiene lugares decimales, el método debería devolver un InvalidInputFailure.

 

test.dart

test(
  'should return a failure when the string is not an integer',
  () async {
    // arrange
    final str = 'abc';
    // act
    final result = inputConverter.stringToUnsignedInteger(str);
    // assert
    expect(result, Left(InvalidInputFailure()));
  },
);

Analizar un String inválida a un int, arroja una Excepción de Formato, por lo que queremos atrapar eso y convertirlo en Failure

 

implementation.dart

Either<Failure, int> stringToUnsignedInteger(String str) {
  try {
    return Right(int.parse(str));
  } on FormatException {
    return Left(InvalidInputFailure());
  }
}

Finalmente, queremos limitar al usuario para que solo ingrese enteros positivos o un cero. Si el entero ingresado es negativo, también devolveremos un InvalidInputFailure

 

test.dart

test(
  'should return a failure when the string is a negative integer',
  () async {
    // arrange
    final str = '-123';
    // act
    final result = inputConverter.stringToUnsignedInteger(str);
    // assert
    expect(result, Left(InvalidInputFailure()));
  },
);

implementation.dart

Either<Failure, int> stringToUnsignedInteger(String str) {
  try {
    final integer = int.parse(str);
    if (integer < 0) throw FormatException();
    return Right(integer);
  } on FormatException {
    return Left(InvalidInputFailure());
  }
}

Esto es todo lo que InputConverter hará en la aplicación Number Trivia. Lo usaremos desde dentro del Bloc en la siguiente parte.

Estados

 

Los estados generados por el bloc son los que controlan la interfaz de usuario. Ya hay una clase concreta generada en el archivo number_trivia_state.dart. Solo cámbiele el nombre a Vacío.

En nuestro caso, habrá cuatro estados: Vacío, Cargando, Cargado y Error. De forma similar a cómo los eventos transportan datos desde la interfaz de usuario al bloc, los estados transportan datos desde el bloc a la interfaz de usuario. El estado Cargado contendrá una entidad NumberTrivia para mostrar los datos, y el estado Error contendrá un mensaje de error.

 

number_trivia_state.dart

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

import '../../domain/entities/number_trivia.dart';

@immutable
abstract class NumberTriviaState extends Equatable{
  NumberTriviaState([List props = const <dynamic>[]]) : super(props);
}

class Empty extends NumberTriviaState{}

class Loading extends NumberTriviaState{}

class Loaded extends NumberTriviaState{
  final NumberTrivia trivia;

  Loaded({@required this.trivia}) : super([trivia]);
}

class Error extends NumberTriviaState{
  final String message;

  Error({@required this.message}) : super([message]);
}

¿Qué sigue?

Hemos creado los eventos y estados para el bloc, junto con la clase InputConverter que contiene la lógica de presentación para convertir un string en un int.

La próxima parte es la implementación del Bloc, por supuesto. Esto significa que haremos un desarrollo basado en pruebas con Streams, porque sobre eso se basa el patrón BLoC.

 

 

Previous

Flutter Arquitectura Limpia [9] – Fuente de Datos Remota

Flutter Arquitectura Limpia [11] – Implementación de Bloc1/2

Next

Deja un comentario