[This is my experience of building a production grade mobile app using .NET MAUI from scratch. I will highlight what worked, what did not work and ways to overcome some of the challenges.]
I have been a Xamarin Forms developer for the past 5 years and have built a handful of apps (and published in the app stores) over the years. When .NET MAUI was announced at Build 2020, it got me very excited and I was anxiously looking forward to it. Unfortunately, I had to wait over a year for MAUI preview bits to be dropped for developers to test and play with. I started testing the waters the first time around with Preview 4 in May 2021 but was immediately put off due to a plethora of limitations in the core framework itself. Don't get me wrong; it was a preview build after all. I again played with it a bit more next month with Preview 5 and was able to put forward a decently functional MAUI app (https://github.com/naweed/Weekly_MAUI_App). But the tooling support was extremely limited at that time and I was unable to run it on an Android emulator or iPhone simulator. So, I gave up on it until more stable builds are announced. With RC2 announced in April of this year, and promise (or indication of, if I have to be legally correct) of production release around the corner (in a month or two's time), I knew it was time for me to jump back in.
With Mac being my main machine and lack of tooling support on Mac (I know I could have partially worked on VS for Mac RC1), I decided to build a new desktop with Windows 11 and installed Visual Studio 17.2 Preview with MAUI RC2 bits and started playing with it. To my surprise, it was as smooth an experience as it can be and everything I tried in MAUI worked well, both from the framework as well as the tooling perspective. Now, experimenting with features of a framework is pretty easy. Everything usually works well in isolation. The problem comes when you try to build a full-fledged production app. And btw, this is true for anything else as well, including stable release versions. So, I decided to build a FULL app which I can potentially publish to the app stores. Rather than building a hypothetical app, I decided to take my recently released Vocably app and PORT it (watch out for this later) to MAUI RC2. I have built the current app using Xamarin Forms 5. A huge challenge for me, but ACCEPTED nonetheless.
So, I started the journey with VS 17.2 Preview and MAUI RC2 and completed the re-write successfully. By the time I completed (as of today), I am using VS 17.3 Preview with MAUI RC3. Now, I mentioned "re-write" here and not "porting". When I started the migration, I very soon realized that migration will not give me the best experience of experimenting with all the features of the platform, and I will eventually be leftover with legacy pieces here and there. When things get tough or you are not able to find a solution using the new framework, you leave the old code as is and use it in legacy mode. This is not what I wanted. And 2-3 days into the migration, I scrapped the idea and started afresh from a blank .NET MAUI project. I set myself a goal to be 100% legacy-free and happy to report that on completion of this app, I have attained this objective fully.
Of course, there were some challenges which I will touch base on later. And btw, building real apps and identifying shortcomings and bugs is the best way to prompt the amazing MAUI team to stabilize the platform going forward (production and beyond). So, this is my experience of building a fresh app using .NET MAUI and I hope that it will be useful for everyone looking to build cross-platform apps with .NET MAUI. I will break it down into "The Good", "The Bad" and "The Ugly".
THE GOOD - for the most part
Overall, my experience developing with .NET MAUI was extremely smooth. The framework is stable enough. There are a few quirks which I am sure will be resolved by the time MAUI goes GA (by May 23rd, ahem ahem...). And VS 17.3 Preview was extremely stable as well.
The best thing I noticed is that lot of things are now baked into the platform and make it a breeze for us developers. I was in fact able to retire most of the frameworks or libraries that I had written for Xamarin Forms apps earlier or third-party ones, in favor of built-in provisions.
- Small things, but very important ones. Some of the platform-specific settings such as app identifier, version, splash screen, icon, display name, etc. are now part of the single project itself. Makes it very easy to control it across platforms.
- Working with images (and image sizes) in MAUI from a common place is a welcome addition. I had tried Jonathan's Resizetizer.NT earlier in XF with limited success. It was a hit and miss for me; sometimes it worked and sometimes it didn't. I assume that it usually broke between XF version upgrades. The MauiImage builds on Resizetizer.NT and provides a stable and easy way to work with images across platforms.
- I was able to get rid of Newtonsoft JSON dependency in favor of System.Text.Json. Not a big thing but one less dependency in the project.
- CommunityToolkit has come a long way and the Popup control has matured and stabilized a lot. When I tried it a couple of months ago for the same XF version of the app, I was disappointed and resorted to using Rg.Plugins.Popup instead. This time around, I was able to use popups from MAUI CommunityToolkit with no issues and the same results.
- I was able to move all the platform-specific application initialization code to a single place (within MauiProgram class) using ConfigureLifecycleEvents extension.
- I had earlier written my own DI and MVVM framework for use in my XF apps. I previously used to use Prism framework but discarded it 2-3 years ago in favor of my own custom-written framework which provided me greater control over how I wanted to use it and not wait for bug fixes or feature enhancements from the Prism team. And also, to counter the upgrade pitfalls. I remember between XF and Prism releases, I had to sometimes make major changes to my apps (coz of breaking changes in either platforms - mostly Prism), and test the apps very thoroughly.
- The built-in DI framework (extended from ASP.NET Core) works pretty well. For the purpose of this app, and also the way I wrote my own framework, I cannot see any reason why this framework will not work for me (and others as well). In fact, it served all my needs for this app.
- I had earlier written my own MVVM framework (BindableBase, AsyncDelegateCommand, AsyncDelegateCommand<T> etc.) to assist with data binding and navigation. Thanks to CommunityToolkit.Mvvm package, I was able to get rid of it and have much cleaner code in the process. As an example, see how I was able to simplify my view model.
XAMARIN FORMS VERSION:
public class StatsPageViewModel : AppViewModelBase
{
public AsyncDelegateCommand ShareCommand { get; set; }
private List<Level> _gamesLevels;
public List<Level> GameLevels
{
get => _gamesLevels;
set => SetProperty(ref _gamesLevels, value);
}
public StatsPageViewModel(IApiService appApiService, ISettingsService appSettingsService, IDatabaseService appDBService)
{
ShareCommand = new DelegateCommand(ShareStats);
}
private async Task ShareStats()
{
.........
}
}
MAUI VERSION:
[INotifyPropertyChanged]
public partial class StatsPageViewModel : AppViewModelBase
{
[ObservableProperty]
private List<Level> gameLevels;
public StatsPageViewModel(IApiService appApiService, ISettingsService appSettingsService, IDatabaseService appDBService)
{
}
[ICommand]
private async Task ShareStats()
{
...........
}
}
THE BAD - or call it inconvenience
I did stumble upon some issues with the core framework itself. On further research, I found that almost all of them have already been reported as open issues in the MAUI repository. Anyways, I was able to find workarounds for them and move on. Hopefully, these issues will be resolved in the GA version.
- The first issue I encountered was a strange one. I have a grid on top of another grid. The bottom grid has some image buttons with TapGestureRecognizer on them. The top grid overlays the bottom grid, but still I was able to touch on the image buttons underneath and trigger their TapGestureRecognizer actions. I tried with InputTransparent as well as ZIndex properties on the grids but with no success. Eventually, I solved it by writing a bit of code to play with the IsVisble property of the bottom grid to only make it visible when needed. This helped my cause, and I was able to move on. If this was not resolved, I could have given up much earlier. Have a look at this for more details here.
- The Opacity on Frames (either declaratively in XAML or in Code-behind) doesn't work. No matter what opacity you set, it always resorts back to "1". I was able to circumvent this issue by using FadeTo(0,...) method in the code-behind to make it transparent to start with.
- If you set the margin on the sub-element of the ScrollView, or set padding on the ScrollView itself, it stops scrolling. In my case, I moved the padding to the Grid containing the ScrollView and it resolved my issue. Have a look at this tweet. Btw, I was able to resolve it in most of the places. In one of the forms, I had a ScrollView in the 2nd row of the Grid. I was not able to resolve it there. But I did manage to solve it by changing my layout altogether and getting rid of the ScrollView.
- Also noticed that CollectionView behaves very erratically. The Padding or Margin in the container Frame or Grid inside the DataTemplate doesn't work. I was able to solve it by moving the Padding or Margin from container object to child elements by specifying their margins.
- The layout in the CollectionView also gets messed up when scrolling up and down. It starts off well, but as you scroll down and then back up, some of the items randomly will have their layout all messed up. Not able to solve it for now, but at least not something I am cribbing about. This page gets used very infrequently in my app.
- The TouchEffect (from Xamarin CommunityToolkit) is missing in MAUI Community Toolkit, so I had to resort to using TapGestureRecognizer for my image buttons. I hope the team adds it to MAUI version soon.
THE UGLY - not really
I had used a total of three custom controls in my XF Vocably app. One was a third-party control and the other two were my own. I could not get any of them to work in MAUI (even after trying to re-write them), so I resorted to alternatives.
One of my own custom controls was written using SkiaSharp, with no custom render business. This control draws a gradient progress bar (based on this amazing blog post by Cesar). I added the MAUI version of SkiaSharp to my project and brought in my control (and changed the namespaces accordingly). But it refused to run. I continued getting an error related to Handlers. Btw, the issue with the other two controls is also Handler related. I assume that the version of SkiaSharp I am using (v.2.88.0-preview.266) is not compatible with MAUI RC3. But no worries; no love lost. I was able to replace it (to some extent) with SfLinearGauge from Syncfusion MAUI controls. P.S. this is the error I was getting with SkiaSharp (Handler not found for view SkiaSharp.Views.Maui.Controls.SKCanvasView).
Second, I had written another custom control and used it in many of my XF apps. This control draws an animated radial progress bar and relies on custom renderers to extend Android.Widget.ProgressBar on Android, and draw the whole control from scratch in iOS. I tried to re-write this using Handlers in MAUI but could not succeed. I assume that something has changed with how handlers are written in RC3, and the documentation did not help me much either. So, after spending a good 1 day on it, I gave up and instead resorted back to our good friend Syncfusion. I was able to achieve 100% similar outcome with SfRadialGauge.
And lastly, I have used this amazing third-party Segmented control from Jasper. Unfortunately, he doesn't have a MAUI version yet, but fortunately he did have a branch with MAUI equivalent code. I tried to take this code and compile it myself but couldn't succeed. Again, something to do with the way handlers need to be written (or may have changed between versions). Anyways, I did it manually using two buttons and custom code to switch for the time being.
CONCLUSION
So, here you have it. I hope this was useful for some of the Devs coming from Xamarin background. Overall, it was a smooth experience and I can't complain much. It was a release candidate after all, but much stabler than anything else that I have seen before. I am also anxiously waiting for the GA release and the stability that will come with it. Next up, I will be releasing the app on the app stores; hopefully before MAUI GA itself. Stay tuned for the updates.
Just for reference (and all transparency), these are all the Nuget packages I am using in this MAUI app:
<PackageReference Include="CommunityToolkit.Maui" Version="1.0.0-rc3" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.0.0-preview3" />
<PackageReference Include="MonkeyCache.FileStore" Version="2.0.0-beta" />
<PackageReference Include="sqlite-net-pcl" Version="1.8.116" />
<PackageReference Include="Syncfusion.Maui.Gauges" Version="20.1.55-preview" />