Manejo de estados en flutter, BLoC Pattern sin librerías
Tags: dart, flutter
Published: 2020-09-04 23:30:29

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](https://user-images.githubusercontent.com/2531939/92287705-259ee500-eed0-11ea-8c3a-0d027aec1a6f.gif)

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

{% github marcedroid/Flutter-State-Management-Demos no-readme %}

## 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] (#patrón-bloc-para-nombre-de-usuario)
 - [user_event.dart] (#usereventdart)
 - [user_state.dart] (#userstatedart)
 - [user_bloc.dart] (#userblocdart)
- [Patrón BLoC para catálogo] (#patrón-bloc-para-catálogo)
 - [catalog_event.dart] (#catalogeventdart)
 - [catalog_state.dart] (#catalogstatedart)
 - [catalog_bloc.dart] (#catalogblocdart)
- [Vistas] (#vistas)
 - [user.dart] (#userdart)
 - [catalog.dart] (#catalogdart)
 - [cart.dart] (#cartdart)

------------
------------

## 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
```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.
```dart
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`.
```dart
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.
```dart
class GetUsernameEvent extends UserEvent {}
```

------------

### user_state.dart
```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.
```dart
String _username = 'Guest';
```

Necesitamos tener una instancia que sea compartida y que solo pueda ser instanciada una vez (singleton).
```dart
// 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.
```dart
String get username => _username;
```

Por último crear el método para actualizar el nombre de usuario.
```dart
void onChange(value) {
  _username = value;
}
```

------------

### user_bloc.dart
```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.
```dart
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()`.
```dart
UserState _userState = UserState();
```

Crear dos variables privadas de tipo `StreamController()`.
```dart
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.
```dart
StreamSink<UserEvent> get sendEvent => _input.sink;
Stream<String> get userStream => _output.stream;
```

Crear un constructor que escuche los eventos.
```dart
UserBloc() {
  _input.stream.listen(_onEvent);
}
```

Crear un método privado `_onEvent` que se encargue de procesar los eventos que escucha el `_input.stream`.
```dart
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.
```dart
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
```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.
```dart
import 'package:state_management/models/item_model.dart';
```

Crear una clase abstracta de la cual van a extender los otros eventos.
```dart
abstract class CatalogEvent {}
```

Definir un evento para agregar un item al carrito, recibe como parámetro un ItemModel.
```dart
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.
```dart
class RemoveCatalogItemEvent extends CatalogEvent {
  final ItemModel item;

  RemoveCatalogItemEvent(this.item);
}
```

Definir un evento que se ejecutará para obtener el listado del catálogo.
```dart
class GetCatalogEvent extends CatalogEvent {}
```

------------

### catalog_state.dart
```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.
```dart
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.
```dart
List<ItemModel> _catalog = [];
```

Necesitamos tener una instancia que sea compartida y que solo pueda ser instanciada una vez (singleton).
```dart
// 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
```dart
List<ItemModel> get catalog => _catalog;
```

Crear el método que agrega un item al carrito.
```dart
void addToCatalog(ItemModel itemModel) {
  _catalog.add(itemModel);
}
```

Crear el método que elimina un item del carrito.
```dart
void removeFromCatalog(ItemModel itemModel) {
  _catalog.remove(itemModel);
}
```

------------

### catalog_bloc.dart
```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.
```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';
```

Crear una variable privada de tipo `CatalogState()`.
```dart
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();`
```dart
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.
```dart
StreamSink<CatalogEvent> get sendEvent => _input.sink;
Stream<List<ItemModel>> get catalogStream => _output.stream;
```

Crear un constructor que escuche los eventos.
```dart
CatalogBloc() {
  _input.stream.listen(_onEvent);
}
```

Crear un método privado `_onEvent` que se encargue de procesar los eventos que escucha el `_input.stream`.
```dart
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.
```dart
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
```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.
```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';
```

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();`
```dart
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.
```dart
// 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
```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.
```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';
```

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();`
```dart
// 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.
```dart
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.
```dart
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

```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.
```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';
```

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();`
```dart
// 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**.
```dart
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.
```dart
// 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.
```dart
// 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 marcedroid/Flutter-State-Management-Demos no-readme %}