leanmind logo leanmind text logo

Blog

Código sostenible

Si te gustan nuestros artículos, te gustará nuestro nuevo libro, Código sostenible.

BLoC Pattern con Flutter

En este artículo explicaré cómo funciona el BLoC Pattern de Flutter. Tengo un boilerplate de flutter en el Github de LeanMind, que quizás sea de ayuda para seguir este post.

Pero primero vamos a hacer una pequeña introducción. Este patrón fue presentado en la Dart Conference de 2018 📹, es decir, es bastante nuevo. El objetivo de Paolo Soares y Cong Hu (trabajadores de Google), era hacer que el código fuera reutilizable entre las aplicaciones móviles hechas con Flutter y las aplicaciones web hechas con Angular Dart.

El concepto principal es que exista una capa intermedia entre las vistas y el modelo. Esta capa gestionará los estados y manejará los datos, dependiendo de los eventos que se reciban de la vista.

He dibujado este diagrama: 🎨

diagrama

A partir de ahora, todo el código que veamos estará directamente relacionado con el diagrama y la llamaremos “perfil”.

Crearemos el código mínimo necesario, dividido en cuatro ficheros:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// profile_bloc.dart
class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
  @override
  ProfileState get initialState => throw UnimplementedError();

  @override
  Stream<ProfileState> mapEventToState(ProfileEvent event) async* {
    throw UnimplementedError();
  }
}

// profile_screen.dart
class ProfileScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    throw UnimplementedError();
  }
}

// profile_event.dart
abstract class ProfileEvent {}

// profile_state.dart
abstract class ProfileState {}

Estos son los paquetes que usaremos, hay que añadirlos en el pubspec.yaml:

Sabiendo esto, empecemos a crear el estado inicial, lo llamaremos InitialState y no tendrá ninguna propiedad.

1
2
3
4
5
// profile_state.dart
abstract class InitialState extends ProfileState implements Built<InitialState, InitialStateBuilder> {
  InitialState._();
  factory InitialState([void Function(InitialStateBuilder) updates]) = _$InitialState;
}

Uhm… quizás sea necesario explicarlo:

💭 Es una clase abstracta llamada InitialState que extenderá de nuestra clase ProfileState anteriormente creada e implementará el Built de sí mismo. Dentro, creamos el constructor privado para que sea inaccesible desde fuera y una factoría que recibe una función (opcional) para construir el estado, más adelante veremos cómo se construye cuando necesita parámetros.

Hecho eso, vamos a tener que añadir justo después de los imports la siguiente línea de código:

1
2
// profile_state.dart
part 'profile_state.g.dart';

Lanzaremos el comando flutter pub run build_runner watch --delete-conflicting-outputs que ejecutará el paquete build_runner y autogenerará el código necesario en el fichero especificado para hacer funcionar nuestro InitialState.

¡Perfecto! 👏 Hemos creado nuestro primer estado… pero no se utiliza en ningún momento, así que vamos a indicárselo a nuestro bloc:

1
2
3
// profile_bloc.dart
@override
ProfileState get initialState => InitialState();

Teniendo este estado, vamos a tener que asociar la vista con el bloc ¿Cómo? Pues con el BlocBuilder (de flutter_bloc):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// profile_screen.dart
@override
Widget build(BuildContext context) {
  return BlocBuilder<ProfileBloc, ProfileState>(
    builder: (BuildContext context, ProfileState state) {
      return Container(
        color: Colors.blue[900],
        child: Center(child: CircularProgressIndicator()),
      )
    },
  );
}

Así, cada vez que nuestro estado cambie, la vista se dará cuenta y pintará lo que haga falta dependiendo del estado. Como por ahora solamente tenemos uno, pintará un spinner en el centro de la pantalla.

Toca crear el evento que hará pedir la información al repository. Seguimos exactamente el mismo proceso que hicimos con el estado, pero con un evento que llamaremos LoadDataEvent:

1
2
3
4
5
6
7
8
9
// profile_event.dart
part 'profile_event.g.dart';

abstract class ProfileEvent {}

abstract class LoadDataEvent extends ProfileEvent implements Built<LoadDataEvent, LoadDataEventBuilder> {
  LoadDataEvent._();
  factory LoadDataEvent([void Function(LoadDataEventBuilder) updates]) = _$LoadDataEvent;
}

Ahora que ya tenemos el evento, tenemos que lanzarlo cuando el estado sea InitialState:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// profile_screen.dart
class ProfileScreen extends StatefulWidget {
  @override
  _ProfileScreenState createState() => _ProfileScreenState();
}

class _ProfileScreenState extends State<ProfileScreen> {
  ProfileBloc bloc;

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ProfileBloc, ProfileState>(
      builder: (BuildContext context, ProfileState state) {
        if (state is InitialState) {
          bloc = BlocProvider.of<ProfileBloc>(context);
          bloc.add(LoadDataEvent());
        }
        return buildSpinner();
      },
    );
  }

  Widget buildSpinner() {
    return Container(
      color: Colors.blue[900],
      child: Center(child: CircularProgressIndicator()),
    );
  }

  @override
  void dispose() {
    bloc?.close();
    super.dispose();
  }
}

Aquí hay varios puntos importantes:

Pasamos al bloc. En la función mapEventToState añadiremos nuestro evento y pediremos al repository los datos tal que así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// profile_bloc.dart
@override
Stream<ProfileState> mapEventToState(ProfileEvent event) async* {
  if (event is LoadDataEvent){
    String name = await getName();
  }
}

// Esto simulará el repository
Future<String> getName() async {
  await Future.delayed(Duration(seconds: 2));
  return "Any Name";
}

Vale… sí, tenemos los datos, pero… ¿y ahora qué? Pues tendremos que crear los siguientes estados. Según nuestro diagrama, cuando se lanza el evento LoadDataEvent, pasará a estar en el estado LoadingState, pedirá los datos al repository y el estado cambiará a RecoveredDataState. Es decir, vamos a tener que crear estos dos estados nuevos ¡Al lío!

El estado LoadingState tampoco tendrá ninguna propiedad, ya que no necesitamos que tenga ningún tipo de dato a la hora de mostrar en pantalla el spinner, así que será así:

1
2
3
4
5
// profile_state.dart
abstract class LoadingState extends ProfileState implements Built<LoadingState, LoadingStateBuilder> {
  LoadingState._();
  factory LoadingState([void Function(LoadingStateBuilder) updates]) = _$LoadingState;
}

Nada nuevo, es prácticamente igual que el InitialState. Ahora pasemos al estado RecoveredDataState, que sí tendrá propiedades. Esta será la única diferencia:

1
2
3
4
5
6
7
// profile_state.dart
abstract class RecoveredDataState extends ProfileState implements Built<RecoveredDataState, RecoveredDataStateBuilder> {
  String get name;

  RecoveredDataState._();
  factory RecoveredDataState([void Function(RecoveredDataStateBuilder) updates]) = _$RecoveredDataState;
}

¡Guau, ya tenemos tres estados! 😄 Ahora volvamos al bloc y terminemos el evento LoadDataEvent:

1
2
3
4
5
6
7
8
9
// profile_bloc.dart
@override
Stream<ProfileState> mapEventToState(ProfileEvent event) async* {
  if (event is LoadDataEvent) {
    yield LoadingState();
    String name = await getName();
    yield RecoveredDataState((builder) => builder..name = name);
  }
}

¿yield? ¿async*? Seguramente te diste cuenta antes, pero… ¿qué es esto? No quiero entrar mucho en detalle ahora, así que te recomiendo que leas este artículo, está en inglés, pero creo que se entiende bastante bien.

Por otro lado, también es importante resaltar el estado RecoveredDataState, estamos pasándole el nombre a través del builder que es el que se autogeneró en el estado, imagínate que también tuvieramos el apellido, ¿cómo se lo pasaríamos? Pues así:

1
2
3
4
yield RecoveredDataState((builder) => builder
  ..name = name
  ..surname = surname
);

Es un poco extraño, ¿no? 🤔 Pues realmente es lo mismo que si hicieramos así:

1
2
3
4
5
yield RecoveredDataState((builder) {
  builder.name = name;
  builder.surname = surname;
  return builder;
});

Bien, vayamos a la vista. Estamos mandando dos nuevos estados a la vista, pero esta no va a cambiar, porque no lo estamos controlando:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// profile_screen.dart
@override
Widget build(BuildContext context) {
  return BlocBuilder<ProfileBloc, ProfileState>(
    builder: (BuildContext context, ProfileState state) {
      if (state is InitialState) {
        bloc = BlocProvider.of<ProfileBloc>(context);
        bloc.add(LoadDataEvent());
      }
      if (state is RecoveredDataState) {
        return buildName(state.name);
      }
      return buildSpinner();
    },
  );
}

Widget buildName(String name) {
  return Container(
    color: Colors.grey[300],
    child: Center(
      child: Text(name, style: TextStyle(fontSize: 16)),
    ),
  );
}

Podemos comprobar que en cualquier estado va a aparecer el spinner, excepto si el estado es RecoveredDataState, que significará que tendremos en el estado el nombre (state.name) y podremos pintarlo.

Para completar el diagrama, faltaría cambiar lo que mostramos en el estado RecoveredDataState por un TextField, con el nombre y un RaisedButton (o similar), para que cuando se presione el botón, lance un evento con el nuevo valor del TextField, el bloc escuche este evento y pase por los otros estados, ErrorState ❌ o SuccessState ✔. Sin embargo, con lo que hemos visto, creo que es suficiente para entender que la vista se construye basándonos en los estados y que los estados cambian en relación con los eventos que les mandes.

Espero que realmente te haya servido de algo leer este post y te animo a completar el diagrama. Si quieres contactar conmigo porque tienes alguna duda o sugerencia, puedes escribirme a mi correo michael.reyes@leanmind.es 📥.

¡Muchas gracias por leerme! 📖

Publicado el 19/06/2020 por
Michael image

Michael Reyes Seiffert

https://www.mreysei.dev

¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.

Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?

Impulsamos el crecimiento profesional de tu equipo de developers