.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:DataTypein XAML - Enable startup tracing:
Trace.Listeners.Add(...) - Reduce layout nesting
- Use
CollectionViewoverListView - 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?