El otro día, en uno de los muchos grupos en los que participo, específicamente en uno sobre Flutter, vi a una persona realizar la siguiente pregunta:
Mi respuesta fue exactamente la que se muestra en la imagen. Aunque es una solución válida, no necesariamente es la mejor. Sin embargo, tiene la ventaja de ser una solución que se puede implementar de manera fácil y rápida.
Así que decidí crear una versión mejorada (o al menos la mejor versión que pude) para diseñar unas sillas de cine. Para ello, me inspiré en la aplicación de Cinépolis.
Diseño Cinépolis
Como pueden ver básicamente es una matriz de asientos, aunque no es cuadrada porque no hay la misma cantidad de asientos en algunas filas.
Para desarrollar la lógica, me guié parcialmente, aunque no al pie de la letra, por la metodología Atomic Design a fin de separar los distintos componentes, que esencialmente son tres:
Asiento (Botón)
Este es el único widget que tiene estado ya que reacciona a los toques y/o puede construirse en un estado especifico, así como su tamaño color y dos propiedades para identificarse así mismo, que es una letra (String) y numero de botón, A,10 por ejemplo.
Tras explorar un poco la aplicación y comprar algunos boletos, observé que básicamente un asiento puede encontrarse en los siguientes estados.
- Libre
- Ocupado (no disponible)
- Asiento especial (tiene un icono de silla de ruedas)
- Libre/Ocupado (este último cuando se selecciona y/o deselecciona)
La clase para este propósito es BtnSeat
y representa los lugares o asientos que tiene una sala.
Tiene un callback para capturar la respuesta al toque del botón y su estado. La utilizo para propagar la información retrospectivamente a través de todo el ciclo de vida del botón. Así, puedo identificar qué botón se está tocando, basándome en sus dos valores clave: letra (String) y número.
Fila de Asientos
Este widget construye una fila de asientos. Lo que hace es implementar la clase anterior y crear una fila compuesta por varios botones. La cantidad de botones depende del número que se le pase al constructor, así como de la letra. De este modo, cada botón ya tendrá un identificador que irá de 1 a N, acompañado de una letra, como, por ejemplo, ‘A’.
La clase se denomina RowSeats
y cuenta con dos propiedades clave: specialSeats
y blockSeats
. Ambas propiedades son listas de números enteros. En specialSeats
se especifican cuáles botones del rango 1-N serán especiales, mientras que en blockSeats
se señalan cuáles estarán bloqueados. Los botones especiales presentan un icono de silla de ruedas, y los bloqueados, que no tienen reacción al ser seleccionados (lugares no disponibles en la sala), muestran un icono de una persona.
Fila de Letras y Lugares
Por último, existe una clase denominada TheaterRows
. Esta clase representa la implementación de cada una de las filas mencionadas anteriormente, junto con una letra que sirve como coordenada de la fila.
En esta parte, se construye literalmente la vista, realizando cálculos adicionales para generar un diseño responsivo. La vista consta de dos columnas: una para mostrar las letras y otra para la fila de asientos. Estos últimos se crean enviando el número de asientos y su correspondiente letra. Esta información se propaga hasta llegar al botón individual, que contendrá un par clave-valor: letra y número.
Esta clase también recibe información sobre qué asientos están bloqueados y cuáles son especiales, y esta información se propaga durante la implementación.
Como pueden observar en el dibujo, la idea es que no todas las filas tengan la misma cantidad de lugares.
Diseno responsivo
Dado que Flutter puede ejecutarse en múltiples sistemas y adaptarse a diferentes tamaños de pantalla, decidí crear un diseño ‘Responsive’. El principal era conseguir un ajuste predominante en la anchura más que en la altura. Dado que Flutter permite una gran manipulación a nivel de píxeles, con unos simples cálculos es posible realizar los ajustes necesarios para adaptarse a distintos tamaños de pantalla.
Como se puede ver en la imagen los tamaños y márgenes los base en el tamaño en horizontal del contenido S, esto puede ser el tamaño de la pantalla o el tamaño del contenedor de este widget.
- S: Tamano en ancho del contenedor.
- MX: número mayor de lugares, la fila con más lugares.
- MRB: El espacio entre cada lugar (margen).
- MRF: Espacio entre cada fila (margen).
- BT: Lado del cuadrado del botón de letras.
- BN: Lado del cuadrado del botón de asientos.
- BR: Borde del botón
- T: Tamano del texto.
- N: Tamano del texto numerico.
Todos estos cálculos están expresados en porcentaje de ahi que mis multiplicadores sean 0.1, 0.7 etc.
Código Flutter
Para implementar TheaterRows
que es el widget que va a generar toda la vista se necesita enviar los siguientes parámetros:
Propiedad | Descripción |
---|---|
rowsByLetter | Map<String,int> donde se indica la letra y el número de lugares por letra. |
specialSeats | Map<String,List<int>> donde se indica que asientos de que fila serán especiales (icono silla de ruedas) |
blockSeats | Map<String,List<int>> donde se indica que asientos de que fila estarán bloqueados u ocupados (no reaccionarán) |
width | int , tamaño en ancho del contenedor del widget |
onTap | Function(String, int) , es un método que se propaga desde el BtnRow y obtiene la letra y numero de botón, fila y asiento básicamente. |
maxZoom | double , Este valor se utiliza para especificar el máximo nivel de zoom que podrás aplicar a la vista. Muchas veces, debido a la gran cantidad de lugares, los cuadros resultan muy pequeños y se necesita aplicar zoom. Además, observé que la aplicación de Cinépolis incorpora esta misma función, y quise replicarla |
color | Color , es el color del boton, por defecto es azul. |
iconColor | Color , es el color del icono sin seleccionar o bloqueado. |
toggleIcon | IconData , es el icono del asiento bloqueado o seleccionado. |
toggleColor | Color , es el color para el icono bloqueado o seleccionado. |
Ejemplo Flutter
TheaterRows(
rowsByLetter: Map.from({
'A': 15,
'B': 10,
'C': 9,
}),
specialSeats: Map.from({
'A': [3],
'B': [1, 2, 5],
}),
blockSeats: Map.from({
'A': [4],
'B': [3, 4],
}),
width: MediaQuery.of(context).size.width,
onTap: btnOntap,
maxZoom: 3
)
El codigo anterior generara la siguiente vista:
Solo se generan tres filas de asientos, pero se podrían crear virtualmente ilimitadas; el código no impone un límite sobre cuántas filas y asientos por fila puede haber.
El código es una biblioteca para Flutter y no se integra dentro de un proyecto específico. Más adelante, una vez que elabore un README adecuado y organice mejor el código, tengo planes de subirlo a pub.dev.
Para utilizar el código, basta con clonarlo directamente desde el repositorio y emplearlo como dependencia. Simplemente se debe agregar la ruta del proyecto local en la configuración correspondiente, y estará listo para usarse.
dependencies
g_cinema_layout:
path: /rutadelproyecto/g_cinema_layout/
Consideraciones
El widget que crea toda la interfaz no tiene estado; por ende, no registra ni considera qué filas y qué asientos están seleccionados, los botones solo reaccionan y cambian pero no hay manera de obtener en un momento dado esa informacion. Dicha información deberá almacenarse en otro lugar y se obtiene mediante la propagación de cada botón individual a través del método onTap
.
Actualmente, no existe ninguna restricción respecto a la cantidad de lugares que se pueden seleccionar, y no hay ningún parámetro para imponer un límite. Por lo tanto, de momento, es posible seleccionar todos los asientos disponibles. En el futuro, planeo incorporar una funcionalidad para restringir cuantos lugares se pueden seleccionar.
La propiedad rowsByLetter
es, en esencia, un Map<String, int>
. Esto significa que, técnicamente, no se limita a usar letras como identificadores de las filas; también se puede emplear cualquier texto o número, es decir, cualquier cadena de caracteres en realidad. No pienso meter una validacion aqui, tendria que extender el Map o hacer otro tipo de dato que le agregaria complejidad.
Actualmente, el código no cuenta con pruebas automatizadas. Planeo desarrollar e implementar tests adecuados antes de hacerlo disponible en pub.dev.
Un aspecto distintivo entre este diseño y el de Cinépolis radica en la disposición de los asientos. En esta implementación, todos los lugares se generan desde el centro hacia los extremos, mientras que, según mi observación, Cinépolis parece organizarlos de derecha a izquierda.
Happy Coding!!