Patrones de diseño con Flutter: 1 — Singleton

Paolo Pinto
5 min readApr 12, 2024

--

Alguna vez estuve pensando de forma descabellada el por qué un país no puede tener más de un gobierno oficial. Esto evidentemente es una potencial mala práctica. La cuestión es ¿Como esta analogía se nos presentó cuando hacíamos software?

En el desarrollo de cualquier proyecto, flutter en nuestro caso, se nos presentan casos particulares. Cuando intentamos guardar una pequeña configuración local, dark mode, o guardar el nombre de nuestro usuario esto normalmente se solucionaría con el package shared-preferences. Así al utilizarlo vemos que es poco escalable y mantenible con el tiempo.

¿Que se puede hacer?

Singleton es un patrón de diseño creacional que nos permite asegurarnos de que una clase tenga una única instancia, a la vez que proporciona un punto de acceso global a dicha instancia.

fuente: refactoring.guru

Como es de esperarse este patron tiene una fuerte conexion con la POO, pero no te preocupes que aqui te dejo el diagrama, que en este caso no es largo, en donde puedes aprender como funciona.

diagrama de clases patron singleton

Se podría decir que es una variable que se llama a si misma.

ejemplo serio patron singleton

Cada aplicación va creciendo con el tiempo y los requerimientos varian constantemente, los patrones de diseño fueron creados para darnos una solucion limpia y escalable. Si queremos aprender de forma práctica tendriamos que poner a prueba ciertos casos de estudio. En este articulo te muestro algunos.

Caso A. Texto Global

  1. First we define the base clase
import 'package:flutter/foundation.dart';

/*
La palabra "Base" en el nombre de la clase indica que esta clase está destinada a ser
heredada por otras clases. Es una convención común en la programación orientada a objetos
llamar "Base" a las clases que se utilizan como base para la herencia. Esto ayuda a indicar
la intención de la clase y a hacer el código más fácil de entender.
*/


base class ExampleStateBase {
@protected
late String initialText;
@protected
late String stateText;
String get currentText => stateText;

// ignore: use_setters_to_change_properties
void setStateText(String text) {
stateText = text;
}

void reset() {
stateText = initialText;
}
}

2. Then we Implement the state based on the base class

import 'example_state_base.dart';

final class ExampleState extends ExampleStateBase {
static final ExampleState _instance = ExampleState._internal();

factory ExampleState() {
return _instance;
}

ExampleState._internal() {
initialText = "A new 'ExampleState' instance has been created.";
stateText = initialText;
}
}

3. example state by the definition

import 'example_state_base.dart';

final class ExampleStateByDefinition extends ExampleStateBase {
static ExampleStateByDefinition? _instance;

ExampleStateByDefinition._internal() {
initialText = "A new 'ExampleStateByDefinition' instance has been created.";
stateText = initialText;
}

// ignore: prefer_constructors_over_static_methods
static ExampleStateByDefinition getState() {
return _instance ??= ExampleStateByDefinition._internal();
}
}

3. the implementation in the 3 ways

import 'package:flutter/material.dart';

import '../../../constants/constants.dart';
import '../../../design_patterns/singleton/singleton.dart';
import '../../platform_specific/platform_button.dart';
import 'singleton_example_card.dart';

class SingletonExample extends StatefulWidget {
const SingletonExample();

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

class _SingletonExampleState extends State<SingletonExample> {
final List<ExampleStateBase> stateList = [
ExampleState(),
ExampleStateByDefinition.getState(),
ExampleStateWithoutSingleton(),
];

void _setTextValues([String text = 'Singleton']) {
for (final state in stateList) {
state.setStateText(text);
}
setState(() {});
}

void _reset() {
for (final state in stateList) {
state.reset();
}
setState(() {});
}

@override
Widget build(BuildContext context) {
return ScrollConfiguration(
behavior: const ScrollBehavior(),
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: LayoutConstants.paddingL,
),
child: Column(
children: <Widget>[
for (final state in stateList)
Padding(
padding: const EdgeInsets.only(
bottom: LayoutConstants.paddingL,
),
child: SingletonExampleCard(text: state.currentText),
),
const SizedBox(height: LayoutConstants.spaceL),
PlatformButton(
materialColor: Colors.black,
materialTextColor: Colors.white,
onPressed: _setTextValues,
text: "Change states' text to 'Singleton'",
),
PlatformButton(
materialColor: Colors.black,
materialTextColor: Colors.white,
onPressed: _reset,
text: 'Reset',
),
const SizedBox(height: LayoutConstants.spaceXL),
const Text(
"Note: change states' text and navigate the application (e.g. go to main menu, then go back to this example) to see how the Singleton state behaves!",
textAlign: TextAlign.justify,
),
],
),
),
);
}
}

4. Don’t forget the card

import 'package:flutter/material.dart';

import '../../../constants/constants.dart';

class SingletonExampleCard extends StatelessWidget {
final String text;

const SingletonExampleCard({
required this.text,
});

@override
Widget build(BuildContext context) {
return Card(
child: Container(
height: 64.0,
padding: const EdgeInsets.all(LayoutConstants.paddingL),
child: Center(
child: Text(
text,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
),
),
);
}
}

Thats all, this is your first singleton!

Caso B. Shared Preferences

Imagina que tienes que implementar un SplashScreen y debes manejar una ruta predefinida, para esto Flutter con una variable en initialRoute basta pero tiene que guardarse en el estado de la app.

Se te puede presentar que al acceder a la variable en diferentes Widgets tiende a ensuciar tu codigo. El patron Singleton combinado con el package SharedPreferences luce como una solución viable. Veamos en el ejemplo:

1. Creamos nuestras clase “Preferencias de Usuario”

Observe que utilizamos UserPreferences._internal() en la variable _instancia el cual es un constructor privado, significa que no se podra instanciar esta clase fuera de si misma.

Ademas utilizamos un Constructor factory, esto quiere decir:

  • Un constructor factory No siempre crea un objeto nuevo.

El constructor factory Puede ser útil para:

  • Ahorrar tiempo reutilizando objetos existentes.
  • Elegir el tipo de objeto más adecuado para cada caso.
import 'package:shared_preferences/shared_preferences.dart';

class UserPreferences {

static final UserPreferences _instancia = new UserPreferences._internal();

factory UserPreferences() {
return _instancia;
}

UserPreferences._internal();

SharedPreferences? _prefs;

initPrefs() async {
this._prefs = await SharedPreferences.getInstance();
}
// definicion de variable defaultRoute
String get deafultRoute {
return _prefs?.getString('deafultRoute') ?? '/login';
}
// setter para cambiar/actualizar su valor
set deafultRoute(String value ) {
_prefs?.setString('deafultRoute', value);
}
}

2. Iniciamos nuestras Preferencias de Usuario en el main.dart como Singleton(solo una instancia).

void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Inicializa Preferencias de Usuario
final prefs = UserPreferences();
await prefs.initPrefs();
runApp(const MyApp());
}

3. Accedemos a las variables

Ahora cada vez que querramos acceder al valor en cualquier parte de nuestra aplicacion solo debemos hacer la llamada a UserPreferences() unica instancia.

ElevatedButton(
onPressed: () {
final email = emailController.text;
final password = passwordController.text;
_auth.createUserWithEmailAndPassword(email, password).then((value) {
final prefs = UserPreferences();
prefs.deafultRoute = '/layout';
Navigator.pushNamed(context, '/layout');
});
},
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.purple),
minimumSize: MaterialStateProperty.all(const Size(double.infinity, 46))
),
child: const Text('Registrarme'),
);

y YA. Happy Coding!

Otros articulos para esta serie

Creacionales:

Estructurales:

  • Pronto…

De Comportamiento:

  • Pronto…..

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

--

--