Blog

Ya está aquí el primer post de la serie de tutoriales de Dart & Flutter

23/01/2020
Flutter and Dart

Lo prometido es deuda. La pasada semana nuestro compañero Antonio Cantero publicó un post en el blog adelantando una serie de traducciones que próximamente iba a publicar en ASPgems. Pues bien, a partir de ahora cada semana encontrarás un nuevo post publicado. Si quieres saber más sobre Flutter, aquí tienes la oportunidad perfecta para hacerlo.

Super Enum: Dart & Flutter Tutorial – Almacenar datos personalizados

Este tutorial es una traducción del genial trabajo de Matej Rešetár. Puedes leer la versión original aquí o si lo prefieres ver el videotutorial.

Los tipos enumerados en Dart nunca han sido demasiado útiles comparado con otros lenguajes. No podemos almacenar datos adicionales en ellos y usarlos en una sentencia switch es un suplicio. Sin embargo, cuando usamos el package super_enum, podemos traer a Dart un poco de la gloría de los enum de Kotlin.

Proyecto inicial

Puedes descargar el proyecto completo aquí.

En este tutorial vamos a aprender sobre super_enum en Flutter construyendo estados y eventos para un BLoC usado para mostrar el pronóstico del tiempo. No pasa nada si no sabes mucho de BLoC – no vamos a tocarlo mucho. Por supuesto, puedes utilizar este package en cualquier proyecto Dart y en cualquier ocasión.
El proyecto inicial es ya una app funcional y básicamente sólo vamos a pasar de una jerarquía de clases desordenada a algo más ordenado con un tipo de datos algebraicamente generados.

Añadir super_enum

Dado que este package usa generación de código fuente, necesitamos dependencies y dev_dependencies.

pubspec.yaml
dependencies:
flutter:
sdk: flutter
super_enum: ^0.2.0
flutter_bloc: ^2.1.1
equatable: ^1.0.1
dev_dependencies:
flutter_test:
sdk: flutter
build_runner:
super_enum_generator: ^0.2.0

Refactorizando la jerarquía de clases

Lo que pasa con el código Dart puro no es que sea incapaz de hacer cosas, es solo que escribir código Dart a menudo es más repetitivo y propenso a errores de lo que debería ser. Veamos, por ejemplo, las clases WeatherState. ¿Cuántas líneas de código repetitivo puedes detectar?

Nota que la cantidad de código repetitivo ya se ha reducido al usar Equatable para la igualdad de valores. A pesar de esto aún queda mucho código redundante.

///main.dart

abstract class WeatherState extends Equatable {
const WeatherState();
}

class WeatherInitial extends WeatherState {
const WeatherInitial();
@override
List<object width="300" height="150"> get props => [];}class WeatherLoading extends WeatherState {const WeatherLoading();@overrideList<object> get props => [];}</object></object>

class WeatherLoaded extends WeatherState {
final Weather weather;
const WeatherLoaded(this.weather);
@override
List<object width="300" height="150"> get props => [weather];}</object>

class WeatherError extends WeatherState {
final String message;
const WeatherError(this.message);
@override
List<object width="300" height="150"> get props => [message];}```</object>

Adicionalmente, una vez que queremos comprobar todos los estados desde la UI, nuevamente deja mucho que desear. Además del código repetitivo de las sentencias **if**, que es obvio, ¡ni siquiera es exhaustivo!

```dart

///weather_search_page.dart

...

if (state is WeatherInitial) {
return buildInitialInput();
} else if (state is WeatherLoading) {
return buildLoading();
} else if (state is WeatherLoaded) {
return buildColumnWithData(context, state.weather);
} else if (state is WeatherError) {
return buildInitialInput();
}

...

Entendemos aquí por exhaustividad que tenemos que comprobar todas las subclases de, en este caso, WeatherState. Actualmente, añadir otra subclase no rompería la cadena if-else, lo cual es un problema.

Crear super enums

Podemos eludir todos los problemas descritos anteriormente al convertir WeatherState en una enumeración y anotarlo sucintamente mientras mantenemos el código repetitivo al mínimo. Por detrás, super_enum_generator tomará la enumeración anotada y la convertirá en clases que admiten un método when (cómo en Kotlin) y también valoración de igualdad.

Los valores de un super enum pueden ser anotados con @object cuando no contienen ningún dato o @Data si queremos pasar algún dato en ellos.

///weather_state.dart

import 'package:super_enum/super_enum.dart';

import '../data/model/weather.dart';

part 'weather_state.g.dart';

@superEnum
enum _WeatherState {
@object
Initial,
@object
Loading,
@Data(fields: [
DataField('weather', Weather),
])
Loaded,
@Data(fields: [
DataField('message', String),
])
Error,
}

Después de ejecutar el comando build…

flutter packages pub run build_runner watch

El código en el resto de nuestra aplicación se rompió. Antes de cambiarlo todo a la implementación de super_enum modifiquemos rápidamente WeatherEvent también, aunque solo hay una clase en este momento.

<br />///weather_event.dart

import 'package:super_enum/super_enum.dart';

part 'weather_event.g.dart';

@superEnum
enum _WeatherEvent {
@Data(fields: [
DataField('cityName', String),
])
GetWeather,
// Other events go here...
}

No uses super_enum para un solo valor en aplicaciones reales. Aunque, si lo pensamos, hacerlo puede ser una buena opción si en el futuro decidimos agregar otros valores.

Usar super enums

El modo de usar clases generadas por super_enum es sencillo: reemplace todas las instrucciones if o switch llamando al método when en la instancia de enum directamente y use los constructores factory del tipo super.

¿Cómo se ve esto en la práctica? Comencemos modificando el WeatherBloc. Es recomendable aprender más sobre BLoC, pero no es un problema si tampoco está familiarizado con él. El proyecto inicial contiene el siguiente código (que en estos momentos está roto):

///weather_bloc.dart

class WeatherBloc extends Bloc&lt;WeatherEvent, WeatherState&gt; {
final WeatherRepository weatherRepository;

WeatherBloc(this.weatherRepository);

@override
WeatherState get initialState =&gt; WeatherInitial();

@override
Stream mapEventToState(
WeatherEvent event,
) async* {
// Instantiating state classes directly
yield WeatherLoading();
// Using type checks for determining events
if (event is GetWeather) {
try {
final weather = await weatherRepository.fetchWeather(event.cityName);
yield WeatherLoaded(weather);
} on NetworkError {
yield WeatherError("Couldn't fetch weather. Is the device online?");
}
}
}
}

Gracias al poder de super_enum vamos a convertir esto en un código más sencillo, manejable y exhaustivo:

///weather_bloc.dart

class WeatherBloc extends Bloc&lt;WeatherEvent, WeatherState&gt; {
final WeatherRepository weatherRepository;

WeatherBloc(this.weatherRepository);

@override
WeatherState get initialState =&gt; WeatherState.initial();

@override
Stream mapEventToState(
WeatherEvent event,
) async* {
// Instantiating states using factories
yield WeatherState.loading();
// Exhaustive when "statement"
yield* event.when(
getWeather: (e) =&gt; mapGetWeatherToState(e),
);
}

Stream mapGetWeatherToState(GetWeather e) async* {
try {
final weather = await weatherRepository.fetchWeather(e.cityName);
yield WeatherState.loaded(weather: weather);
} on NetworkError {
yield WeatherState.error(
message: "Couldn't fetch weather. Is the device online?");
}
}
}

El último paso es cambiar el código de los widgets de la UI que se reconstruirán en función del estado entrante utilizando BlocBuilder. Además, también mostramos un SnackBar cuando el estado entrante es un Error en BlocListener.

Cómo puede verse a continuación, no necesitamos usar el método when de forma exhaustivo cuando no es necesario, como en el caso de mostrar una SnackBar cuando el estado es Error.

///weather_search_page.dart

...

import '../bloc/weather_state.dart' as weather_state;

class WeatherSearchPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
...
body: Container(
...
child: BlocListener&lt;WeatherBloc, WeatherState&gt;(
listener: (context, state) {
// There's no point to switch through all states in the listener,
// when all we want to do is show an error snackbar.
if (state is weather_state.Error) {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
),
);
}
},
child: BlocBuilder&lt;WeatherBloc, WeatherState&gt;(
builder: (context, state) {
return state.when(
initial: (_) =&gt; buildInitialInput(),
loading: (_) =&gt; buildLoading(),
loaded: (s) =&gt; buildColumnWithData(context, s.weather),
error: (_) =&gt; buildInitialInput(),
);
},
),
),
),
);
}

...

}

class CityInputField extends StatelessWidget {

...

void submitCityName(BuildContext context, String cityName) {
final weatherBloc = BlocProvider.of(context);
weatherBloc.add(WeatherEvent.getWeather(cityName: cityName));
}
}

¡Y ahora ya nada te impide simplificar tus jerarquías de clases con super_enum! No solo hay que escribir menos código repetitivo, sino que además obtenemos una verificación de subtipos más exhaustiva de forma gratuita. Eso es lo que yo llamo un buen trato.

Si te ha gustado este post no olvides suscribirte a nuestra newsletter para seguir informado de todas nuestras novedades.

Un post escrito por Antonio Cantero

También te puede gustar…

ASPgems icon
C/ Sextante, 9
28023 Madrid,
España

Hablemos.

A %d blogueros les gusta esto: