DEV Community

Marcelo Sebastián
Marcelo Sebastián

Posted on • Updated on

Manejo de estados en flutter, BLoC Pattern sin librerías

En esta guía se mostrará el uso del patrón BLoC sin hacer uso de librerías de terceros.

Manejo de estados en flutter, BLoC Pattern sin librerías

El código de esta guía se lo puede encontrar en GitHub.

GitHub logo marcedroid / Flutter-State-Management-Demos

Manejo de estados en Flutter

Importante

La rama bloc-no-libraries es la que tiene la implementación del patrón BLoC.
La rama master tiene el código base de la aplicación.


La aplicación cuenta con tres vistas:

  • User: En esta vista al cambiar los valores del campo de texto también se actualizará el título del AppBar, además se actualizará en nombre de usuario en la vista cart.
  • Catalog: En esta vista se muestra un listado de Items y al interactuar con cada uno de ellos se puede ver que el ícono para agregar/eliminar cambia, también se actualiza el contador de items del AppBar y el listado de items de la vista cart.
  • Cart: En esta vista se muestra un listado de los productos que se han seleccionado en la vista de catalog también se muestra el precio total y el nombre de usuario junto con las iniciales del mismo en un CircleAvatar.

Para tener el código ordenado se debe crear en la carpeta lib la carpeta blocs y dentro de esta carpeta crear catalog y user

  • blocs
    • catalog
    • user

Índice



Patrón BLoC para nombre de usuario

Crear tres archivos dentro de la carpeta blocs/user

  • user_event.dart: Archivo en el cual se declararán los eventos de patrón BLoC.
  • user_state.dart: Archivo que se encargará de la lógica para obtener, modificar o eliminar la data.
  • user_bloc.dart: Detectará el tipo de evento recibido y se encargará de responder a ellos.

user_event.dart

abstract class UserEvent {}

class OnChangeEvent extends UserEvent {
  final String value;

  OnChangeEvent(this.value);
}

class GetUsernameEvent extends UserEvent {}

Es un archivo bastante simple, que podíamos haber incluido directamente en el archivo _bloc, pero de esta forma tenemos mucho mejor ordenado el código.

Explicación:

Primero es necesario definir los eventos, una de las formas más fáciles es hacerlo con clases.
Vamos a crear una clase abstracta base de la cual van a extender el resto de nuestros eventos de usuario.

abstract class UserEvent {}

Luego vamos a definir el evento que se ejecutará al momento de hacer un cambio en el nombre de usuario.
Este va a recibir como parámetro un String.

class OnChangeEvent extends UserEvent {
  final String value;

  OnChangeEvent(this.value);
}

Por último vamos a definir otro evento con el cual obtendremos el nombre de usuario actual, esto lo vamos a necesitar para actualizar la data entre pantallas o tomar el valor fuera de una vista.

class GetUsernameEvent extends UserEvent {}

user_state.dart

class UserState {
  String _username = 'Guest';

  UserState._();
  static UserState _instance = UserState._();
  factory UserState() => _instance;

  String get username => _username;

  void onChange(value) {
    _username = value;
  }
}

Explicación:

Crear una variable privada, en este caso quiero que el usuario por default sea Guest es por eso que no he dejado un string vacío.

String _username = 'Guest';

Necesitamos tener una instancia que sea compartida y que solo pueda ser instanciada una vez (singleton).

// Definimos un constructor privado.
UserState._();

// Crear una instancia privada usando nuestro constructor.
static UserState _instance = UserState._();

// Crear una factory (método que genera las instancias)
factory UserState() => _instance;

Crear un get para obtener en nombre de usuario.

String get username => _username;

Por último crear el método para actualizar el nombre de usuario.

void onChange(value) {
  _username = value;
}

user_bloc.dart

import 'dart:async';

import 'package:state_management/blocs/user/user_event.dart';
import 'package:state_management/blocs/user/user_state.dart';

class UserBloc {
  UserState _userState = UserState();

  StreamController<UserEvent> _input = StreamController();
  StreamController<String> _output = StreamController();

  StreamSink<UserEvent> get sendEvent => _input.sink;
  Stream<String> get userStream => _output.stream;

  UserBloc() {
    _input.stream.listen(_onEvent);
  }

  void _onEvent(UserEvent event) {
    if (event is OnChangeEvent) {
      _userState.onChange(event.value);
    }

    _output.add(_userState.username);
  }

  void dispose() {
    _input.close();
    _output.close();
  }
}

Explicación:

Importar dart:async ya que es requerido por StreamController, también importar el archivo de eventos y estado.

import 'dart:async';

import 'package:state_management/blocs/user/user_event.dart';
import 'package:state_management/blocs/user/user_state.dart';

Crear una variable privada de tipo UserState().

UserState _userState = UserState();

Crear dos variables privadas de tipo StreamController().

StreamController<UserEvent> _input = StreamController();
StreamController<String> _output = StreamController();

Exponer sink y stream mediante getters, esto es para que la vistas puedan enviar eventos y escuchar los cambios.

StreamSink<UserEvent> get sendEvent => _input.sink;
Stream<String> get userStream => _output.stream;

Crear un constructor que escuche los eventos.

UserBloc() {
  _input.stream.listen(_onEvent);
}

Crear un método privado _onEvent que se encargue de procesar los eventos que escucha el _input.stream.

void _onEvent(UserEvent event) {
// Si el evento es de tipo OnChangeEvent, ejecutar la función onChange de _userState.
  if (event is OnChangeEvent) {
    _userState.onChange(event.value);
  }

// Enviamos el nuevo valor utilizando el _output.
  _output.add(_userState.username);
}

Liberar los recursos de los StreamController() cuando no se los necesite con un dispose.

void dispose() {
  _input.close();
  _output.close();
}


Patrón BLoC para catálogo

Estos pasos son prácticamente los mismos que se hicieron para el patrón BLoC del nombre de usuario.

Crear tres archivos dentro de la carpeta blocs/catalog

  • catalog_event.dart: Archivo en el cual se declararán los eventos de patrón BLoC.
  • catalog_state.dart: Archivo que se encargará de la lógica para obtener, modificar o eliminar la data.
  • catalog_bloc.dart: Detectará el tipo de evento recibido y se encargará de responder a ellos.

catalog_event.dart

import 'package:state_management/models/item_model.dart';

abstract class CatalogEvent {}

class AddCatalogItemEvent extends CatalogEvent {
  final ItemModel item;

  AddCatalogItemEvent(this.item);
}

class RemoveCatalogItemEvent extends CatalogEvent {
  final ItemModel item;

  RemoveCatalogItemEvent(this.item);
}

class GetCatalogEvent extends CatalogEvent {}

Explicación:

Importar item_model.dart ya que este BLoC va a usar una lista de ItemModel.

import 'package:state_management/models/item_model.dart';

Crear una clase abstracta de la cual van a extender los otros eventos.

abstract class CatalogEvent {}

Definir un evento para agregar un item al carrito, recibe como parámetro un ItemModel.

class AddCatalogItemEvent extends CatalogEvent {
  final ItemModel item;

  AddCatalogItemEvent(this.item);
}

Definir un evento para eliminar un item del carrito, recibe como parámetro un ItemModel.

class RemoveCatalogItemEvent extends CatalogEvent {
  final ItemModel item;

  RemoveCatalogItemEvent(this.item);
}

Definir un evento que se ejecutará para obtener el listado del catálogo.

class GetCatalogEvent extends CatalogEvent {}

catalog_state.dart

import 'package:state_management/models/item_model.dart';

class CatalogState {
  List<ItemModel> _catalog = [];

  CatalogState._();
  static CatalogState _instance = CatalogState._();
  factory CatalogState() => _instance;

  List<ItemModel> get catalog => _catalog;

  void addToCatalog(ItemModel itemModel) {
    _catalog.add(itemModel);
  }

  void removeFromCatalog(ItemModel itemModel) {
    _catalog.remove(itemModel);
  }
}

Explicación:

Importar item_model.dart ya que este BLoC va a usar una lista de ItemModel.

import 'package:state_management/models/item_model.dart';

Crear una variable privada que va a contener una lista de objetos ItemModel, en este caso se inicial la lista vacía.

List<ItemModel> _catalog = [];

Necesitamos tener una instancia que sea compartida y que solo pueda ser instanciada una vez (singleton).

// Definimos un constructor privado.
CatalogState._();

// Crear una instancia privada usando nuestro constructor.
static CatalogState _instance = CatalogState._();

// Crear la factory (método que genera las instancias)
factory CatalogState() => _instance;

Crear un getter para obtener el listado de ItemModel

List<ItemModel> get catalog => _catalog;

Crear el método que agrega un item al carrito.

void addToCatalog(ItemModel itemModel) {
  _catalog.add(itemModel);
}

Crear el método que elimina un item del carrito.

void removeFromCatalog(ItemModel itemModel) {
  _catalog.remove(itemModel);
}

catalog_bloc.dart

import 'dart:async';

import 'package:state_management/blocs/catalog/catalog_event.dart';
import 'package:state_management/blocs/catalog/catalog_state.dart';
import 'package:state_management/models/item_model.dart';

class CatalogBloc {
  CatalogState _catalogState = CatalogState();

  StreamController<CatalogEvent> _input = StreamController();
  StreamController<List<ItemModel>> _output =
      StreamController<List<ItemModel>>.broadcast();

  StreamSink<CatalogEvent> get sendEvent => _input.sink;
  Stream<List<ItemModel>> get catalogStream => _output.stream;

  CatalogBloc() {
    _input.stream.listen(_onEvent);
  }

  void _onEvent(CatalogEvent event) {
    if (event is AddCatalogItemEvent) {
      _catalogState.addToCatalog(event.item);
    } else if (event is RemoveCatalogItemEvent) {
      _catalogState.removeFromCatalog(event.item);
    }

    _output.add(_catalogState.catalog);
  }

  void dispose() {
    _input.close();
    _output.close();
  }
}

Explicación:

Importar dart:async ya que es requerido por StreamController, también importar el archivo de eventos, estado e ItemModal.

import 'dart:async';

import 'package:state_management/blocs/catalog/catalog_event.dart';
import 'package:state_management/blocs/catalog/catalog_state.dart';
import 'package:state_management/models/item_model.dart';

Crear una variable privada de tipo CatalogState().

CatalogState _catalogState = CatalogState();

Crear dos variables provadas de tipo StreamController().
En este caso el _output va a ser escuchado en más de un lugar de la vista, por lo cual debe ser un broadcast.
StreamController<List<ItemModel>>.broadcast();

StreamController<CatalogEvent> _input = StreamController();
StreamController<List<ItemModel>> _output = StreamController<List<ItemModel>>.broadcast();

Exponer sink y stream mediante getters, esto es para que la vistas puedan enviar eventos y escuchar los cambios.

StreamSink<CatalogEvent> get sendEvent => _input.sink;
Stream<List<ItemModel>> get catalogStream => _output.stream;

Crear un constructor que escuche los eventos.

CatalogBloc() {
  _input.stream.listen(_onEvent);
}

Crear un método privado _onEvent que se encargue de procesar los eventos que escucha el _input.stream.

void _onEvent(CatalogEvent event) {
  if (event is AddCatalogItemEvent) {
// Si el evento es de tipo AddCatalogItemEvent, ejecutar la función addToCatalog de _catalogState.
    _catalogState.addToCatalog(event.item);
  } else if (event is RemoveCatalogItemEvent) {
// Si el evento es de tipo RemoveCatalogItemEvent, ejecutar la función removeFromCatalog de _catalogState.
    _catalogState.removeFromCatalog(event.item);
  }

  _output.add(_catalogState.catalog);
}

Liberar los recursos de los StreamController() cuando no se los necesite con un dispose.

void dispose() {
  _input.close();
  _output.close();
}


Vistas

En esta sección se mostrará los cambios necesarios a realizar para que las vistas se actualicen usando el patrón BLoC.


user.dart

...

import 'package:state_management/blocs/user/user_bloc.dart';
import 'package:state_management/blocs/user/user_event.dart';
import 'package:state_management/blocs/user/user_state.dart';

...

TextEditingController _textEditingController;
UserBloc _userBloc = UserBloc();

@override
void initState() {
  super.initState();

  _textEditingController = TextEditingController(text: UserState().username);
  _userBloc.sendEvent.add(GetUsernameEvent());
}

@override
void dispose() {
  _userBloc.dispose();
  super.dispose();
}

...

title: StreamBuilder<String>(
    stream: _userBloc.userStream,
    builder: (context, snapshot) {
      return Text('User - ${snapshot.data}');
    }),

...

onChanged: (value) {
  _userBloc.sendEvent.add(OnChangeEvent(value));
},

...

Explicación:

Importar los archivos event, stats y bloc de usuario.

import 'package:state_management/blocs/user/user_bloc.dart';
import 'package:state_management/blocs/user/user_event.dart';
import 'package:state_management/blocs/user/user_state.dart';

Al sobre escribir initState() se debe colocar el nuevo código después de super.initState();
Al sobre escribir dispose() se debe colocar el nuevo código antes de super.dispose();

TextEditingController _textEditingController;
// Instanciar el user bloc
UserBloc _userBloc = UserBloc();

// Sobrescribir `initState` y enviar el evento `GetUsernameEvent` para actualizar el nombre de usuario al cambiar de página.
@override
void initState() {
  super.initState();

// Asignarle un valor inicial al TextEditingController, en este caso el nombre de usuario.
  _textEditingController = TextEditingController(text: UserState().username);
  _userBloc.sendEvent.add(GetUsernameEvent());
}

// Sobrescribir `dispose` para librerar los recursos del BLoC cuando ya no se usen.
@override
void dispose() {
  _userBloc.dispose();
  super.dispose();
}

Envolver el Text widget del AppBar en un StreamBuilder, de esta forma se actualizará de manera automática.

// Es necesario asignar el tipo de valor que se va a actualizar, en este caso String
title: StreamBuilder<String>(
// El stream que se pasa hace referencia a _output.stream
    stream: _userBloc.userStream,
    builder: (context, snapshot) {
// snapshot.data es el valor que se actualiza.
      return Text('User - ${snapshot.data}');
    }),

catalog.dart

...

import 'package:state_management/blocs/catalog/catalog_bloc.dart';
import 'package:state_management/blocs/catalog/catalog_event.dart';
import 'package:state_management/models/item_model.dart';

...

CatalogBloc _catalogBloc = CatalogBloc();

@override
void initState() {
  super.initState();
  _catalogBloc.sendEvent.add(GetCatalogEvent());
}

@override
void dispose() {
  _catalogBloc.dispose();
  super.dispose();
}

...

child: CircleAvatar(
  child: StreamBuilder<List<ItemModel>>(
      initialData: [],
      stream: _catalogBloc.catalogStream,
      builder: (context, snapshot) {
        return Text(
          '${snapshot.data.length}',
          style: TextStyle(
            fontWeight: FontWeight.bold,
            fontSize: 14.0,
          ),
        );
      }),

...

trailing: IconButton(
  onPressed: () {
    if (items[index].addedToCart) {
      widget._catalogBloc.sendEvent.add(
        RemoveCatalogItemEvent(items[index]),
      );
    } else {
      widget._catalogBloc.sendEvent.add(
        AddCatalogItemEvent(items[index]),
      );
    }
  },
),

...

Explicación:

Importar los archivos bloc y state de catalog además de item_model.

import 'package:state_management/blocs/catalog/catalog_bloc.dart';
import 'package:state_management/blocs/catalog/catalog_event.dart';
import 'package:state_management/models/item_model.dart';

Al sobre escribir initState() se debe colocar el nuevo código después de super.initState();
Al sobre escribir dispose() se debe colocar el nuevo código antes de super.dispose();

// Instanciar el catalog bloc
CatalogBloc _catalogBloc = CatalogBloc();

// Sobrescribir `initState` y enviar el evento `GetCatalogEvent` para actualizar el catálogo al cambiar de página.
@override
void initState() {
  super.initState();
  _catalogBloc.sendEvent.add(GetCatalogEvent());
}

// Sobrescribir `dispose` para librerar los recursos del BLoC cuando ya no se usen.
@override
void dispose() {
  _catalogBloc.dispose();
  super.dispose();
}

Envolver el Text widget en un StreamBuilder que contiene el número de items seleccionados.

child: CircleAvatar(

// En este caso el StreamBuilder es de tipo <List<ItemModel>>
  child: StreamBuilder<List<ItemModel>>(

// Se coloca una Lista vacía como data inicial para evitar errores al momento de hacer snapshot.data.length
      initialData: [],

// Pasar como stream el catalogStream del archivo catalog_bloc
      stream: _catalogBloc.catalogStream,
      builder: (context, snapshot) {
        return Text(

// snapshot.data.length es para obtener el número de items seleccionado
          '${snapshot.data.length}',
          style: TextStyle(
            fontWeight: FontWeight.bold,
            fontSize: 14.0,
          ),
        );
      }),

En este caso se ve widget._catalogBloc en lugar de solo
_catalogBloc, esto se debe a que se pasa _catalogBloc como atributo.

trailing: IconButton(
  onPressed: () {
    if (items[index].addedToCart) {

// Se envía el evento RemoveCatalogItemEvent y se pasa como parámetro el item actual
      widget._catalogBloc.sendEvent.add(
        RemoveCatalogItemEvent(items[index]),
      );
    } else {

// Se envía el evento AddCatalogItemEvent y se pasa como parámetro el item actual
      widget._catalogBloc.sendEvent.add(
        AddCatalogItemEvent(items[index]),
      );
    }
  },
),

cart.dart

...

import 'package:state_management/blocs/catalog/catalog_bloc.dart';
import 'package:state_management/blocs/catalog/catalog_event.dart';
import 'package:state_management/blocs/user/user_bloc.dart';
import 'package:state_management/blocs/user/user_event.dart';
import 'package:state_management/models/item_model.dart';

...

UserBloc _userBloc = UserBloc();
CatalogBloc _catalogBloc = CatalogBloc();

@override
void initState() {
  super.initState();
  _userBloc.sendEvent.add(GetUsernameEvent());
  _catalogBloc.sendEvent.add(GetCatalogEvent());
}

@override
void dispose() {
  _userBloc.dispose();
  _catalogBloc.dispose();
  super.dispose();
}

...

Expanded(
  child: StreamBuilder<List<ItemModel>>(
    initialData: [],
    stream: _catalogBloc.catalogStream,
    builder: (context, snapshot) {
      return ListView.builder(
        itemCount: snapshot.data.length,
        itemBuilder: (context, index) => ListTile(
          title: Text(
            '${snapshot.data[index].name}'.toUpperCase(),
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          trailing: Text(
            '\$${snapshot.data[index].price.toStringAsFixed(2)}',
          ),
        ),
      );
    },
  ),
),

...

StreamBuilder<String>(
  stream: _userBloc.userStream,
  builder: (context, snapshot) {
    return Column(
      children: [
        CircleAvatar(
          child: Text(
            getInitials('${snapshot.data}').toUpperCase(),
          ),
        ),
        Text(
          '${snapshot.data}',
          style: TextStyle(
            fontWeight: FontWeight.bold,
          ),
        ),
      ],
    );
  },
),

...

StreamBuilder<List<ItemModel>>(
  initialData: [],
  stream: _catalogBloc.catalogStream,
  builder: (context, snapshot) {
    return Text(
      '\$${formatTotal(snapshot.data)}',
      style: TextStyle(
        fontSize: 20.0,
        fontWeight: FontWeight.bold,
      ),
    );
  },
),

...

Explicación:

Importar los archivos bloc y state de user y catalog, además de item_model.

import 'package:state_management/blocs/catalog/catalog_bloc.dart';
import 'package:state_management/blocs/catalog/catalog_event.dart';
import 'package:state_management/blocs/user/user_bloc.dart';
import 'package:state_management/blocs/user/user_event.dart';
import 'package:state_management/models/item_model.dart';

Al sobre escribir initState() se debe colocar el nuevo código después de super.initState();
Al sobre escribir dispose() se debe colocar el nuevo código antes de super.dispose();

// Instanciar user y catalog bloc
UserBloc _userBloc = UserBloc();
CatalogBloc _catalogBloc = CatalogBloc();

// Sobrescribir `initState` y enviar el evento `GetUsernameEvent` `GetCatalogEvent` para actualizar el nombre y catálogo al cambiar de página.
@override
void initState() {
  super.initState();
  _userBloc.sendEvent.add(GetUsernameEvent());
  _catalogBloc.sendEvent.add(GetCatalogEvent());
}

// Sobrescribir `dispose` para librerar los recursos del BLoC cuando ya no se usen.
@override
void dispose() {
  _userBloc.dispose();
  _catalogBloc.dispose();
  super.dispose();
}

Envolver el ListView widget en un StreamBuilder para actualizar el title y trailing del ListTile.

Expanded(

// En este caso el StreamBuilder es de tipo <List<ItemModel>>
  child: StreamBuilder<List<ItemModel>>(

// Se coloca una Lista vacía como data inicial para evitar errores al momento de hacer snapshot.data.length
    initialData: [],

// Pasar como stream el catalogStream del archivo catalog_bloc
    stream: _catalogBloc.catalogStream,
    builder: (context, snapshot) {
      return ListView.builder(

// snapshot.data.length es para obtener el número de items seleccionado
        itemCount: snapshot.data.length,
        itemBuilder: (context, index) => ListTile(
          title: Text(

// snapshot.data[index].name nombre del producto.
            '${snapshot.data[index].name}'.toUpperCase(),
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          trailing: Text(

// snapshot.data[index].price.toStringAsFixed(2) precio del producto con dos decimales
            '\$${snapshot.data[index].price.toStringAsFixed(2)}',
          ),
        ),
      );
    },
  ),
),

Envolver el Column widget en un StreamBuilder para actualizar el nombre de usuario.

// En este caso el StreamBuilder es de tipo <String>
StreamBuilder<String>(

// Pasar como stream el userStream del archivo user_bloc
  stream: _userBloc.userStream,
  builder: (context, snapshot) {
    return Column(
      children: [
        CircleAvatar(
          child: Text(

// Muestra las iniciales del nombre de usuario
            getInitials('${snapshot.data}').toUpperCase(),
          ),
        ),
        Text(

// Muestra el nombre de usuario
          '${snapshot.data}',
          style: TextStyle(
            fontWeight: FontWeight.bold,
          ),
        ),
      ],
    );
  },
),

Envolver el Text widget en un StreamBuilder para actualizar el precio total de los items seleccionados.

// En este caso el StreamBuilder es de tipo <List<ItemModel>>
StreamBuilder<List<ItemModel>>(

// Se coloca una Lista vacía como data inicial para evitar errores al momento de hacer snapshot.data
  initialData: [],

// Pasar como stream el catalogStream del archivo catalog_bloc
  stream: _catalogBloc.catalogStream,
  builder: (context, snapshot) {
    return Text(

// Muestra el precio total de los items seleccionados
      '\$${formatTotal(snapshot.data)}',
      style: TextStyle(
        fontSize: 20.0,
        fontWeight: FontWeight.bold,
      ),
    );
  },
),


Eso es todo, recuerda que código de esta guía se lo puede encontrar en GitHub.

GitHub logo marcedroid / Flutter-State-Management-Demos

Manejo de estados en Flutter

Top comments (2)

Collapse
 
maiconcrespo profile image
Maicon Crespo

Great Man, la mejor explicacion que he encontrado hasta ahora para entender bloc, para iniciantes como yo, muy simple , claro y dinamico, com mas practicas seguro que aprendo, y con el apoyo de esta page, y otra sin librerias...wow...es lo mejor. jajaaj

Collapse
 
jovanymezura profile image
Raul Jovany

Me ayudo mucho esta guía gracias por compartir con esto empiezo a poner los pies sobre la tierra acerca del patron bloc y flutter. Con la practica seguro que todo se aclarara. Saludos