Awesome
Here are some GIFs where you can see the design and behavior of the app. And I also inform you of the most outstanding details of the application and possible future improvements.
Preview
<img src="./images/memoryGameLightMode.gif" alt="memory game in light mode" width="250"/> <img src="./images/memoryGameWinLightMode.gif" alt="win animation in light mode" width="250"/> <img src="./images/memoryGameLoseLightMode.gif" alt="lose animation in light mode" width="250"/> <img src="./images/memoryGameDarkMode.gif" alt="memory game in dark mode" width="250"/> <img src="./images/memoryGameWinDarkMode.gif" alt="win animation in dark mode" width="250"/> <img src="./images/memoryGameLoseDarkMode.gif" alt="lose animation in dark mode" width="250"/>
Platforms
- iOS
- Android
It works only in portrait mode
Running in my computer
It is necessary a version of visual studio intallation, compatible with dotnet maui. And the tools to deploy on the desired platform (Xcode, Android SDK...) Note that at the time of writing this document some nuget packages were in pre-release, so it may be necessary to activate that check in the nuget package manager.
Nugets
- ReactiveUI.Maui: As in other applications that I have developed, I have used this nuget to create the base classes from which both my graphic elements and their ViewModel hang. I feel very comfortable working with him. I think it simplifies development a lot without penalizing performance and giving me the control to free up memory used by certain resources, like bindings, event subscriptions, observables...
- ReactiveUI.Fody: I use it to save some code when working with RxUI.
- SkiaSharp.Views.Maui.Controls: I have used it to draw the control that shows the progress of the time consumed once the game has started.
- SkiaSharp.Extended.UI.Maui: skia sharp extension that allows to play lottie animations.
Base classes
I like to have some base classes, from which the rest inherit and offers some common functionality.
- BaseContentPage
- Manage activation / deactivation with RxUI, to dispose resources when they are no longer needed.
- Notifies the ViewModel when the page appears and disappears. Useful to respond from the ViewModel to these actions.
- Controls the pressing of the back button on Android.
- Execute appearance and disappearance page animations, to achieve very successful effects in the navigation from one page to another.
- BaseContentView
- All it has are methods for adding disposable elements to the collection of the ContentPage it's in.
- BasePopup
- It's a BaseContentPage with a popup appearance. In Xamarin I used RxPopup, but it is not available in dotnet MAUI (at least at the time of writing this post) and with the _CommunityTool_Kit popup I had some problems on Android (I repeat, all this at the time of writing this post)
- BaseViewModel
- Handle activation/deactivation with RxUI, to dispose resources when they are no longer needed.
- Implement the back navigation command.
Services
In the services I add all that transversal functionality to the application and that would generate coupling if it were included in the base classes.
- DialogService
- I use it to show info, confirmation dialogs and action sheets.
- LogService
- Service that allows me to leave traces. In this application I only write diagnostic information, but it could be integrated with AppCenter, Crashlytics, Application insights...
- NavigationService
- Although I use a shell, I like to encapsulate the navigation functionalities in a service to be able to perform specific actions of the navigation.
Controls
I like to create custom controls to simplify my pages and in order to reuse them. There are times when styles and templates are not enough, that is why I have created the following controls.
<img src="./images/CardButton.png" alt="Card button" width="175" /> <img src="./images/CardView.png" alt="Card view" width="250" /> <img src="./images/CircleProgress.png" alt="Circle progress" width="245" /> <img src="./images/CustomButton.png" alt="Card button" width="300" /> <img src="./images/RoundedButton.png" alt="Rounded button" width="120" />Note that to make these controls more attractive, I have used new features of dotnet MAUI, which simplify the design, such as borders and shadows. Likewise, in the case of the CircleProgressBar I have used skia to draw the control.
Features
Most of the application's functionality is housed here, in ContentPages and ViewModels. I will not go into detail to explain everything I have done here, but I will expose some interesting points or that may cause some confusion.
All pages have a background gradient, which is achieved very easily using LinearGradientBrush.
In all ContentPage methods have been implemented to perform animations when a page is opened and when it disappears.
- ThemeSelector: Initial page in which the theme is selected. CardButtons are used to display the distinct buttons.
- LevelSelector: Similar to the previous one, but instead of using CardButtons, CustomButtons have been used.
- Game: Depending on the level selected, board is created with different sizes and the user is given more or less time to discover all the images. These images vary depending on the chosen theme. NOTE: In the FillGridBoard method of the page, you can see that the cards are discovered when different events are fired on iOS and Android. It has been done this way because what worked on one platform did not work on the other.
- GameOver: Popup that shows a lottie animation and a text, depending on whether the user has revealed all the cards or the time has run out.
Navigation
Navigation is done via shell. Parameter are send also with shell functionalities. In addition, it is the navigation service that, before leaving a page, launches the animation of its disappearance, taking into account that all pages implement the IAnimatePage interface.
There is a point that should be highlighted in the navigation. From GamePage the popup is opened as a modal showing the game is over. Closing that popup, instead of navigating back closing the modal and displaying again the previous GamePage, it navigates to a new instance of GamePage and clears the old one from the navigation stack. Why have I done this? Because when it navigates to the modal, OnDisappearing is fired, performing the page and ViewModel deactivation, but when it navigates back from the modal, OnAppearing is not fired and therefore the page activation is not performed, so bindings, observables, subscriptions to events... don't work
More details
It is interesting to talk about certain details that I have used.
- MauiAppBuilder: for building pages, ViewModels, and dependency resolution and injection.
- FontIcons: to display the images of the back and close buttons.
- Text resources: In English and Spanish, using the system language.
- Images in common project: So it is not necessary to add images on different platforms. Although the ideal would have been use svg as image format.
- MAUI icon and splash: defined in the csproj, without the need to do anything in the platform code.
- Light and Dark Mode: All colors are defined with both modes in mind.
Future improvements
There are certain improvements that I have left for later. Here I talk about some of them, although I'm sure there are more. There is always something to improve.
- Adapt the views to desktop and horizontal position.
- Add tests.
- Save scores to make a classification with the best times by levels.
- Images in svg.
- Traces in AppCenter (or similar).