.NET MAUI: Building Cross-Platform Mobile Apps

.NET MAUI: Building Cross-Platform Mobile Apps

Introduction

[Explain .NET MAUI as evolution of Xamarin.Forms; single project structure; native controls; hot reload productivity.]

Prerequisites

  • Visual Studio 2022 17.3+ or VS Code
  • .NET 7+ SDK
  • Android SDK, iOS SDK (Mac for iOS deployment)

Project Structure

MyApp/
├── Platforms/
│   ├── Android/
│   ├── iOS/
│   ├── MacCatalyst/
│   └── Windows/
├── Resources/
│   ├── Images/
│   ├── Fonts/
│   └── AppIcon/
├── ViewModels/
├── Views/
├── Services/
├── App.xaml
└── MauiProgram.cs

Step-by-Step Guide

Step 1: Create MAUI Project

dotnet new maui -n MyMauiApp
cd MyMauiApp
dotnet build

Step 2: Configure MauiProgram.cs

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });

        // Register services
        builder.Services.AddSingleton<IApiService, ApiService>();
        builder.Services.AddTransient<MainViewModel>();
        builder.Services.AddTransient<MainPage>();

        return builder.Build();
    }
}

Step 3: XAML Page with MVVM

MainPage.xaml:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:viewmodels="clr-namespace:MyMauiApp.ViewModels"
             x:Class="MyMauiApp.MainPage"
             x:DataType="viewmodels:MainViewModel">

    <Grid RowDefinitions="Auto,*,Auto" Padding="20">
        
        <!-- Search Bar -->
        <SearchBar Grid.Row="0"
                   Placeholder="Search items..."
                   Text="{Binding SearchText}"
                   SearchCommand="{Binding SearchCommand}" />

        <!-- Items List -->
        <CollectionView Grid.Row="1"
                        ItemsSource="{Binding Items}"
                        SelectionMode="Single"
                        SelectedItem="{Binding SelectedItem}">
            <CollectionView.ItemTemplate>
                <DataTemplate x:DataType="models:Item">
                    <Frame Margin="10" Padding="15" CornerRadius="10">
                        <Grid ColumnDefinitions="Auto,*,Auto">
                            <Image Grid.Column="0"
                                   Source="{Binding ImageUrl}"
                                   WidthRequest="60" HeightRequest="60" />
                            <StackLayout Grid.Column="1" Padding="10,0">
                                <Label Text="{Binding Name}" FontSize="18" FontAttributes="Bold" />
                                <Label Text="{Binding Description}" FontSize="14" TextColor="Gray" />
                            </StackLayout>
                            <Label Grid.Column="2"
                                   Text="{Binding Price, StringFormat='{0:C}'}"
                                   FontSize="16" VerticalOptions="Center" />
                        </Grid>
                    </Frame>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>

        <!-- Add Button -->
        <Button Grid.Row="2"
                Text="Add Item"
                Command="{Binding AddCommand}"
                HorizontalOptions="Fill" />
    </Grid>

</ContentPage>

MainViewModel.cs:

public partial class MainViewModel : ObservableObject
{
    private readonly IApiService _apiService;

    [ObservableProperty]
    private string searchText;

    [ObservableProperty]
    private ObservableCollection<Item> items = new();

    [ObservableProperty]
    private Item selectedItem;

    public MainViewModel(IApiService apiService)
    {
        _apiService = apiService;
        Items = new ObservableCollection<Item>();
    }

    [RelayCommand]
    private async Task LoadItems()
    {
        try
        {
            var data = await _apiService.GetItemsAsync();
            Items.Clear();
            foreach (var item in data)
            {
                Items.Add(item);
            }
        }
        catch (Exception ex)
        {
            await Shell.Current.DisplayAlert("Error", ex.Message, "OK");
        }
    }

    [RelayCommand]
    private async Task Search()
    {
        var filtered = await _apiService.SearchItemsAsync(SearchText);
        Items.Clear();
        foreach (var item in filtered)
        {
            Items.Add(item);
        }
    }

    [RelayCommand]
    private async Task Add()
    {
        await Shell.Current.GoToAsync("additem");
    }
}

Step 4: Platform-Specific Code

Access Device Features:

// Check network connectivity
var isConnected = Connectivity.Current.NetworkAccess == NetworkAccess.Internet;

// Get location
var location = await Geolocation.Default.GetLocationAsync();

// Take photo
var photo = await MediaPicker.Default.CapturePhotoAsync();

// Show notification
var notification = new NotificationRequest
{
    Title = "New Message",
    Description = "You have a new notification",
    CategoryType = NotificationCategoryType.Status
};
await LocalNotificationCenter.Current.Show(notification);

Platform-Specific Implementations:

// Platforms/Android/MainActivity.cs
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true)]
public class MainActivity : MauiAppCompatActivity
{
    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        Platform.Init(this, savedInstanceState);
    }
}

// Platforms/iOS/AppDelegate.cs
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

Step 5: Shell Navigation

AppShell.xaml:

<?xml version="1.0" encoding="UTF-8" ?>
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:views="clr-namespace:MyMauiApp.Views"
       x:Class="MyMauiApp.AppShell">

    <TabBar>
        <ShellContent Title="Home"
                      Icon="home.png"
                      Route="home"
                      ContentTemplate="{DataTemplate views:MainPage}" />
        
        <ShellContent Title="Profile"
                      Icon="profile.png"
                      Route="profile"
                      ContentTemplate="{DataTemplate views:ProfilePage}" />
    </TabBar>

    <Shell.FlyoutHeader>
        <Grid BackgroundColor="#2196F3" HeightRequest="200">
            <Label Text="My App" FontSize="24" TextColor="White" VerticalOptions="Center" HorizontalOptions="Center" />
        </Grid>
    </Shell.FlyoutHeader>

</Shell>

Programmatic Navigation:

// Navigate to route
await Shell.Current.GoToAsync("//home/details", new Dictionary<string, object>
{
    ["Item"] = selectedItem
});

// Navigate back
await Shell.Current.GoToAsync("..");

Step 6: Data Binding & Converters

Value Converter:

public class BoolToColorConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (bool)value ? Colors.Green : Colors.Red;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

XAML Usage:

<ContentPage.Resources>
    <converters:BoolToColorConverter x:Key="BoolToColor" />
</ContentPage.Resources>

<Label Text="Status"
       TextColor="{Binding IsActive, Converter={StaticResource BoolToColor}}" />

Step 7: Blazor Hybrid Integration

Add BlazorWebView:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Maui;assembly=Microsoft.AspNetCore.Components.WebView.Maui">

    <BlazorWebView HostPage="wwwroot/index.html">
        <BlazorWebView.RootComponents>
            <RootComponent Selector="#app" ComponentType="{x:Type local:Main}" />
        </BlazorWebView.RootComponents>
    </BlazorWebView>

</ContentPage>

Step 8: Deployment

Android:

dotnet publish -f net7.0-android -c Release

iOS (Mac):

dotnet publish -f net7.0-ios -c Release

Configure App Icons & Splash Screen:

<!-- MauiProgram.cs -->
builder.UseMauiApp<App>()
    .ConfigureFonts(fonts => { ... })
    .ConfigureEssentials(essentials =>
    {
        essentials.UseMapServiceToken("YOUR_MAPS_TOKEN");
    });

Advanced Features

Custom Handlers

Extend Platform Control:

Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping("CustomEntry", (handler, view) =>
{
#if ANDROID
    handler.PlatformView.SetBackgroundColor(Android.Graphics.Color.Transparent);
#elif IOS
    handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;
#endif
});

Animations

await myImage.ScaleTo(2, 500);
await myImage.RotateTo(360, 1000);
await myImage.TranslateTo(100, 100, 500);

Community Toolkit

dotnet add package CommunityToolkit.Maui
builder.UseMauiApp<App>()
    .UseMauiCommunityToolkit();

Performance Optimization

  • Use compiled bindings: x:DataType in XAML
  • Enable startup tracing: Trace.Listeners.Add(...)
  • Reduce layout nesting
  • Use CollectionView over ListView
  • Cache images with UriImageSource

Troubleshooting

Issue: App crashes on iOS startup
Solution: Verify provisioning profile; check Info.plist permissions

Issue: Images not displaying
Solution: Ensure Build Action is MauiImage; check path casing

Issue: Hot reload not working
Solution: Enable in Visual Studio options; restart debugger

Best Practices

  • Follow MVVM pattern with CommunityToolkit.Mvvm
  • Use dependency injection for services
  • Implement proper error handling
  • Test on real devices (not just emulators)
  • Use Shell for navigation consistency

Key Takeaways

  • Single codebase targets iOS, Android, Windows, macOS.
  • XAML + MVVM provides clean separation of concerns.
  • Platform-specific code handled via handlers or conditional compilation.
  • Blazor Hybrid enables web component reuse.

Next Steps

  • Add offline data sync with SQLite
  • Implement push notifications
  • Integrate Azure App Center for analytics
  • Publish to App Store and Google Play

Additional Resources


Ready to ship your first cross-platform app?