Memory game con dotNet MAUI

Pasan las semanas y sigo jugando con dotNet MAUI. Tras el conecta 4, le he cogido el gusto a implementar juegos sencillos, porque ahora he creado un memory game.
A continuación os dejo algunos GIF en los que podéis ver el diseño y comportamiento de la app. Y también os informo de los detalles más destacados de la aplicación y posibles mejoras futuras.
Aplicación en acción






Nugets
- ReactiveUI.Maui: Al igual que en otras aplicaciones que he desarrollador, he utilizado este nuget para crear las clases bases de las que cuelgan tanto mis elementos gráficos como sus ViewModel. Me siento muy cómodo trabajando con él. Creo que simplifica mucho el desarrollo sin penalizar el rendimiento y dándome el control para liberar la memoria utilizada por ciertos recursos, como bindings, suscripción a eventos, observables…
- ReactiveUI.Fody: Lo uso para ahorrar algo de código al trabajar con RxUI.
- SkiaSharp.Views.Maui.Controls: Lo he utilizado para dibujar el control que muestra el progreso del tiempo consumido una vez que se ha iniciado el juego.
- SkiaSharp.Extended.UI.Maui: Extensión de skia sharp que me permite reproducir animaciones lottie.
Clases base
En todos los proyectos que realizo, me gusta tener unas clases bases, de las que heredan el resto y me ofrecen cierta funcionalidad común.
- BaseContentPage
- Gestiona la activación / desactivación con RxUI, para liberar los recursos cuando ya no son necesarios.
- Notifica al ViewModel cuando la pagina aparece y desaparece. Util para responder desde el ViewModel a estas acciones.
- Controla la pulsación del back button en Android.
- Ejecuta las animaciones de aparición y desaparición de las páginas, para conseguir efectos muy resultones en la navegación de una página a otra.
- BaseContentView
- Lo único que tiene son métodos para añadir elementos disposables a la colección de la ContentPage en la que se encuentra.
- BasePopup
- Es un BaseContentPage con una apariencia de popup. En Xamarin utilizaba RxPopup, pero no está disponible en dotnet MAUI (al menos en el momento de escribir este post) y con el popup de _CommunityTool_Kit tenía algunos problemas en Android (repito, todo esto en el momento de escribir este post)
- BaseViewModel
- Maneja la activación / desactivación con RxUI, para liberar los recursos cuando ya no son necesarios.
- Implementa el command de navegación atrás.
Servicios
En los servicios añado toda esa funcionalidad transversal a la aplicación y que generaría acoplamiento si se incluyese dentro las clases base.
- DialogService
- Lo utilizo para mostrar diálogos informativos, de confirmación y action sheets.
- LogService
- Servicio que me permite dejar trazas. En esta aplicación solo escribo información de diagnostico, pero podríamos integrar AppCenter, Crashlytics, Application insights…
- NavigationService
- Aunque utilizo shell, me gusta encapsular la navegación un servicio para evitar acoplamientos y poder realizar acciones especificas de la navegación.
Controles
Me gusta crear controles personalizados para simplificar mis páginas y poder reutilizarlos. Hay veces que con estilos y templates no es suficiente, es por ello, que he creado los siguientes controles.

CardButton

CardView

CircleProgessBar

CustomButton

RoundedButton
Nótese, que para dar más vistosidad a estos controles, he utilizado nuevas funcionalidad de dotnet MAUI, que facilitan las labores de diseño, como son los borders y shadows. Asimismo, en el caso del CircleProgressBar he utilizado skia para el dibujado del control.
Features
Aquí se aloja el grueso de la funcionalidad de nuestra aplicación, en ContentPages y ViewModels. No entraré en detalle a explicar todo lo que he hecho aquí, pero si expondré algunos puntos interesantes o que pueden provocar cierta confusión.
Todas las páginas tienen un degradado de fondo, que se consigue de forma muy sencilla utilizando LinearGradientBrush.
En todas las ContentPage se han implementado métodos para realizar animaciones cuando se abre una página y cuando esta desaparece.
- ThemeSelector: Página inicial en la que se selecciona el tema. Se utilizan CardButtons para mostrar los distintos botones.
- LevelSelector: Similar a la anterior, pero, en vez de optar por CardButtons, se han utilizado CustomButtons.
- Game: En función del nivel seleccionado, se crean tableros con distintos tamaños y se ofrece más o menos tiempo al usuario para descubrir todas las imágenes. Éstas imágenes varían dependiendo del tema escogido. NOTA: En el método FillGridBoard de la página, se puede ver que las tarjetas se descubren cuando se lanzan eventos distintos en iOS y Android. Se ha hecho así porque lo que funcionaba en una plataforma no lo hacía en la otra.
- GameOver: Popup que muestra una animación lottie y un texto, dependiendo de si el usuario ha descubierto todas las tarjetas o, por el contrario, se le ha terminado el tiempo.
Navegación
La navegación y el paso de parámetros se hace mediante shell. Además, es el servicio de navegación el que, antes de abandonar una página, lanza la animación de desaparición de está, teniendo en cuenta que todas las paginas implementan la interfaz IAnimatePage.
Hay un detalle que cabría destacar en la navegación. Desde GamePage se abre en modal el popup que indica que el juego se ha terminado. Al cerrar ese popup, en lugar de navegar atrás para cerrar la modal y volver a mostrar GamePage, se navega a una nueva instancia de GamePage y se borra de la pila de navegación la instancia anterior. ¿Por qué he hecho esto? Porque al navegar a la modal, se lanza OnDisappearing, realizando la desactivación de la página y el ViewModel, pero al navegar atrás desde la modal, no se lanza OnAppearing y, por consiguiente, no se realiza la activación de la página, por lo que no funcionan los bindings, observables, subscripciones a eventos…
Más detalles
Es interesante hablar de ciertos detalles que he utilizado.
- MauiAppBuilder: para la construcción de páginas, ViewModels y resolución e inyección de dependencias.
- FontIcons: para mostrar las imágenes de los botones atrás y cerrar.
- Recursos de textos: En inglés y castellano, seleccionando el idioma del sistema.
- Imágenes en proyecto común: Por lo que no es necesario añadir imágenes en las distintas plataformas. Aunque lo ideal hubiese sido que el formato de esas imágenes fuese svg.
- Icono y splash de MAUI: definidos en el csproj, sin necesidad de hacer nada en el código de plataforma.
- Modo claro y oscuro: Todos los colores se definen teniendo en cuenta los dos modos.
Mejoras pendientes
Hay ciertas mejoras que he dejado para más adelante. Aquí os detallo algunas que se me ocurren, aunque seguro que hay más. Siempre hay algo que mejorar.
- Adaptar la interfaz al uso en escritorio y en posición horizontal.
- Añadir tests.
- Guardar puntuaciones para confeccionar una clasificación con los mejores tiempos por niveles.
- Imágenes en svg.
- Trazas en AppCenter (o similar).
Show me the code!!
Como siempre, aquí os dejo un enlace al repositorio del proyecto de ejemplo que he creado, para que podáis descargarlo y jugar con el ejemplo que he creado.