Patrones de diseño con Flutter: 7— Composite

Paolo Pinto
4 min readApr 20, 2024

--

Los ejércitos de la mayoría de países se estructuran como jerarquías y esto en el código se presenta la mayoría del tiempo. El problema es cuando nuestra información se representa en forma de árbol. ¿ Como podemos estructurar nuestra aplicación para que sea limpio y sobre todo escalable?

Imagina! que estas a cargo de un peloton, siguiendo la analogía de los soldados. Las órdenes se dan en la parte superior de la jerarquía y se pasan hacia abajo por cada nivel hasta que todos los soldados saben lo que hay que hacer.

estructura en un ejercito

¿ Que podemos hacer con el patrón composite?

Para entender este patrón partimos del concepto de trabajar con Productos y Cajas. Composite sugiere que trabajes con estos Productos y Cajas a través de una interfaz común que declara un método para calcular el precio total.

Es un proceso recursivo sobre todos los componentes de un árbol de objetos. Esto pasa cuando pasamos por cada Caja y en cada producto y, en caso de que amerite, nos de el precio.

fuente: refactoring.guru

Si recordamos nuestras clases de estructuras de datos, sabremos que un arbol contiene ramas y hojas(leaf) ademas de una cabeza(head). Esto para nuestro diagrama de clases nos ayuda bastante.

diagrama de clases patron composite

Este es un ejemplo de una app de folder de directorios, en el que se soportan los archivos de tipo audio, imagen, texto, y video.

Helper: Nos ayudara a convertir de bytes a String.

class FileSizeConverter {
const FileSizeConverter._();

static String bytesToString(int bytes) {
final sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
var len = bytes.toDouble();
var order = 0;

while (len >= 1024 && order++ < sizes.length - 1) {
len /= 1024;
}

return '${len.toStringAsFixed(2)} ${sizes[order]}';
}
}

Component(Interface): IFile

abstract interface class IFile {
int getSize();
Widget render(BuildContext context);
}

Hoja(Leaf): File

base class File extends StatelessWidget implements IFile {
final String title;
final int size;
final IconData icon;

const File({
required this.title,
required this.size,
required this.icon,
});

@override
int getSize() => size;

@override
Widget render(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: LayoutConstants.paddingS),
child: ListTile(
title: Text(
title,
style: Theme.of(context).textTheme.bodyLarge,
),
leading: Icon(icon),
trailing: Text(
FileSizeConverter.bytesToString(size),
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Colors.black54),
),
dense: true,
),
);
}

@override
Widget build(BuildContext context) => render(context);
}

Hojas B: AudioFile, ImageFile, TextFile, VideoFile

final class AudioFile extends File {
const AudioFile({
required super.title,
required super.size,
}) : super(icon: Icons.music_note);
}

final class ImageFile extends File {
const ImageFile({
required super.title,
required super.size,
}) : super(icon: Icons.image);
}

final class TextFile extends File {
const TextFile({
required super.title,
required super.size,
}) : super(icon: Icons.description);
}

final class VideoFile extends File {
const VideoFile({
required super.title,
required super.size,
}) : super(icon: Icons.movie);
}

Composite: Directory

class Directory extends StatelessWidget implements IFile {
final String title;
final bool isInitiallyExpanded;
final List<IFile> files = [];

Directory(this.title, {this.isInitiallyExpanded = false});

void addFile(IFile file) => files.add(file);

@override
int getSize() {
var sum = 0;
for (final file in files) {
sum += file.getSize();
}
return sum;
}

@override
Widget render(BuildContext context) {
return Theme(
data: ThemeData(
expansionTileTheme: Theme.of(context)
.expansionTileTheme
.copyWith(iconColor: Colors.black, textColor: Colors.black),
),
child: Padding(
padding: const EdgeInsets.only(left: LayoutConstants.paddingS),
child: ExpansionTile(
leading: const Icon(Icons.folder),
title: Text('$title (${FileSizeConverter.bytesToString(getSize())})'),
initiallyExpanded: isInitiallyExpanded,
children: files.map((IFile file) => file.render(context)).toList(),
),
),
);
}

@override
Widget build(BuildContext context) => render(context);
}

Cliente: CompositeExample

class CompositeExample extends StatelessWidget {
const CompositeExample();

Widget _buildMediaDirectory() {
final musicDirectory = Directory('Music')
..addFile(const AudioFile(title: 'Darude - Sandstorm.mp3', size: 2612453))
..addFile(const AudioFile(title: 'Toto - Africa.mp3', size: 3219811))
);

final moviesDirectory = Directory('Movies')
..addFile(const VideoFile(title: 'The Matrix.avi', size: 951495532))
..addFile(const VideoFile(title: 'The Matrix.mp4', size: 1251495532),
);

final catPicturesDirectory = Directory('Cats')
..addFile(const ImageFile(title: 'Cat 1.jpg', size: 844497))
..addFile(const ImageFile(title: 'Cat 3.png', size: 1975363));

final picturesDirectory = Directory('Pictures')
..addFile(catPicturesDirectory)
..addFile(const ImageFile(title: 'Not a cat.png', size: 2971361));

final mediaDirectory = Directory('Media', isInitiallyExpanded: true)
..addFile(musicDirectory)
..addFile(musicDirectory)
..addFile(moviesDirectory)
..addFile(picturesDirectory)
..addFile(Directory('New Folder'))
..addFile(
const TextFile(title: 'Nothing suspicious there.txt', size: 430791),
)
..addFile(const TextFile(title: 'TeamTrees.txt', size: 104));

return mediaDirectory;
}
// HERE COMES THE WIDGET
@override
Widget build(BuildContext context) {
return ScrollConfiguration(
behavior: const ScrollBehavior(),
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: LayoutConstants.paddingL,
),
child: _buildMediaDirectory(),
),
);
}
}

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

--

--