Patrones de diseño con Flutter: 10— Proxy

Paolo Pinto
6 min readApr 20, 2024

--

Es un hecho que cuando vamos creciendo nuestro ingresos nacen nuevas necesidades. El problema surge cuando necesitamos de una tarjeta de credito, asi no llevamos un manojo de billetes. La observación es ¿Si una tarjeta de credito se utiliza tanto como dinero en efectivo en pagos nos dice algo como programadores?

Esta solución surgió desde el problema de tener un objeto que consume muchos recursos del sistema. Buscas soluciones, pero te estrellas con el caso de uso de que es una libreria de terceros. Al entender este problema sabemos que necesitamos el objeto, pero solo de vez en cuando.

¿Que nos ofrece el patron Proxy?

Proxy como patrón estructural nos permite abstraer un sustituto para otro objeto. Esto nos ayuda a controlar el acceso al objeto original, permitiéndote hacer algo antes o después de que la solicitud llegue al objeto original.

fuente: refactoring.guru

Recordando algo de asincronismo una frase clave es “ en diferido”. El proxy se camufla como objeto de la base de datos. Gestiona la inicialización diferida y el caché de resultados sin que el cliente o el objeto real de la base de datos lo sepan.

¿Como lo implementaremos en Flutter?

Nos inspiraremos de su estructura teórica.

  1. ServiceInterface: Sera la abstracción que nos permitira camuflar al Proxy
  2. Service: Es el objeto o servicio lógica de negocio útil y pesado. Lo que nos ocasiona problemas.
  3. Proxy: Are u kiddin me?
  4. Client: En aqui ya funcionan servicios y proxies en la misma interfaz, es decir, se puede usar un proxy a cualquier código que espere un objeto de servicio.

Listado de Clientes

En el siguiente ejemplo veremos un listado de clientes que al hacer clic en “ver info” hace la petición, aparece el loading, la información aparece. Cuando se repite el proceso en “ver info” la información ya esta ahi. No hace falta repetir la petición a la base de datos.

Ahora veremos como esto se aplica a nuestro ejemplo en comparación al diagrama original:

  1. Customer | CustomerDetails: Información extra para el ejemplo
// Customer
class Customer {
Customer()
: id = faker.guid.guid(),
name = faker.person.name();

final String id;
final String name;
CustomerDetails? details;
}

// CustomerDetails
class CustomerDetails {
const CustomerDetails({
required this.customerId,
required this.email,
required this.hobby,
required this.position,
});

final String customerId;
final String email;
final String hobby;
final String position;
}

2. ServiceInterface: ICustomerDetailsInterface

abstract interface class ICustomerDetailsService {
Future<CustomerDetails> getCustomerDetails(String id);
}

3. Service: CustomerDetailsService

Simulamos la peticion a una API con Future.delayed

class CustomerDetailsService implements ICustomerDetailsService {
const CustomerDetailsService();

@override
Future<CustomerDetails> getCustomerDetails(String id) => Future.delayed(
const Duration(seconds: 2),
() => CustomerDetails(
customerId: id,
email: faker.internet.email(),
hobby: faker.sport.name(),
position: faker.job.title(),
),
);
}

4. Proxy: CustomerDetailsServiceProxy

class CustomerDetailsServiceProxy implements ICustomerDetailsService {
CustomerDetailsServiceProxy(this.service);

final ICustomerDetailsService service;
final Map<String, CustomerDetails> customerDetailsCache = {};

@override
Future<CustomerDetails> getCustomerDetails(String id) async {
if (customerDetailsCache.containsKey(id)) return customerDetailsCache[id]!;

final customerDetails = await service.getCustomerDetails(id);
customerDetailsCache[id] = customerDetails;

return customerDetails;
}
}

5. Client: Código Cliente

ProxyExample

Widget para renderizar la información y utilizar nuestro Proxy.

class ProxyExample extends StatefulWidget {
const ProxyExample();

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

class _ProxyExampleState extends State<ProxyExample> {
// PROXY!! :D
final _customerDetailsServiceProxy = CustomerDetailsServiceProxy(
const CustomerDetailsService(),
);

final _customerList = List.generate(10, (_) => Customer());

void _showCustomerDetails(Customer customer) => showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => CustomerDetailsDialog(
service: _customerDetailsServiceProxy,
customer: customer,
),
);

@override
Widget build(BuildContext context) {
return ScrollConfiguration(
behavior: const ScrollBehavior(),
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: LayoutConstants.paddingL,
),
child: Column(
children: <Widget>[
Text(
'Presiona en el item de la lista para ver más información',
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: LayoutConstants.spaceL),
for (final customer in _customerList)
Card(
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.grey,
child: Text(
customer.name[0],
style: const TextStyle(color: Colors.white),
),
),
trailing: const Icon(Icons.info_outline),
title: Text(customer.name),
onTap: () => _showCustomerDetails(customer),
),
),
],
),
),
);
}
}

CustomerDetailsColumn

Widget para Renderizar el Customer y sus datos personales

class CustomerDetailsColumn extends StatelessWidget {
final CustomerDetails customerDetails;

const CustomerDetailsColumn({
required this.customerDetails,
});

@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
CustomerInfoGroup(
label: 'E-mail',
text: customerDetails.email,
),
const SizedBox(height: 42),
CustomerInfoGroup(
label: 'Position',
text: customerDetails.position,
),
const SizedBox(height: 42),
CustomerInfoGroup(
label: 'Hobby',
text: customerDetails.hobby,
),
],
);
}
}

CustomerInfoGroup

Widget para darle estilos a la etiqueta(campo) y a su texto.

class CustomerInfoGroup extends StatelessWidget {
final String label;
final String text;

const CustomerInfoGroup({
required this.label,
required this.text,
});

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
label,
style: Theme.of(context).textTheme.titleSmall,
),
Text(
text,
style: Theme.of(context).textTheme.titleMedium,
),
],
);
}
}

Extra: PlatformButton

Widget que nos permite implementar un boton de distino estilo detectando si estamos en android o IOS(Material o cupertino). Este es implementado con los patrones (estructurales y creacional)Bridge y FactoryMethod.

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),
),
);
}
}

CustomerDetailsDialog

Lo que hacemos al hacer click en el item con ListTile es lanzar un dialog con _showCustomerDetails()

class CustomerDetailsDialog extends StatefulWidget {
const CustomerDetailsDialog({
required this.customer,
required this.service,
});

final Customer customer;
// No le decimos si es Proxy o el Original
final ICustomerDetailsService service;

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

class _CustomerDetailsDialogState extends State<CustomerDetailsDialog> {
@override
void initState() {
super.initState();

// info desde el padre (id de cliente)
widget.service.getCustomerDetails(widget.customer.id).then(
(CustomerDetails customerDetails) => setState(() {
widget.customer.details = customerDetails;
}),
);
}

void _closeDialog() => Navigator.of(context).pop();

@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.customer.name),
content: SizedBox(
height: 200.0,
child: widget.customer.details == null
? Center(
child: CircularProgressIndicator(
backgroundColor: lightBackgroundColor,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.black.withOpacity(0.65),
),
),
)
: CustomerDetailsColumn(
customerDetails: widget.customer.details!,
),
),
actions: <Widget>[
Visibility(
visible: widget.customer.details != null,
child: PlatformButton(
materialColor: Colors.black,
materialTextColor: Colors.white,
onPressed: _closeDialog,
text: 'Cerrar',
),
),
],
);
}

La magia sucede en el Dialog, si notas en el inicio del widget, estamos declarando al servicion como Interface ICustomerDetailsService. No le importa si es o no Proxy. Se refleja una mejora en el performance de la App.

y YA!

Creacionales:

Estructurales:

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

--

--