Patrones de diseño con Flutter: 15 — State

Paolo Pinto
6 min readApr 26, 2024

--

Es un patron que nace desde el dilema de que en cualquier momento dado, un programa puede encontrarse en un número finito de estados. Sin embargo, dependiendo de un estado actual, el programa puede cambiar o no a otros estados. ¿Como podríamos hacer que un objeto “aparente” cambiar su clase?

Se dice que patrón State está estrechamente relacionado con el concepto de la Máquina de estados finitos, estos representados en un grafo.

Una Máquina de Estado Finito (Finite State Machine), llamada también Autómata Finito es una abstracción computacional que describe el comportamiento de un sistema reactivo mediante un número determinado de Estados y un número determinado de Transiciones entre dicho Estados.

Nosotros describimos el comportamiento de nuestro automata de forma sofisticada y en un numero finito de “pasos”. Describir que es lo que hara de inicio a fin.

Se implmentan normalmente con muchos operadores condicionales (if o switch) que definen el comportamiento adecuado dependiendo del estado actual del objeto. Te suena algo como esto?

class Document is
field state: string
// ...
method publish() is
switch (state)
"draft":
state = "moderation"
break
"moderation":
if (currentUser.role == "admin")
state = "published"
break
"published":
// No hacer nada.
break
// ...

Esta estructura es propensa no escalar de una forma adecuda y a terminar rapidamente en un contenedor de condicionales monstruosos.

Se te puede presentar que con la evolución del proyecto este empeore. Es bastante difícil predecir todos los estados y transiciones posibles en la etapa de diseño. Por ello, una máquina de estados esbelta, creada con un grupo limitado de condicionales, puede crecer hasta convertirse en un desastre con el tiempo.

¿Que podemos hacer con el patron State?

State nos permite a un objeto alterar su comportamiento cuando su estado interno cambia. Parece como si el objeto cambiara su clase.

Este patron nos sugiere crear nuevas clases para todos los estados posibles de un objeto y extraer todos los comportamientos específicos del estado para colocarlos dentro de esas clases.

El patron se consigue almacenando una referencia a uno de los objetos de estado que representa su estado actual y delega todo el trabajo relacionado con el estado a ese objeto.

Una analogía

Los botones de tu celular se comportan de forma diferente dependiendo del estado actual:

Cuando está desbloqueado, al pulsar botones se ejecutan varias funciones.

Cuando está bloqueado, pulsar un botón desbloquea la pantalla.

Cuando la batería está baja, pulsar un botón muestra la pantalla de carga.

¿Como lo implementaremos en Flutter?

as

diagrama de clases patron state

Context:

Almacena una referencia a uno de los objetos de ConcreteState y le delega todo el trabajo específico del estado(State). El contexto se comunica con el objeto de estado a través de la interfaz de estado(State). El contexto expone un “setter” para pasarle un nuevo objeto de estado.

State(interface):

Declara los métodos específicos del estado. Estos deben tener sentido para todos los estados concretos, porque no querrás que uno de tus estados tenga métodos inútiles que nunca son invocados.

ConcreteState:

Proporcionan sus propias implementaciones para los métodos específicos del estado. Para evitar la duplicación de código similar a través de varios estados, puedes incluir clases abstractas intermedias que encapsulen algún comportamiento común.

Nuestro ejemplo tratara de Simular el traer datos de una Api de Nombres

Para este ejemplo utilizaremos el package faker para valores aleatorios.

FakeApi: Se utiliza una API falsa para generar aleatoriamente una lista de nombres de personas.

class FakeApi {
const FakeApi();
Future<List<String>> getNames() => Future.delayed(
const Duration(seconds: 2),
() {
if (random.boolean()) return _getRandomNames();
throw Exception('Unexpected error');
},
);
List<String> _getRandomNames() => List.generate(
random.boolean() ? 3 : 0,
(_) => faker.person.name(),
);
}

State<interface>: IState

abstract interface class IState {
Future<void> nextState(StateContext context);
Widget render();
}

Context: StateContext

class StateContext {
final _stateStream = StreamController<IState>();
Sink<IState> get _inState => _stateStream.sink;
Stream<IState> get outState => _stateStream.stream;

late IState _currentState;

StateContext() {
_currentState = const NoResultsState();
_addCurrentStateToStream();
}

void dispose() {
_stateStream.close();
}

void setState(IState state) {
_currentState = state;
_addCurrentStateToStream();
}

void _addCurrentStateToStream() {
_inState.add(_currentState);
}

Future<void> nextState() async {
await _currentState.nextState(this);

if (_currentState is LoadingState) {
await _currentState.nextState(this);
}
}
}

ConcreteStates: NoResultsState | ErrorState | LoadingState | LoadedState

  • ErrorState
class ErrorState implements IState {
const ErrorState();

@override
Future<void> nextState(StateContext context) async {
context.setState(const LoadingState());
}

@override
Widget render() {
return const Text(
'Oops! Something went wrong...',
style: TextStyle(
color: Colors.red,
fontSize: 24.0,
),
textAlign: TextAlign.center,
);
}
}
  • LoadedState
class LoadedState implements IState {
const LoadedState(this.names);

final List<String> names;

@override
Future<void> nextState(StateContext context) async {
context.setState(const LoadingState());
}

@override
Widget render() {
return Column(
children: names
.map(
(name) => Card(
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.grey,
foregroundColor: Colors.white,
child: Text(name[0]),
),
title: Text(name),
),
),
)
.toList(),
);
}
}
  • NoResultsState

class NoResultsState implements IState {
const NoResultsState();

@override
Future<void> nextState(StateContext context) async {
context.setState(const LoadingState());
}

@override
Widget render() {
return const Text(
'No Results',
style: TextStyle(fontSize: 24.0),
textAlign: TextAlign.center,
);
}
}
  • LoadingState
// LoadingState
class LoadingState implements IState {
const LoadingState({
this.api = const FakeApi(),
});

final FakeApi api;

@override
Future<void> nextState(StateContext context) async {
try {
final resultList = await api.getNames();

context.setState(
resultList.isEmpty ? const NoResultsState() : LoadedState(resultList),
);
} on Exception {
context.setState(const ErrorState());
}
}

@override
Widget render() {
return const CircularProgressIndicator(
backgroundColor: Colors.transparent,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.black,
),
);
}
}

Client:

Extra: PlatformButton

Boton que identifica en que plataforma estamos(IOS o Android o Web). Esto para renderizar ya sea desde Material o Cupertino.

import 'dart:io';

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class PlatformButton extends StatelessWidget {
final String text;
final Color materialColor;
final Color materialTextColor;
final VoidCallback? onPressed;

const PlatformButton({
required this.text,
required this.materialColor,
required this.materialTextColor,
this.onPressed,
});

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(4.0),
child: kIsWeb || Platform.isAndroid
? MaterialButton(
color: materialColor,
textColor: materialTextColor,
disabledColor: Colors.grey,
disabledTextColor: Colors.white,
onPressed: onPressed,
child: Text(text, textAlign: TextAlign.center),
)
: CupertinoButton(
color: Colors.black,
onPressed: onPressed,
child: Text(text, textAlign: TextAlign.center),
),
);
}
}

StateExample

StateExample solo conoce la clase de estado inicial: NoResultsState, pero no conoce ningún detalle sobre otros estados posibles, ya que su manejo se define en la clase StateContext.

Esto permite separar la lógica del negocio de la UI y agregar nuevos estados de tipo IState a la aplicación sin aplicar ningún cambio a los componentes de la interfaz de usuario.

class StateExample extends StatefulWidget {
const StateExample();

@override
_StateExampleState createState() => _StateExampleState();
}

class _StateExampleState extends State<StateExample> {
final _stateContext = StateContext();

Future<void> _changeState() async {
await _stateContext.nextState();
}

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

@override
Widget build(BuildContext context) {
return ScrollConfiguration(
behavior: const ScrollBehavior(),
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: LayoutConstants.paddingL,
),
child: Column(
children: <Widget>[
PlatformButton(
materialColor: Colors.black,
materialTextColor: Colors.white,
onPressed: _changeState,
text: 'Cargar Nombres',
),
const SizedBox(height: LayoutConstants.spaceL),
StreamBuilder<IState>(
initialData: const NoResultsState(),
stream: _stateContext.outState,
builder: (context, snapshot) => snapshot.data!.render(),
),
],
),
),
);
}
}

y YA!

Otros articulos para esta serie

Creacionales:

Estructurales:

De Comportamiento:

Tu contribución

👏 ¡Presiona el botón de aplaudir a continuación para mostrar tu apoyo y motivarme a escribir mejor!
💬 Deje una respuesta a este artículo brindando sus ideas, comentarios o deseos para la serie.
📢 Comparte este artículo con tus amigos y colegas en las redes sociales.
➕ Sígueme en Medium.
⭐ Ve los ejemplos prácticos desde mi canal en Youtube: Pacha Code

--

--