19. Mobile targets: Android and iOS

Goal

Why this matters

Prerequisites

1. Projects and workload setup

Install .NET workloads and mobile SDKs:

# Android
sudo dotnet workload install android

# iOS (macOS only)
sudo dotnet workload install ios

# Optional: wasm-tools for browser
sudo dotnet workload install wasm-tools

Check workloads with dotnet workload list.

Project structure:

dotnet new avalonia.app --multiplatform creates the shared project plus heads (MyApp.Android, MyApp.iOS, optional MyApp.Browser). The Android head references Avalonia.Android (which contains AvaloniaActivity and AvaloniaApplication); the iOS head references Avalonia.iOS (which contains AvaloniaAppDelegate).

Keep trimming/linker settings in Directory.Build.props so shared code doesn't lose reflection-heavy ViewModels. Example additions:

<PropertyGroup>
  <TrimMode>partial</TrimMode>
  <IlcInvariantGlobalization>true</IlcInvariantGlobalization>
  <PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>

Use TrimmerRootAssembly or DynamicDependency attributes if you depend on reflection-heavy frameworks (e.g., ReactiveUI). Test Release builds on devices early to catch linker issues.

2. Single-view lifetime

ISingleViewApplicationLifetime hosts one root view. Configure in App.OnFrameworkInitializationCompleted (Chapter 4 showed desktop branch).

public override void OnFrameworkInitializationCompleted()
{
    var services = ConfigureServices();

    if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
    {
        singleView.MainView = services.GetRequiredService<ShellView>();
    }
    else if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
    {
        desktop.MainWindow = services.GetRequiredService<MainWindow>();
    }

    base.OnFrameworkInitializationCompleted();
}

ShellView is a UserControl with mobile-friendly layout and navigation.

Hot reload: on Android, Rider/Visual Studio can use .NET Hot Reload against MyApp.Android. For XAML hot reload in Previewer, add <ItemGroup><XamlIlAssemblyInfo>true</XamlIlAssemblyInfo></ItemGroup> to the shared project and keep the head running via dotnet build -t:Run.

3. Mobile navigation patterns

Use view-model-first navigation (Chapter 12) but ensure a visible Back control.

<UserControl xmlns="https://github.com/avaloniaui" x:Class="MyApp.Views.ShellView">
  <Grid RowDefinitions="Auto,*">
    <StackPanel Orientation="Horizontal" Spacing="8" Margin="16">
      <Button Content="Back"
              Command="{Binding BackCommand}"
              IsVisible="{Binding CanGoBack}"/>
      <TextBlock Text="{Binding Title}" FontSize="20" VerticalAlignment="Center"/>
    </StackPanel>
    <TransitioningContentControl Grid.Row="1" Content="{Binding Current}"/>
  </Grid>
</UserControl>

ShellViewModel keeps a stack of view models and implements BackCommand/NavigateTo. Hook Android back button (Next section) to BackCommand and mirror the same logic inside AvaloniaAppDelegate to react to swipe-back gestures on iOS.

4. Safe areas and input insets

Phones have notches and OS-controlled bars. Use IInsetsManager to apply safe-area padding.

public partial class ShellView : UserControl
{
    public ShellView()
    {
        InitializeComponent();
        this.AttachedToVisualTree += (_, __) =>
        {
            var top = TopLevel.GetTopLevel(this);
            var insets = top?.InsetsManager;
            if (insets is null) return;

            void ApplyInsets()
            {
                RootPanel.Padding = new Thickness(
                    insets.SafeAreaPadding.Left,
                    insets.SafeAreaPadding.Top,
                    insets.SafeAreaPadding.Right,
                    insets.SafeAreaPadding.Bottom);
            }

            ApplyInsets();
            insets.Changed += (_, __) => ApplyInsets();
        };
    }
}

Soft keyboard (IME) adjustments: subscribe to TopLevel.InputPane.Showing/Hiding and adjust margins above keyboard.

var pane = top?.InputPane;
if (pane is not null)
{
    pane.Showing += (_, args) => RootPanel.Margin = new Thickness(0, 0, 0, args.OccludedRect.Height);
    pane.Hiding += (_, __) => RootPanel.Margin = new Thickness(0);
}

Touch input specifics: prefer gesture recognizers (Tapped, DoubleTapped, PointerGestureRecognizer) over mouse events, and test with real hardware—emulators may not surface haptics or multi-touch.

5. Platform head customization

5.1 Android head (MyApp.Android)

public override void OnBackPressed()
{
    if (!AvaloniaApp.Current?.TryGoBack() ?? true)
        base.OnBackPressed();
}

TryGoBack calls into shared navigation service and returns true if you consumed the event. To embed Avalonia inside an existing native activity, host AvaloniaView inside a layout and call AvaloniaView.Initialize(this, AppBuilder.Configure<App>()...).

5.2 iOS head (MyApp.iOS)

Handle universal links or background tasks by bridging to shared services in AppDelegate. For swipe-back gestures, implement TryGoBack inside AvaloniaNavigationController or intercept UINavigationControllerDelegate callbacks.

5.3 Sharing services across heads

Inject platform implementations for IClipboard, IStorageProvider, notifications, and share targets via dependency injection. Register them in AvaloniaLocator.CurrentMutable inside CustomizeAppBuilder to keep shared code unaware of head-specific services.

6. Permissions & storage

7. Touch and gesture design

8. Performance & profiling

9. Packaging and deployment

Android

cd MyApp.Android
# Debug build to device
msbuild /t:Run /p:Configuration=Debug

# Release APK/AAB
msbuild /t:Publish /p:Configuration=Release /p:AndroidPackageFormat=aab

Sign with keystore for app store.

iOS

Optional: Tizen

Avalonia's Tizen backend (Avalonia.Tizen) targets smart TVs/wearables. The structure mirrors Android/iOS: implement a Tizen Program.cs that calls AppBuilder.Configure<App>().UseTizen<TizenApplication>() and handles platform storage/permissions via Tizen APIs.

10. Browser compatibility (bonus)

Mobile code often reuses single-view logic for WebAssembly. Check ApplicationLifetime for BrowserSingleViewLifetime and swap to a ShellView. Storage/clipboard behave like Chapter 16 with browser limitations.

11. Practice exercises

  1. Configure the Android/iOS heads and run the app on emulator/simulator with a shared ShellView.
  2. Implement a navigation service with back stack and wire Android back button to it.
  3. Adjust safe-area padding and keyboard insets for a login screen (Inputs remain visible when keyboard shows).
  4. Add file pickers via StorageProvider and test on device (consider permission prompts).
  5. Package a release build (.aab for Android, .ipa for iOS), validate icons/splash screens, and confirm Release trimming did not strip services.
  6. (Stretch) Embed Avalonia inside a native screen (AvaloniaView on Android, AvaloniaViewController on iOS) and pass data between native and Avalonia layers.

Look under the hood (source bookmarks)

Check yourself

What's next