Learnixo
Back to blog
Backend Systemsintermediate

.NET MAUI: Cross-Platform Mobile and Desktop Apps with C#

Build iOS, Android, Windows, and macOS apps with .NET MAUI. Covers MVVM, Shell navigation, data binding, platform-specific code, HTTP integration, local storage, and when to choose MAUI vs native.

LearnixoJune 4, 20265 min read
.NETC#MAUIMobileCross-PlatformiOSAndroid
Share:𝕏

What is .NET MAUI?

.NET Multi-platform App UI (MAUI) is the successor to Xamarin.Forms. Write C# and XAML once, deploy to iOS, Android, Windows, and macOS. The same .NET skills, the same NuGet packages, a shared codebase.

.NET MAUI App
    ├── Shared Code (C# + XAML)
    │     ├── Pages / Views
    │     ├── ViewModels
    │     ├── Services
    │     └── Models
    └── Platform-Specific
          ├── Platforms/iOS/
          ├── Platforms/Android/
          ├── Platforms/Windows/
          └── Platforms/MacCatalyst/

When to choose MAUI:

  • Shared business logic with a .NET backend (share models, validation, services)
  • Team knows C# and doesn't want to learn Swift/Kotlin/TypeScript
  • Internal enterprise apps with feature parity requirements across platforms
  • Desktop (Windows/macOS) app alongside mobile

When to choose native:

  • Performance-critical games or advanced UI animations
  • Cutting-edge platform features needed on day one
  • Large platform-specific teams already in place

Project Setup

Bash
dotnet new maui -n OrderFlow.Mobile
cd OrderFlow.Mobile
dotnet build -t:Run -f net9.0-android

Project structure:

OrderFlow.Mobile/
├── MauiProgram.cs          ← DI setup
├── App.xaml / App.xaml.cs  ← app lifecycle
├── AppShell.xaml           ← navigation
├── Pages/                  ← UI pages
├── ViewModels/             ← MVVM view models
├── Services/               ← HTTP, local storage
└── Platforms/              ← platform-specific code

MVVM: The Architecture Pattern

C#
// ViewModels/OrdersViewModel.cs
public partial class OrdersViewModel : ObservableObject
{
    private readonly IOrderService _orderService;

    [ObservableProperty]
    private ObservableCollection<OrderDto> _orders = new();

    [ObservableProperty]
    private bool _isLoading;

    [ObservableProperty]
    private string _errorMessage = "";

    public OrdersViewModel(IOrderService orderService)
    {
        _orderService = orderService;
    }

    [RelayCommand]
    private async Task LoadOrdersAsync()
    {
        IsLoading    = true;
        ErrorMessage = "";

        try
        {
            var result = await _orderService.GetOrdersAsync();
            Orders.Clear();
            foreach (var order in result) Orders.Add(order);
        }
        catch (Exception ex)
        {
            ErrorMessage = "Failed to load orders. Please check your connection.";
        }
        finally
        {
            IsLoading = false;
        }
    }
}

The [ObservableProperty] and [RelayCommand] attributes from CommunityToolkit.Mvvm generate boilerplate (INotifyPropertyChanged, command implementations).


Pages (UI)

XML
<!-- Pages/OrdersPage.xaml -->
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage x:Class="OrderFlow.Mobile.Pages.OrdersPage"
             xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:vm="clr-namespace:OrderFlow.Mobile.ViewModels"
             Title="Orders">

    <ContentPage.BindingContext>
        <vm:OrdersViewModel />
    </ContentPage.BindingContext>

    <Grid>
        <!-- Loading overlay -->
        <ActivityIndicator IsRunning="{Binding IsLoading}"
                           IsVisible="{Binding IsLoading}"
                           HorizontalOptions="Center"
                           VerticalOptions="Center" />

        <!-- Error message -->
        <Label Text="{Binding ErrorMessage}"
               IsVisible="{Binding ErrorMessage, Converter={StaticResource NotEmptyConverter}}"
               TextColor="Red"
               HorizontalOptions="Center" />

        <!-- Orders list -->
        <CollectionView ItemsSource="{Binding Orders}"
                        IsVisible="{Binding IsLoading, Converter={StaticResource InvertBoolConverter}}">
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <Grid Padding="16,8" ColumnDefinitions="*,Auto">
                        <VerticalStackLayout>
                            <Label Text="{Binding Id}" FontAttributes="Bold" />
                            <Label Text="{Binding CustomerName}" TextColor="Gray" />
                        </VerticalStackLayout>
                        <Label Grid.Column="1"
                               Text="{Binding Total, StringFormat='{0:C}'}"
                               VerticalOptions="Center" />
                    </Grid>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </Grid>
</ContentPage>

Shell Navigation

Shell provides a navigation hierarchy, flyout menu, and tab bar with minimal code:

XML
<!-- AppShell.xaml -->
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:pages="clr-namespace:OrderFlow.Mobile.Pages">

    <TabBar>
        <ShellContent Title="Orders" Icon="orders.svg" ContentTemplate="{DataTemplate pages:OrdersPage}" />
        <ShellContent Title="Products" Icon="products.svg" ContentTemplate="{DataTemplate pages:ProductsPage}" />
        <ShellContent Title="Profile" Icon="profile.svg" ContentTemplate="{DataTemplate pages:ProfilePage}" />
    </TabBar>
</Shell>
C#
// Navigate to a page with parameters
await Shell.Current.GoToAsync($"//orders/detail?id={orderId}");

// With typed parameters (query property)
[QueryProperty(nameof(OrderId), "id")]
public partial class OrderDetailPage : ContentPage
{
    public string OrderId { get; set; } = "";
}

HTTP Service with Authentication

C#
// Services/OrderService.cs
public class OrderService : IOrderService
{
    private readonly HttpClient _http;
    private readonly ISecureStorageService _storage;

    public OrderService(HttpClient http, ISecureStorageService storage)
    {
        _http    = http;
        _storage = storage;
    }

    public async Task<List<OrderDto>> GetOrdersAsync(CancellationToken ct = default)
    {
        var token = await _storage.GetTokenAsync();
        _http.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", token);

        var response = await _http.GetAsync("api/orders", ct);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<List<OrderDto>>(ct) ?? new();
    }
}

// MauiProgram.cs
builder.Services.AddHttpClient<IOrderService, OrderService>(client =>
{
    client.BaseAddress = new Uri("https://api.orderflow.com/");
    client.Timeout     = TimeSpan.FromSeconds(30);
});

Local Storage

C#
// Secure storage — for tokens and sensitive data
await SecureStorage.Default.SetAsync("auth_token", token);
var token = await SecureStorage.Default.GetAsync("auth_token");
SecureStorage.Default.Remove("auth_token");

// Preferences — for user settings
Preferences.Default.Set("theme",    "dark");
Preferences.Default.Set("pageSize", 20);
var theme = Preferences.Default.Get("theme", "light");

// File system — for larger data
var path    = Path.Combine(FileSystem.AppDataDirectory, "cache", "orders.json");
await File.WriteAllTextAsync(path, JsonSerializer.Serialize(orders));

Platform-Specific Code

C#
// Use preprocessor directives or partial classes
public partial class LocationService
{
    public partial Task<Location?> GetCurrentLocationAsync();
}

// Platforms/iOS/LocationService.cs
public partial class LocationService
{
    public partial async Task<Location?> GetCurrentLocationAsync()
    {
        var request = new GeolocationRequest(GeolocationAccuracy.High, TimeSpan.FromSeconds(10));
        return await Geolocation.Default.GetLocationAsync(request);
    }
}

// Or conditional compilation
#if ANDROID
    // Android-specific code
#elif IOS
    // iOS-specific code
#endif

DI Setup (MauiProgram.cs)

C#
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();

        builder.UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

        // Register services
        builder.Services.AddSingleton<IOrderService, OrderService>();
        builder.Services.AddSingleton<ISecureStorageService, SecureStorageService>();

        // Register view models as transient (new instance per page)
        builder.Services.AddTransient<OrdersViewModel>();
        builder.Services.AddTransient<OrderDetailViewModel>();

        // Register pages
        builder.Services.AddTransient<OrdersPage>();
        builder.Services.AddTransient<OrderDetailPage>();

        return builder.Build();
    }
}

Interview Questions

Q: What is the difference between .NET MAUI and Xamarin.Forms? MAUI is the successor to Xamarin.Forms. It targets .NET 6+ (single project, unified API), adds desktop support (Windows, macOS), improves performance with .NET 8/9, and has better tooling. Xamarin.Forms is end-of-life. New cross-platform apps should use MAUI.

Q: What is MVVM and why is it the standard pattern for MAUI? Model-View-ViewModel separates UI (View/XAML) from logic (ViewModel) and data (Model). The ViewModel exposes properties and commands; the View binds to them via data binding. This makes ViewModels testable without a UI, and keeps XAML files focused on layout. CommunityToolkit.Mvvm reduces boilerplate with source generators.

Q: How do you share code between a .NET MAUI app and an ASP.NET Core backend? Create a shared library project (.NET Standard 2.1 or net9.0) containing DTOs, validation logic (FluentValidation), domain models, and interfaces. Reference this library from both MAUI and the API. This gives you compile-time type safety across client and server with no duplication.

Q: What is Shell navigation in MAUI? A navigation system that provides flyout menus, tab bars, and a route-based navigation stack. You define the navigation hierarchy in AppShell.xaml. Navigate by URI: Shell.Current.GoToAsync("//orders/detail?id=123"). Routes can be registered for detail pages that aren't in the main hierarchy.

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.