Patrones de diseño con Flutter: 6— Adapter

Paolo Pinto
5 min readApr 20, 2024

--

Cuando viajamos comenzamos a viajar otros países llevamos con nosotros siempre nuestra laptop, cámara, celular. El problema surge cuando al querer cargar cada una y vemos la necesidad de comprar un adaptador. Decimos ¿Existen algo como adaptadores en la programación?

La respuesta es que sí, por eso estas leyendo esto, este patrón se presenta desde la necesidad cuando necesitamos adaptar información. Nuestras fuentes varian siempre, es asi que cuando necesitamos de un adaptador. Pero esto no podría salir siempre bien.

Es cuando lo implementamos tratar de tomar la mejor decisión. El patron adapter funciona bien si sabemos como y cuando utilizarlo. Esto implica que no descompongamos parte del código existente.

fuente: refactoring.guru

¿ Que nos ofrece el patron adapter?

Adapter es un patrón de diseño estructural que permite la colaboración entre objetos con interfaces incompatibles.

Se nos puede presentar el escenario en que estamos extrayendo información de una empresa que usa XML. Pero la libreria(package) que usa esta empresa es en JSON. No puedes modificar la libreria(package) ya que no es tuya y no tienes acceso. ¿Que hacemos?

La respuesta es correcta, usamos el patrón adapter para no modificar el código existente. Pero hay que hacerlo de la forma correcta.

inyección del patrón adapter

Se te pueden presentar otros escenario que no impliquen conversión de datos. También ayudan a objetos con distintas interfaces a colaborar. En ocasiones se puede incluso crear un adaptador de dos direcciones que pueda convertir las llamadas en ambos sentidos.

Ya pasando a la implementación, puedes hacerlo con diferentes principios:

1. Adaptador de objetos(composición de objetos)

El adaptador implementa la interfaz de un objeto y envuelve el otro. Interprétalo como el intermediario de comunicación entre la logica del negoció y el servicio de terceros al que no tenemos acceso.

2. Clase adaptadora(herencia)

La clase adaptadora hereda interfaces de ambos objetos al mismo tiempo. Solo para lenguajes que soporten herencia múltiple, no aplicable en Flutter.

¿Como lo implementaremos?

En el siguiente diagrama de clases vemos

Antes de comenzar, utilizaremos el package xml para poder trabajar con xml desde dart.

Extra Contact: Clase simple de contacto

class Contact {
final String name;
final String email;
final bool favourite;
const Contact({
required this.name,
required this.email,
required this.favourite
});
}

Service 1: JsonContactsApi

class JsonContactsApi {
static const _contactsJson = '''{
"contacts": [
{
"name": "Pepito Perez(JSON)",
"email": "pepito@pepito.com",
"favourite": true
},
{
"name": "Juanito Paredes(JSON)",
"email": "juanito@juanito.com",
"favourite": false
},
{
"name": "Michael Scott(JSON)",
"email": "michael@michael.com",
"favourite": false
}
]
}''';

const JsonContactsApi();

String getContactsJson() => _contactsJson;
}

Service 2: XmlContactsApi

class XmlContactsApi {
static const _contactsXml = '''
<?xml version="1.0"?>
<contacts>
<contact>
<fullname>Pepito Perez (XML)</fullname>
<email>pepito@xml.com</email>
<favourite>false</favourite>
</contact>
<contact>
<fullname>Juanito Paredes (XML)</fullname>
<email>juanito@xml.com</email>
<favourite>true</favourite>
</contact>
<contact>
<fullname>Michael Scott (XML)</fullname>
<email>michael@xml.com</email>
<favourite>true</favourite>
</contact>
</contacts>''';

const XmlContactsApi();
String getContactsXml() => _contactsXml;
}

Client Interface: IContactsAdapter

La interfaz(interface) unifica los adaptadores y les exige que implementen el método getContacts().

abstract interface class IContactsAdapter {
List<Contact> getContacts();
}

Adapter 1: XmlContactsAdapter

Implementa a su manera el método getContacts()

class XmlContactsAdapter implements IContactsAdapter {
const XmlContactsAdapter({
this.api = const XmlContactsApi()
});

final XmlContactsApi api;

@override
List<Contact> getContacts() {
final contactsXml = api.getContactsXml();
final contactsList = <Contact>[];

for(final xmlElement in XmlDocument.parse(contactsXml).findAllElements('contact')) {
final name = xmlElement.findElements('fullname').single.innerText;
final email = xmlElement.findElements('email').single.innerText;
final favouriteString = xmlElement.findElements('favourite').single.innerText;
final favourite = favouriteString.toLowerCase() == 'true';

contactsList.add(Contact(
name: name,
email: email,
favourite: favourite
));
}

return contactsList;
}

}

Adapter 2: JsonContactsAdapter

import 'dart:convert';

class JsonContactsAdapter implements IContactsAdapter {
const JsonContactsAdapter({
this.api = const JsonContactsApi(),
});

final JsonContactsApi api;

@override
List<Contact> getContacts() {
final contactsJson = api.getContactsJson();
final contactsList = _parseContactsJson(contactsJson);

return contactsList;
}

List<Contact> _parseContactsJson(String contactsJson) {
final contactsMap = json.decode(contactsJson) as Map<String, dynamic>;
final contactsJsonList = contactsMap['contacts'] as List;
final contactsList = contactsJsonList.map((json) {
final contactJson = json as Map<String, dynamic>;
return Contact(
name: contactJson['name'] as String,
email: contactJson['email'] as String,
favourite: contactJson['favourite'] as bool,
);
}).toList();

return contactsList;
}
}

Client: Codigo Cliente

ContactsSection

Aquí es donde sucede la magia, lo declaramos a adapter IContactsAdapter ya que al widget no le interesa cual adaptador estamos usando y lo mejor es que la lógica del negocio sigue siendo la misma.

class ContactsSection extends StatefulWidget {
const ContactsSection({
super.key,
required this.adapter,
required this.headerText,
});
final IContactsAdapter adapter;
final String headerText;

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

class _ContactsSectionState extends State<ContactsSection> {
final List<Contact> contacts = [];

void _getContacts() {
setState(() {
contacts.addAll(widget.adapter.getContacts());
});
}

@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Text(widget.headerText),
ElevatedButton(
onPressed: _getContacts,
child: const Text('Obtener contactos'),
),
ListView.builder(
shrinkWrap: true,
itemCount: contacts.length,
itemBuilder: (context, index) {
final contact = contacts[index];
return ListTile(
title: Text(contact.name),
subtitle: Text(contact.email),
trailing: contact.favourite ? const Icon(Icons.star) : null,
);
},
),
],
);

}
}

AdapterExample

Este contiene un widget ContactsSection que renderiza los datos.

import 'card_contacts.dart';

class AdapterExample extends StatelessWidget {
const AdapterExample({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return const ScrollConfiguration(
behavior: ScrollBehavior(),
child: SingleChildScrollView(
child: Column(
children: [
ContactsSection(
adapter: JsonContactsAdapter(),
headerText: 'Contactos desde API JSON'
),
ContactsSection(
adapter: XmlContactsAdapter(),
headerText: 'Contactos desde API XML'
),
],
),
)
);
}
}

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

--

--