Building Cross-Platform Applications
One of Avalonia UI's greatest strengths is its ability to run on multiple platforms from a single codebase. In this section, we'll explore how to build, optimize, and deploy cross-platform applications with Avalonia UI.
Platform-Specific Considerations
While Avalonia UI provides a consistent API across platforms, there are still platform-specific considerations to keep in mind when developing cross-platform applications.
Project Structure
A typical cross-platform Avalonia UI solution has the following structure:
MyApp/
├── MyApp/ # Core project (shared code)
│ ├── App.axaml # Application definition
│ ├── App.axaml.cs # Application code-behind
│ ├── Assets/ # Shared assets
│ ├── Models/ # Data models
│ ├── ViewModels/ # View models
│ ├── Views/ # UI views
│ └── Program.cs # Entry point (for desktop)
├── MyApp.Android/ # Android-specific project
├── MyApp.Desktop/ # Desktop-specific project
├── MyApp.iOS/ # iOS-specific project
└── MyApp.Browser/ # WebAssembly-specific project
You can create this structure using the Avalonia UI cross-platform template:
dotnet new avalonia.xplat -o MyApp
Platform Detection
You can detect the current platform at runtime:
/// <summary>
/// Detects the current operating system and executes platform-specific code.
/// </summary>
public void ExecutePlatformSpecificCode()
{
// Check if the application is running on Windows
if (OperatingSystem.IsWindows())
{
// Windows-specific code goes here
// For example: accessing Windows Registry or using Windows-specific APIs
Console.WriteLine("Running on Windows");
}
// Check if the application is running on macOS
else if (OperatingSystem.IsMacOS())
{
// macOS-specific code goes here
// For example: integrating with macOS services or UI conventions
Console.WriteLine("Running on macOS");
}
// Check if the application is running on Linux
else if (OperatingSystem.IsLinux())
{
// Linux-specific code goes here
// For example: interacting with Linux desktop environments
Console.WriteLine("Running on Linux");
}
// Check if the application is running on Android
else if (OperatingSystem.IsAndroid())
{
// Android-specific code goes here
// For example: accessing Android sensors or notifications
Console.WriteLine("Running on Android");
}
// Check if the application is running on iOS
else if (OperatingSystem.IsIOS())
{
// iOS-specific code goes here
// For example: integrating with iOS features like Face ID
Console.WriteLine("Running on iOS");
}
// Check if the application is running in a web browser via WebAssembly
else if (OperatingSystem.IsBrowser())
{
// WebAssembly-specific code goes here
// For example: interacting with browser APIs
Console.WriteLine("Running in a web browser");
}
}
Platform-Specific Services
For functionality that varies by platform, you can define interfaces in the core project and implement them in platform-specific projects:
// Core project - Define a common interface for file picking operations
/// <summary>
/// Interface for platform-specific file picker implementations.
/// This allows the core application to use file picking functionality
/// without knowing the platform-specific details.
/// </summary>
public interface IFilePickerService
{
/// <summary>
/// Opens a file picker dialog to allow the user to select a file.
/// </summary>
/// <param name="fileTypes">Array of file extensions that the dialog should filter for (e.g., "txt", "pdf")</param>
/// <returns>The full path to the selected file, or null if the user cancels</returns>
Task<string> PickFileAsync(string[] fileTypes);
/// <summary>
/// Opens a save file dialog to allow the user to specify where to save a file.
/// </summary>
/// <param name="defaultFileName">The default file name to suggest to the user</param>
/// <param name="fileTypes">Array of file extensions that the dialog should filter for</param>
/// <returns>The full path where the file should be saved, or null if the user cancels</returns>
Task<string> SaveFileAsync(string defaultFileName, string[] fileTypes);
}
// Platform-specific implementation for Windows
/// <summary>
/// Windows-specific implementation of the file picker service.
/// Uses the Windows native file dialogs.
/// </summary>
public class WindowsFilePickerService : IFilePickerService
{
/// <summary>
/// Implements file picking functionality using Windows native dialogs.
/// </summary>
/// <param name="fileTypes">Array of file extensions to filter by</param>
/// <returns>Selected file path or null</returns>
public async Task<string> PickFileAsync(string[] fileTypes)
{
// Windows-specific implementation
// On Windows, we would typically use the Windows.Storage.Pickers.FileOpenPicker
// or for .NET desktop applications, the Microsoft.Win32.OpenFileDialog
// Example implementation (pseudo-code):
// var dialog = new OpenFileDialog();
// dialog.Filters.Add(new FileDialogFilter { Name = "Supported Files", Extensions = fileTypes.ToList() });
// var result = await dialog.ShowAsync(App.MainWindow);
// return result?.FirstOrDefault();
return await Task.FromResult("C:\\Users\\Example\\Documents\\selected-file.txt");
}
/// <summary>
/// Implements save file dialog functionality using Windows native dialogs.
/// </summary>
/// <param name="defaultFileName">Default file name to suggest</param>
/// <param name="fileTypes">Array of file extensions to filter by</param>
/// <returns>Selected save path or null</returns>
public async Task<string> SaveFileAsync(string defaultFileName, string[] fileTypes)
{
// Windows-specific implementation
// On Windows, we would typically use the Windows.Storage.Pickers.FileSavePicker
// or for .NET desktop applications, the Microsoft.Win32.SaveFileDialog
// Example implementation (pseudo-code):
// var dialog = new SaveFileDialog();
// dialog.InitialFileName = defaultFileName;
// dialog.Filters.Add(new FileDialogFilter { Name = "Supported Files", Extensions = fileTypes.ToList() });
// return await dialog.ShowAsync(App.MainWindow);
return await Task.FromResult("C:\\Users\\Example\\Documents\\" + defaultFileName);
}
}
// Platform-specific implementation for Android
/// <summary>
/// Android-specific implementation of the file picker service.
/// Uses Android's Storage Access Framework.
/// </summary>
public class AndroidFilePickerService : IFilePickerService
{
// Android context is needed to launch intents
private readonly Context _context;
/// <summary>
/// Initializes a new instance of the AndroidFilePickerService.
/// </summary>
/// <param name="context">Android application context</param>
public AndroidFilePickerService(Context context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
/// <summary>
/// Implements file picking functionality using Android's Storage Access Framework.
/// </summary>
/// <param name="fileTypes">Array of file extensions to filter by</param>
/// <returns>Selected file path or null</returns>
public async Task<string> PickFileAsync(string[] fileTypes)
{
// Android-specific implementation
// On Android, we would typically use an Intent with ACTION_OPEN_DOCUMENT
// and handle the result in an activity
// Example implementation (pseudo-code):
// var intent = new Intent(Intent.ActionOpenDocument);
// intent.AddCategory(Intent.CategoryOpenable);
// intent.SetType("*/*");
// intent.PutExtra(Intent.ExtraMimeTypes, fileTypes.Select(ft => MimeTypeMap.GetMimeTypeFromExtension(ft)).ToArray());
// var result = await _context.StartActivityForResultAsync(intent);
// return result.Data?.Data?.ToString();
return await Task.FromResult("/storage/emulated/0/Download/selected-file.txt");
}
/// <summary>
/// Implements save file dialog functionality using Android's Storage Access Framework.
/// </summary>
/// <param name="defaultFileName">Default file name to suggest</param>
/// <param name="fileTypes">Array of file extensions to filter by</param>
/// <returns>Selected save path or null</returns>
public async Task<string> SaveFileAsync(string defaultFileName, string[] fileTypes)
{
// Android-specific implementation
// On Android, we would typically use an Intent with ACTION_CREATE_DOCUMENT
// and handle the result in an activity
// Example implementation (pseudo-code):
// var intent = new Intent(Intent.ActionCreateDocument);
// intent.AddCategory(Intent.CategoryOpenable);
// intent.SetType(MimeTypeMap.GetMimeTypeFromExtension(fileTypes.FirstOrDefault() ?? "txt"));
// intent.PutExtra(Intent.ExtraTitle, defaultFileName);
// var result = await _context.StartActivityForResultAsync(intent);
// return result.Data?.Data?.ToString();
return await Task.FromResult("/storage/emulated/0/Download/" + defaultFileName);
}
}
Register the appropriate implementation at startup:
// In App.axaml.cs
/// <summary>
/// Registers platform-specific services with the dependency injection container.
/// This method is called during application startup to configure services
/// appropriate for the current platform.
/// </summary>
public override void RegisterServices()
{
// Create a service collection for dependency injection
var services = new ServiceCollection();
// Declare a variable to hold the appropriate file picker service implementation
IFilePickerService filePickerService;
// Determine which platform the application is running on and create the appropriate service
// For desktop platforms (Windows, macOS, Linux), use the desktop implementation
if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())
{
// Desktop platforms share similar file system access patterns
// so we can use a common implementation with platform-specific adjustments
filePickerService = new DesktopFilePickerService();
// Log the platform detection for debugging purposes
Console.WriteLine("Registered desktop file picker service");
}
// For Android, use the Android-specific implementation
else if (OperatingSystem.IsAndroid())
{
// Android requires context to access the file system via intents
// Get the Android application context from the current activity
var context = Android.App.Application.Context;
filePickerService = new AndroidFilePickerService(context);
Console.WriteLine("Registered Android file picker service");
}
// For iOS, use the iOS-specific implementation
else if (OperatingSystem.IsIOS())
{
// iOS has its own file picking mechanisms via UIDocumentPickerViewController
filePickerService = new IOSFilePickerService();
Console.WriteLine("Registered iOS file picker service");
}
// For WebAssembly or any other platform, use the web implementation
else
{
// Web platforms use browser APIs for file access
filePickerService = new WebFilePickerService();
Console.WriteLine("Registered web file picker service");
}
// Register the service with the dependency injection container
// This makes the service available throughout the application
// without components needing to know which specific implementation is being used
// For example, with Microsoft.Extensions.DependencyInjection:
services.AddSingleton<IFilePickerService>(filePickerService);
// Register other services as needed...
// Build the service provider
ServiceProvider = services.BuildServiceProvider();
}
/// <summary>
/// Gets the service provider for dependency injection.
/// </summary>
public IServiceProvider ServiceProvider { get; private set; }
/// <summary>
/// Helper method to get a service from the dependency injection container.
/// </summary>
/// <typeparam name="T">The type of service to retrieve</typeparam>
/// <returns>The requested service</returns>
public T GetService<T>() where T : class
{
return ServiceProvider.GetRequiredService<T>();
}
Platform-Specific UI Adjustments
You can make platform-specific UI adjustments using styles:
<!-- App.axaml or a separate Styles.axaml file -->
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Base style for all buttons across platforms -->
<!-- This provides a consistent foundation for button styling -->
<Style Selector="Button">
<!-- Default padding suitable for desktop platforms -->
<Setter Property="Padding" Value="10,5" />
<!-- Consistent corner radius across platforms -->
<Setter Property="CornerRadius" Value="4" />
<!-- Default font size -->
<Setter Property="FontSize" Value="14" />
<!-- Ensure text is centered -->
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<!-- Platform-specific adjustments for touch-based platforms (Android/iOS) -->
<!-- These styles will only apply when running on mobile devices -->
<Style Selector="Button.mobile">
<!-- Increase padding for better touch targets -->
<Setter Property="Padding" Value="16,12" />
<!-- Slightly larger font for better readability on mobile -->
<Setter Property="FontSize" Value="16" />
<!-- Add a subtle animation for touch feedback -->
<Style.Animations>
<!-- Animation that runs when the button is pressed -->
<Animation Duration="0:0:0.2">
<!-- Starting state (normal size) -->
<KeyFrame Cue="0%">
<Setter Property="Padding" Value="16,12" />
<Setter Property="Opacity" Value="1.0" />
</KeyFrame>
<!-- Ending state (slightly larger with opacity change for visual feedback) -->
<KeyFrame Cue="100%">
<Setter Property="Padding" Value="18,14" />
<Setter Property="Opacity" Value="0.8" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<!-- WebAssembly-specific adjustments -->
<Style Selector="Button.web">
<!-- Web platforms often need different styling to match web conventions -->
<Setter Property="Padding" Value="12,8" />
<!-- Flatter appearance for web -->
<Setter Property="BorderThickness" Value="1" />
<!-- Subtle hover effect for web interfaces -->
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Background" Duration="0:0:0.1" />
</Transitions>
</Setter>
</Style>
<!-- High-DPI display adjustments (typically desktop) -->
<Style Selector="Button.highDpi">
<!-- Sharper corners for high-resolution displays -->
<Setter Property="CornerRadius" Value="2" />
<!-- Thinner borders for crisp appearance -->
<Setter Property="BorderThickness" Value="1" />
</Style>
</Styles>
Or using conditional compilation:
/// <summary>
/// Configures UI elements with platform-specific settings at runtime.
/// This method demonstrates how to use conditional compilation to apply
/// different UI configurations based on the target platform.
/// </summary>
/// <remarks>
/// Conditional compilation directives like #if, #elif, and #endif allow
/// different code to be compiled for different platforms. This is determined
/// at build time, not runtime, based on defined preprocessor symbols.
///
/// Common platform symbols include:
/// - WINDOWS, LINUX, OSX for desktop platforms
/// - ANDROID, IOS for mobile platforms
/// - BROWSER for WebAssembly
///
/// You can define these symbols in your project file or build configuration.
/// </remarks>
public void ConfigureUI()
{
// Log which configuration is being applied
Console.WriteLine("Configuring UI for platform...");
#if ANDROID || IOS
// Mobile-specific UI adjustments
// These adjustments will only be compiled into the mobile versions
// Increase font size for better readability on smaller screens
FontSize = 16;
// Add more padding for touch targets
Padding = new Thickness(20);
// Adjust spacing between elements for touch interfaces
Spacing = 15;
// Use larger icons for mobile
IconSize = 32;
Console.WriteLine("Applied mobile UI configuration");
#elif BROWSER
// WebAssembly-specific UI adjustments
// These will only be compiled into the web version
// Medium font size for web
FontSize = 14;
// Moderate padding for web interfaces
Padding = new Thickness(15);
// Standard web spacing
Spacing = 10;
// Standard icon size for web
IconSize = 24;
Console.WriteLine("Applied web UI configuration");
#else
// Desktop-specific UI adjustments (Windows, macOS, Linux)
// This is the default for non-mobile, non-web platforms
// Smaller font size for desktop where precision is expected
FontSize = 12;
// Less padding for desktop where mouse precision allows smaller targets
Padding = new Thickness(10);
// Tighter spacing for desktop interfaces
Spacing = 8;
// Standard icon size for desktop
IconSize = 16;
Console.WriteLine("Applied desktop UI configuration");
#endif
// Common configuration for all platforms
ApplyTheme();
ConfigureAccessibility();
}
/// <summary>
/// Applies the appropriate theme based on system settings.
/// </summary>
private void ApplyTheme()
{
// Implementation details...
}
/// <summary>
/// Configures accessibility features for the UI.
/// </summary>
private void ConfigureAccessibility()
{
// Implementation details...
}
Platform-Specific Resources
You can include platform-specific resources in each platform project:
# Project structure with platform-specific resources
MyApp/
├── MyApp/ # Core shared project
│ ├── Assets/ # Shared assets for all platforms
│ │ ├── logo.png # Default logo (used if no platform-specific version exists)
│ │ ├── icons/ # Shared icons
│ │ │ ├── app-icon.png # Generic app icon
│ │ │ ├── settings-icon.png # Settings icon
│ │ ├── fonts/ # Shared fonts
│ │ │ ├── OpenSans-Regular.ttf # Default font
│ │ ├── sounds/ # Shared sound effects
│ │ │ ├── notification.mp3 # Notification sound
├── MyApp.Android/ # Android-specific project
│ ├── Resources/ # Android resource folder (follows Android conventions)
│ │ ├── drawable/ # Images for various screen densities
│ │ │ ├── logo.png # Android-optimized logo
│ │ │ ├── splash_screen.png # Android splash screen
│ │ ├── mipmap/ # App icons for different densities
│ │ │ ├── ic_launcher.png # Android app icon
│ │ ├── values/ # Android-specific values
│ │ │ ├── colors.xml # Android color definitions
├── MyApp.iOS/ # iOS-specific project
│ ├── Resources/ # iOS resource folder (follows iOS conventions)
│ │ ├── Images.xcassets/ # Asset catalog for iOS
│ │ │ ├── AppIcon.appiconset/ # iOS app icons
│ │ │ ├── LaunchImage.launchimage/ # iOS launch images
│ │ ├── logo.png # iOS-optimized logo
│ │ ├── Default.png # iOS splash screen
├── MyApp.Desktop/ # Desktop-specific project
│ ├── Assets/ # Desktop-specific assets
│ │ ├── logo-hires.png # High-resolution logo for desktop
│ │ ├── icon.ico # Windows icon file
├── MyApp.Browser/ # WebAssembly-specific project
├── wwwroot/ # Web assets
├── images/ # Web images
│ ├── logo.png # Web-optimized logo
├── css/ # Web stylesheets
│ ├── app.css # Web-specific styles
Now let's see how to load these platform-specific resources appropriately in your code:
/// <summary>
/// ResourceManager class that handles loading platform-specific resources.
/// This class demonstrates how to load different resources based on the platform
/// the application is running on.
/// </summary>
public class ResourceManager
{
/// <summary>
/// Gets the appropriate path to the logo image based on the current platform.
/// </summary>
/// <returns>A string containing the resource URI for the logo</returns>
/// <remarks>
/// Avalonia uses the "avares://" URI scheme to reference embedded resources.
/// The format is "avares://AssemblyName/PathToResource"
/// </remarks>
public string GetLogoPath()
{
// Check which platform we're running on and return the appropriate path
if (OperatingSystem.IsAndroid())
{
// Android resources follow the Android resource naming conventions
// and are stored in the Resources/drawable folder
return "avares://MyApp.Android/Resources/drawable/logo.png";
}
else if (OperatingSystem.IsIOS())
{
// iOS resources are stored in the Resources folder
return "avares://MyApp.iOS/Resources/logo.png";
}
else if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())
{
// For desktop platforms, we can use high-resolution assets
return "avares://MyApp.Desktop/Assets/logo-hires.png";
}
else if (OperatingSystem.IsBrowser())
{
// For web, use web-optimized assets
return "avares://MyApp.Browser/wwwroot/images/logo.png";
}
else
{
// Fall back to the default logo in the shared project
// This ensures we always have a logo even on unsupported platforms
return "avares://MyApp/Assets/logo.png";
}
}
/// <summary>
/// Loads an image from the appropriate platform-specific resource.
/// </summary>
/// <param name="resourceName">Base name of the resource to load</param>
/// <returns>A loaded Bitmap object</returns>
public async Task<Bitmap> LoadImageAsync(string resourceName)
{
// Get the platform-specific path
string resourcePath = GetResourcePath(resourceName, "png");
// Load the image using Avalonia's asset loader
var assets = AvaloniaLocator.Current.GetService<IAssetLoader>();
using (var stream = assets.Open(new Uri(resourcePath)))
{
return new Bitmap(stream);
}
}
/// <summary>
/// Gets the path to a resource with the appropriate platform-specific version.
/// </summary>
/// <param name="baseName">Base name of the resource without extension</param>
/// <param name="extension">File extension of the resource</param>
/// <returns>Full resource path</returns>
private string GetResourcePath(string baseName, string extension)
{
// Platform-specific folder structure
string platformFolder;
string assemblyName;
if (OperatingSystem.IsAndroid())
{
assemblyName = "MyApp.Android";
platformFolder = "Resources/drawable";
}
else if (OperatingSystem.IsIOS())
{
assemblyName = "MyApp.iOS";
platformFolder = "Resources";
}
else if (OperatingSystem.IsBrowser())
{
assemblyName = "MyApp.Browser";
platformFolder = "wwwroot/images";
}
else if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())
{
assemblyName = "MyApp.Desktop";
platformFolder = "Assets";
}
else
{
assemblyName = "MyApp";
platformFolder = "Assets";
}
// Construct the full path
return $"avares://{assemblyName}/{platformFolder}/{baseName}.{extension}";
}
}
Usage example:
// In a view model or control
private async void LoadResources()
{
var resourceManager = new ResourceManager();
// Get the logo path
string logoPath = resourceManager.GetLogoPath();
// Use the path in XAML binding
LogoPath = logoPath;
// Or load the image directly
LogoImage = await resourceManager.LoadImageAsync("logo");
// You can also use the resource path directly in XAML:
// <Image Source="{Binding LogoPath}" />
}
Deployment Options
Avalonia UI applications can be deployed to various platforms using different approaches.
Desktop Deployment
When deploying Avalonia UI applications to desktop platforms (Windows, macOS, and Linux), you have several options depending on your requirements for distribution, installation, and runtime dependencies.
Self-Contained Deployment
A self-contained deployment includes the .NET runtime and all dependencies within your application package. This approach offers several advantages:
- No .NET Runtime Requirement: Users don't need to install .NET separately
- Version Control: You control exactly which .NET version is used
- Simplified Distribution: Single package with everything needed to run
- Reduced Compatibility Issues: Less concern about missing dependencies
However, self-contained deployments result in larger package sizes since they include the entire runtime.
# Windows (x64) - Creates a single executable with all dependencies
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=true
# macOS (x64) - Creates a package with all dependencies
dotnet publish -c Release -r osx-x64 --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=true
# Linux (x64) - Creates a package with all dependencies
dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=true
Command Options Explained:
-c Release
: Build in Release configuration (optimized, no debug symbols)-r [RID]
: Runtime identifier specifying the target platform--self-contained true
: Include the .NET runtime in the published output-p:PublishSingleFile=true
: Combine everything into a single executable file-p:PublishTrimmed=true
: Remove unused parts of the framework to reduce size
Common Runtime Identifiers (RIDs):
- Windows:
win-x64
,win-x86
,win-arm64
- macOS:
osx-x64
,osx-arm64
(for Apple Silicon) - Linux:
linux-x64
,linux-arm
,linux-arm64
Framework-Dependent Deployment
A framework-dependent deployment requires the .NET runtime to be installed on the target machine. This approach has different advantages:
- Smaller Package Size: The application package is much smaller
- Shared Runtime: Multiple applications can share the same runtime
- Automatic Updates: Runtime updates can be managed centrally
- Reduced Disk Space: Less storage required when running multiple .NET apps
# Create a framework-dependent deployment
dotnet publish -c Release
# Specify a target framework version
dotnet publish -c Release -f net8.0
Command Options Explained:
-f net8.0
: Target a specific .NET framework version- No runtime identifier is specified, so it builds for any platform where the framework is available
Choosing Between Deployment Types
Consideration | Self-Contained | Framework-Dependent |
---|---|---|
Package Size | Larger (50-150MB) | Smaller (5-20MB) |
Prerequisites | None | .NET runtime required |
Deployment Simplicity | Higher | Lower |
Disk Space Efficiency | Lower | Higher |
Update Process | Entire app must be updated | Runtime can be updated separately |
Best For | End-user applications, commercial software | Internal tools, developer utilities |
Windows-Specific Packaging
For Windows, creating a professional installer provides several benefits:
- Proper application registration in Windows
- Start menu shortcuts and desktop icons
- File associations
- Uninstall functionality
- Optional updates and maintenance
Several popular tools are available for creating Windows installers:
1. WiX Toolset (Windows Installer XML)
WiX is a powerful, open-source toolset that builds Windows installation packages from XML source code. It's highly customizable and used by many professional applications.
Installation:
# Install WiX Toolset
dotnet tool install --global wix
Example using WiX Toolset:
<!-- Product.wxs - Main installer definition file -->
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<!-- Product information -->
<Product Id="*"
Name="MyAvalonia App"
Language="1033"
Version="1.0.0.0"
Manufacturer="MyCompany"
UpgradeCode="PUT-GUID-HERE">
<!-- Package information -->
<Package InstallerVersion="200"
Compressed="yes"
InstallScope="perMachine"
Description="Installer for MyAvalonia App"
Comments="Requires Windows 7 or later" />
<!-- Upgrade handling - prevents downgrades and handles upgrades -->
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed."
AllowSameVersionUpgrades="no" />
<!-- Embed all cabinet files in the MSI -->
<MediaTemplate EmbedCab="yes" />
<!-- Application icon shown in Add/Remove Programs -->
<Icon Id="AppIcon.ico" SourceFile="$(var.MyApp.ProjectDir)\Assets\icon.ico" />
<Property Id="ARPPRODUCTICON" Value="AppIcon.ico" />
<!-- URL for support information -->
<Property Id="ARPURLINFOABOUT" Value="https://www.mycompany.com/support" />
<!-- Features to install -->
<Feature Id="ProductFeature" Title="MyAvalonia App" Level="1">
<ComponentGroupRef Id="ProductComponents" />
<ComponentRef Id="ApplicationShortcut" />
</Feature>
</Product>
<!-- Directory structure for installation -->
<Fragment>
<Directory Id="TARGETDIR" Name="SourceDir">
<!-- Program Files folder -->
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLFOLDER" Name="MyAvalonia App" />
</Directory>
<!-- Start Menu folder -->
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="MyAvalonia App" />
</Directory>
<!-- Desktop folder -->
<Directory Id="DesktopFolder" Name="Desktop" />
</Directory>
</Fragment>
<!-- Application files to install -->
<Fragment>
<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
<!-- Main executable -->
<Component Id="ProductComponent" Guid="*">
<File Id="MyAppEXE"
Source="$(var.MyApp.TargetPath)"
KeyPath="yes">
<!-- File association example -->
<ProgId Id="MyApp.Document" Description="MyAvalonia Document">
<Extension Id="myapp" ContentType="application/x-myapp">
<Verb Id="open" Command="Open" TargetFile="MyAppEXE" Argument='"%1"' />
</Extension>
</ProgId>
</File>
<!-- Add other application files -->
<File Source="$(var.MyApp.TargetDir)\Avalonia.dll" />
<File Source="$(var.MyApp.TargetDir)\Avalonia.Desktop.dll" />
<!-- Add more dependencies as needed -->
</Component>
</ComponentGroup>
<!-- Start Menu shortcut -->
<DirectoryRef Id="ApplicationProgramsFolder">
<Component Id="ApplicationShortcut" Guid="*">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="MyAvalonia App"
Description="Launch MyAvalonia App"
Target="[#MyAppEXE]"
WorkingDirectory="INSTALLFOLDER" />
<RemoveFolder Id="RemoveApplicationProgramsFolder" On="uninstall" />
<RegistryValue Root="HKCU"
Key="Software\MyCompany\MyAvaloniaApp"
Name="installed"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
</DirectoryRef>
</Fragment>
</Wix>
Building the WiX installer:
# Compile the WiX source files
candle.exe Product.wxs -ext WixUIExtension
# Link the object files into an MSI
light.exe Product.wixobj -ext WixUIExtension -out MyAvaloniaApp.msi
2. Inno Setup
Inno Setup is a free installer for Windows programs, known for its simplicity and powerful scripting capabilities.
Example Inno Setup Script:
; MyAvaloniaApp.iss - Inno Setup script for MyAvalonia App
; Basic application information
#define MyAppName "MyAvalonia App"
#define MyAppVersion "1.0.0"
#define MyAppPublisher "MyCompany"
#define MyAppURL "https://www.mycompany.com"
#define MyAppExeName "MyApp.exe"
[Setup]
; Installer settings
AppId={{YOUR-GUID-HERE}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppName}
AllowNoIcons=yes
; Compression settings
Compression=lzma
SolidCompression=yes
; Visual settings
WizardStyle=modern
; Output settings
OutputDir=installer
OutputBaseFilename=MyAvaloniaApp-Setup
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode
[Files]
; Main executable and dependencies
Source: "bin\Release\net8.0\win-x64\publish\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "bin\Release\net8.0\win-x64\publish\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
; Start menu and desktop shortcuts
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
; Option to launch the app after installation
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
3. NSIS (Nullsoft Scriptable Install System)
NSIS is a professional open-source system to create Windows installers with a small footprint and high flexibility.
Example NSIS Script:
; MyAvaloniaApp.nsi - NSIS script for MyAvalonia App
; Define application name and version
!define APPNAME "MyAvalonia App"
!define APPVERSION "1.0.0"
!define COMPANYNAME "MyCompany"
!define DESCRIPTION "Cross-platform application built with Avalonia UI"
; Main install settings
Name "${APPNAME}"
InstallDir "$PROGRAMFILES64\${APPNAME}"
InstallDirRegKey HKLM "Software\${APPNAME}" "Install_Dir"
OutFile "MyAvaloniaApp-Setup.exe"
; Modern interface settings
!include "MUI2.nsh"
!define MUI_ABORTWARNING
!define MUI_ICON "Assets\icon.ico"
!define MUI_UNICON "Assets\icon.ico"
; Pages
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_LICENSE "LICENSE.txt"
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
; Uninstaller pages
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
; Set languages (first is default language)
!insertmacro MUI_LANGUAGE "English"
Section "Install"
SetOutPath $INSTDIR
; Install program files
File /r "bin\Release\net8.0\win-x64\publish\*.*"
; Create uninstaller
WriteUninstaller "$INSTDIR\Uninstall.exe"
; Create start menu shortcut
CreateDirectory "$SMPROGRAMS\${APPNAME}"
CreateShortcut "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk" "$INSTDIR\MyApp.exe"
CreateShortcut "$SMPROGRAMS\${APPNAME}\Uninstall.lnk" "$INSTDIR\Uninstall.exe"
; Create desktop shortcut
CreateShortcut "$DESKTOP\${APPNAME}.lnk" "$INSTDIR\MyApp.exe"
; Write registry information
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "DisplayName" "${APPNAME}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "UninstallString" "$\"$INSTDIR\Uninstall.exe$\""
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "DisplayIcon" "$INSTDIR\MyApp.exe"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "DisplayVersion" "${APPVERSION}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "Publisher" "${COMPANYNAME}"
SectionEnd
Section "Uninstall"
; Remove program files
RMDir /r "$INSTDIR"
; Remove start menu items
RMDir /r "$SMPROGRAMS\${APPNAME}"
; Remove desktop shortcut
Delete "$DESKTOP\${APPNAME}.lnk"
; Remove registry keys
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}"
DeleteRegKey HKLM "Software\${APPNAME}"
SectionEnd
Choosing a Windows Installer Tool
Tool | Pros | Cons | Best For |
---|---|---|---|
WiX Toolset | Industry standard, MSI format, highly customizable | Steeper learning curve, XML-based | Enterprise applications, complex installations |
Inno Setup | Easy to use, small installer size, script-based | Less enterprise features | Small to medium applications, indie developers |
NSIS | Very flexible, small footprint, powerful scripting | More complex than Inno Setup | Applications needing custom installation logic |
macOS-Specific Packaging
macOS applications are distributed as .app
bundles, which are special directories with a specific structure that macOS recognizes as applications. For professional distribution, these bundles are typically packaged in a .dmg
(disk image) file, which provides a nice installation experience.
Creating a macOS App Bundle
An .app
bundle for an Avalonia application has the following structure:
MyApp.app/
├── Contents/
│ ├── Info.plist # Application metadata
│ ├── MacOS/ # Executable files
│ │ └── MyApp # Main executable
│ ├── Resources/ # Application resources
│ │ ├── MyApp.icns # Application icon
│ │ └── [other resources]
│ └── _CodeSignature/ # Code signature (if signed)
You can create a .app
bundle using the .NET SDK:
# Create a .app bundle for Intel Macs (x64)
dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-x64 -p:Configuration=Release
# Create a .app bundle for Apple Silicon Macs (ARM64)
dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-arm64 -p:Configuration=Release
# Create a universal .app bundle (works on both Intel and Apple Silicon)
dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-x64 -p:UseAppHost=true -p:CFBundleIdentifier=com.mycompany.myapp -p:Configuration=Release -p:TargetFrameworkVersion=net8.0 -p:CreatePackage=true -p:EnableDefaultItems=true -p:EnableDefaultCompileItems=true -p:EnableDefaultEmbeddedResourceItems=true
Customizing the App Bundle
To customize your .app
bundle, you can create an Info.plist
file in your project:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Basic application information -->
<key>CFBundleIdentifier</key>
<string>com.mycompany.myapp</string>
<key>CFBundleName</key>
<string>MyAvalonia App</string>
<key>CFBundleDisplayName</key>
<string>MyAvalonia App</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleExecutable</key>
<string>MyApp</string>
<!-- Application icon -->
<key>CFBundleIconFile</key>
<string>MyApp.icns</string>
<!-- Document types the application can open -->
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>MyApp Document</string>
<key>CFBundleTypeExtensions</key>
<array>
<string>myapp</string>
</array>
<key>CFBundleTypeRole</key>
<string>Editor</string>
</dict>
</array>
<!-- macOS specific settings -->
<key>NSHighResolutionCapable</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2024 MyCompany. All rights reserved.</string>
<key>LSMinimumSystemVersion</key>
<string>10.15</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string>
<!-- Hardened runtime for notarization -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
Creating a DMG Installer
A .dmg
(disk image) file provides a professional way to distribute macOS applications. When opened, it typically shows a stylized window with the application and a shortcut to the Applications folder, making it easy for users to drag and drop the app to install it.
You can create a .dmg
file using the create-dmg
tool:
# Install create-dmg (requires Homebrew)
brew install create-dmg
# Create a basic DMG
create-dmg \
--volname "MyAvalonia App" \
--window-pos 200 120 \
--window-size 800 400 \
--icon-size 100 \
--icon "MyApp.app" 200 190 \
--hide-extension "MyApp.app" \
--app-drop-link 600 185 \
"MyAvaloniaApp.dmg" \
"path/to/MyApp.app"
For a more customized DMG with a background image and precise positioning:
# Create a customized DMG
create-dmg \
--volname "MyAvalonia App" \
--volicon "AppIcon.icns" \
--background "DMG-Background.png" \
--window-pos 200 120 \
--window-size 800 500 \
--icon-size 128 \
--icon "MyApp.app" 200 240 \
--hide-extension "MyApp.app" \
--app-drop-link 600 240 \
--text-size 14 \
--eula "License.txt" \
"MyAvaloniaApp.dmg" \
"path/to/MyApp.app"
Code Signing and Notarization
For distribution outside the Mac App Store, Apple requires applications to be both code signed and notarized:
- Code Signing authenticates your app and ensures it hasn't been tampered with.
- Notarization verifies your app is free from known malware.
# Code sign the app
codesign --force --options runtime --sign "Developer ID Application: Your Name (TEAM_ID)" "MyApp.app"
# Create a ZIP archive for notarization
ditto -c -k --keepParent "MyApp.app" "MyApp.zip"
# Submit for notarization
xcrun notarytool submit "MyApp.zip" --apple-id "your.email@example.com" --password "app-specific-password" --team-id "TEAM_ID" --wait
# After successful notarization, staple the ticket to your app
xcrun stapler staple "MyApp.app"
# Now create the DMG with the notarized app
create-dmg [options] "MyAvaloniaApp.dmg" "path/to/MyApp.app"
# Sign the DMG itself
codesign --force --sign "Developer ID Application: Your Name (TEAM_ID)" "MyAvaloniaApp.dmg"
Automating macOS Packaging
You can automate the entire process using a shell script:
#!/bin/bash
# macOS packaging script for Avalonia UI application
# Configuration
APP_NAME="MyAvalonia App"
BUNDLE_ID="com.mycompany.myapp"
VERSION="1.0.0"
DEVELOPER_ID="Developer ID Application: Your Name (TEAM_ID)"
APPLE_ID="your.email@example.com"
APP_PASSWORD="app-specific-password"
TEAM_ID="TEAM_ID"
# Build the app
echo "Building application..."
dotnet publish -c Release -r osx-x64 --self-contained true
# Create app bundle
echo "Creating .app bundle..."
mkdir -p "dist/$APP_NAME.app/Contents/MacOS"
mkdir -p "dist/$APP_NAME.app/Contents/Resources"
# Copy files
cp -R "bin/Release/net8.0/osx-x64/publish/"* "dist/$APP_NAME.app/Contents/MacOS/"
cp "AppIcon.icns" "dist/$APP_NAME.app/Contents/Resources/"
# Create Info.plist
cat > "dist/$APP_NAME.app/Contents/Info.plist" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>$BUNDLE_ID</string>
<key>CFBundleName</key>
<string>$APP_NAME</string>
<key>CFBundleDisplayName</key>
<string>$APP_NAME</string>
<key>CFBundleVersion</key>
<string>$VERSION</string>
<key>CFBundleShortVersionString</key>
<string>$VERSION</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleExecutable</key>
<string>MyApp</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
EOF
# Code sign
echo "Code signing..."
codesign --force --options runtime --sign "$DEVELOPER_ID" "dist/$APP_NAME.app"
# Notarize
echo "Notarizing..."
ditto -c -k --keepParent "dist/$APP_NAME.app" "dist/$APP_NAME.zip"
xcrun notarytool submit "dist/$APP_NAME.zip" --apple-id "$APPLE_ID" --password "$APP_PASSWORD" --team-id "$TEAM_ID" --wait
# Staple
echo "Stapling ticket..."
xcrun stapler staple "dist/$APP_NAME.app"
# Create DMG
echo "Creating DMG..."
create-dmg \
--volname "$APP_NAME" \
--volicon "AppIcon.icns" \
--window-pos 200 120 \
--window-size 800 400 \
--icon-size 100 \
--icon "$APP_NAME.app" 200 190 \
--hide-extension "$APP_NAME.app" \
--app-drop-link 600 185 \
"dist/$APP_NAME.dmg" \
"dist/$APP_NAME.app"
# Sign DMG
codesign --force --sign "$DEVELOPER_ID" "dist/$APP_NAME.dmg"
echo "Packaging complete!"
Linux-Specific Packaging
Linux distributions use different package formats and package managers. The most common formats are:
.deb
for Debian-based distributions (Debian, Ubuntu, Linux Mint, etc.).rpm
for Red Hat-based distributions (Fedora, RHEL, CentOS, etc.).AppImage
for distribution-agnostic packages- Flatpak and Snap for containerized applications
Creating .DEB Packages
Debian packages (.deb
) are used by Debian, Ubuntu, and their derivatives. You can create them using the dotnet-deb
tool:
# Install the dotnet-deb tool
dotnet tool install -g dotnet-deb
# Create a .deb package
dotnet deb -c Release -r linux-x64 --self-contained
For more control, you can create a .deb
package manually:
# Create the package directory structure
mkdir -p myapp_1.0.0_amd64/DEBIAN
mkdir -p myapp_1.0.0_amd64/usr/local/bin/myapp
mkdir -p myapp_1.0.0_amd64/usr/share/applications
mkdir -p myapp_1.0.0_amd64/usr/share/icons/hicolor/256x256/apps
# Create the control file
cat > myapp_1.0.0_amd64/DEBIAN/control << EOF
Package: myapp
Version: 1.0.0
Section: utils
Priority: optional
Architecture: amd64
Maintainer: Your Name <your.email@example.com>
Description: MyAvalonia App
A cross-platform application built with Avalonia UI.
This application provides useful functionality for users.
Depends: libc6 (>= 2.27), libgcc1 (>= 1:8.4.0)
EOF
# Copy the application files
cp -r bin/Release/net8.0/linux-x64/publish/* myapp_1.0.0_amd64/usr/local/bin/myapp/
# Create a desktop entry
cat > myapp_1.0.0_amd64/usr/share/applications/myapp.desktop << EOF
[Desktop Entry]
Name=MyAvalonia App
Comment=A cross-platform application built with Avalonia UI
Exec=/usr/local/bin/myapp/MyApp
Icon=myapp
Terminal=false
Type=Application
Categories=Utility;
EOF
# Copy the icon
cp AppIcon.png myapp_1.0.0_amd64/usr/share/icons/hicolor/256x256/apps/myapp.png
# Build the package
dpkg-deb --build myapp_1.0.0_amd64
Creating .RPM Packages
RPM packages (.rpm
) are used by Fedora, RHEL, CentOS, and other Red Hat-based distributions. You can create them using the dotnet-rpm
tool:
# Install the dotnet-rpm tool
dotnet tool install -g dotnet-rpm
# Create an .rpm package
dotnet rpm -c Release -r linux-x64 --self-contained
For more control, you can create an .rpm
package manually using a spec file:
# myapp.spec
Name: myapp
Version: 1.0.0
Release: 1%{?dist}
Summary: MyAvalonia App
License: MIT
URL: https://www.mycompany.com
Source0: %{name}-%{version}.tar.gz
BuildArch: x86_64
Requires: glibc >= 2.17
%description
A cross-platform application built with Avalonia UI.
This application provides useful functionality for users.
%prep
%setup -q
%install
mkdir -p %{buildroot}/usr/local/bin/myapp
mkdir -p %{buildroot}/usr/share/applications
mkdir -p %{buildroot}/usr/share/icons/hicolor/256x256/apps
cp -r * %{buildroot}/usr/local/bin/myapp/
cat > %{buildroot}/usr/share/applications/myapp.desktop << EOF
[Desktop Entry]
Name=MyAvalonia App
Comment=A cross-platform application built with Avalonia UI
Exec=/usr/local/bin/myapp/MyApp
Icon=myapp
Terminal=false
Type=Application
Categories=Utility;
EOF
cp AppIcon.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/myapp.png
%files
/usr/local/bin/myapp
/usr/share/applications/myapp.desktop
/usr/share/icons/hicolor/256x256/apps/myapp.png
%changelog
* Wed May 15 2024 Your Name <your.email@example.com> - 1.0.0-1
- Initial release
Build the RPM package:
# Create the tarball
tar -czf myapp-1.0.0.tar.gz -C bin/Release/net8.0/linux-x64/publish .
# Build the RPM
rpmbuild -ba myapp.spec
Creating AppImages
AppImage is a distribution-agnostic package format that allows applications to run on many Linux distributions without installation. It's a single file that contains the application and all its dependencies.
# Install the AppImageKit tools
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
# Create the AppDir structure
mkdir -p MyApp.AppDir/usr/bin
mkdir -p MyApp.AppDir/usr/share/applications
mkdir -p MyApp.AppDir/usr/share/icons/hicolor/256x256/apps
# Copy the application files
cp -r bin/Release/net8.0/linux-x64/publish/* MyApp.AppDir/usr/bin/
# Create the AppRun script
cat > MyApp.AppDir/AppRun << EOF
#!/bin/bash
SELF=\$(readlink -f "\$0")
HERE=\${SELF%/*}
export PATH="\${HERE}/usr/bin:\${PATH}"
export LD_LIBRARY_PATH="\${HERE}/usr/lib:\${LD_LIBRARY_PATH}"
exec "\${HERE}/usr/bin/MyApp" "\$@"
EOF
chmod +x MyApp.AppDir/AppRun
# Create a desktop entry
cat > MyApp.AppDir/myapp.desktop << EOF
[Desktop Entry]
Name=MyAvalonia App
Comment=A cross-platform application built with Avalonia UI
Exec=MyApp
Icon=myapp
Terminal=false
Type=Application
Categories=Utility;
EOF
# Copy the icon
cp AppIcon.png MyApp.AppDir/myapp.png
# Create the AppImage
./appimagetool-x86_64.AppImage MyApp.AppDir MyAvaloniaApp-x86_64.AppImage
Creating Flatpak Packages
Flatpak is a system for building, distributing, and running sandboxed desktop applications on Linux.
# Create a Flatpak manifest
cat > com.mycompany.MyApp.yml << EOF
app-id: com.mycompany.MyApp
runtime: org.freedesktop.Platform
runtime-version: '22.08'
sdk: org.freedesktop.Sdk
command: MyApp
finish-args:
- --share=ipc
- --socket=x11
- --socket=wayland
- --filesystem=host
- --device=dri
modules:
- name: myapp
buildsystem: simple
build-commands:
- install -D MyApp /app/bin/MyApp
- install -D myapp.desktop /app/share/applications/com.mycompany.MyApp.desktop
- install -D AppIcon.png /app/share/icons/hicolor/256x256/apps/com.mycompany.MyApp.png
sources:
- type: dir
path: bin/Release/net8.0/linux-x64/publish
- type: file
path: myapp.desktop
- type: file
path: AppIcon.png
EOF
# Build the Flatpak package
flatpak-builder --repo=repo build-dir com.mycompany.MyApp.yml
# Create a Flatpak bundle
flatpak build-bundle repo MyAvaloniaApp.flatpak com.mycompany.MyApp
Creating Snap Packages
Snap is a software packaging and deployment system developed by Canonical for Linux operating systems.
# Create a snapcraft.yaml file
cat > snapcraft.yaml << EOF
name: myavaloniaapp
version: '1.0.0'
summary: MyAvalonia App
description: |
A cross-platform application built with Avalonia UI.
This application provides useful functionality for users.
base: core22
confinement: strict
grade: stable
apps:
myavaloniaapp:
command: MyApp
extensions: [gnome]
plugs:
- home
- network
- opengl
- wayland
- x11
parts:
myavaloniaapp:
plugin: dump
source: bin/Release/net8.0/linux-x64/publish
organize:
'*': bin/
stage-packages:
- libgtk-3-0
- libicu70
EOF
# Build the snap package
snapcraft
Choosing a Linux Package Format
Format | Pros | Cons | Best For |
---|---|---|---|
DEB | Native on Debian/Ubuntu, widely supported | Distribution-specific | Applications targeting Debian-based systems |
RPM | Native on Fedora/RHEL, good for enterprise | Distribution-specific | Applications targeting Red Hat-based systems |
AppImage | No installation needed, works on many distros | No automatic updates | Simple applications with broad compatibility |
Flatpak | Sandboxed, works on many distros, has updates | Larger size | Applications needing isolation and broad compatibility |
Snap | Easy to build, automatic updates | Controlled by Canonical | Applications targeting Ubuntu users primarily |
Mobile Deployment
Android Deployment
Deploying Avalonia UI applications to Android involves building an Android APK (Android Package) file that can be installed on Android devices or published to the Google Play Store.
Prerequisites
Before deploying to Android, ensure you have:
- Android SDK installed (can be installed via Android Studio)
- Java Development Kit (JDK) version 11 or later
- Android workload for .NET:
dotnet workload install android
Building for Android
The Android build process involves several steps:
- Build the Android project:
# Build the Android project in Debug configuration
dotnet build -c Debug MyApp.Android
# Build the Android project in Release configuration
dotnet build -c Release MyApp.Android
- Create an APK:
# Create a debug APK (for testing)
dotnet publish -c Debug -f net8.0-android MyApp.Android
# Create a release APK (for distribution)
dotnet publish -c Release -f net8.0-android MyApp.Android
The resulting APK will be located in:
MyApp.Android/bin/Release/net8.0-android/publish/
Android Manifest Configuration
The Android manifest (AndroidManifest.xml
) defines essential information about your app for the Android system. You can customize it in your project:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:installLocation="auto">
<!-- Application attributes -->
<application
android:label="MyAvalonia App"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="true">
<!-- Main activity -->
<activity
android:name="com.mycompany.myapp.MainActivity"
android:theme="@style/MyAppTheme"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:launchMode="singleInstance"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- File associations -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:mimeType="application/x-myapp" />
<data android:pathPattern=".*\\.myapp" />
</intent-filter>
</activity>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- Features -->
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<!-- SDK requirements -->
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
</manifest>
Signing the APK for Release
Before publishing to the Google Play Store, you need to sign your APK with a release key:
- Create a keystore (if you don't already have one):
# Generate a new keystore
keytool -genkey -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000
- Sign the APK:
# Sign the APK with your keystore
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 -keystore my-release-key.keystore \
MyApp.Android/bin/Release/net8.0-android/publish/com.mycompany.myapp-Signed.apk my-key-alias
- Verify the signature:
# Verify the signed APK
jarsigner -verify -verbose -certs MyApp.Android/bin/Release/net8.0-android/publish/com.mycompany.myapp-Signed.apk
- Optimize the APK (optional but recommended):
# Optimize the APK using zipalign
zipalign -v 4 MyApp.Android/bin/Release/net8.0-android/publish/com.mycompany.myapp-Signed.apk \
MyApp.Android/bin/Release/net8.0-android/publish/com.mycompany.myapp-Signed-Aligned.apk
Android App Bundle (AAB)
Google Play Store prefers Android App Bundles (AAB) over APKs. You can create an AAB using:
# Create an Android App Bundle
dotnet publish -c Release -f net8.0-android MyApp.Android /p:AndroidPackageFormat=aab
Automating Android Deployment
You can automate the Android deployment process using a script:
#!/bin/bash
# Android deployment script for Avalonia UI application
# Configuration
APP_NAME="MyAvalonia App"
PACKAGE_NAME="com.mycompany.myapp"
VERSION="1.0.0"
VERSION_CODE=1
KEYSTORE_PATH="my-release-key.keystore"
KEYSTORE_ALIAS="my-key-alias"
KEYSTORE_PASSWORD="your-keystore-password"
# Build the app
echo "Building Android application..."
dotnet publish -c Release -f net8.0-android \
/p:AndroidPackageFormat=aab \
/p:ApplicationId="$PACKAGE_NAME" \
/p:ApplicationVersion="$VERSION" \
/p:ApplicationDisplayVersion="$VERSION" \
/p:AndroidVersionCode=$VERSION_CODE \
MyApp.Android
# Sign the AAB
echo "Signing Android App Bundle..."
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \
-keystore "$KEYSTORE_PATH" \
-storepass "$KEYSTORE_PASSWORD" \
MyApp.Android/bin/Release/net8.0-android/publish/"$PACKAGE_NAME".aab \
"$KEYSTORE_ALIAS"
echo "Android deployment complete!"
Testing on Android Devices
To test your app on an Android device:
- Enable Developer Options on your Android device
- Enable USB Debugging in Developer Options
- Connect your device via USB
- Install the app:
# Install the app on a connected device
adb install MyApp.Android/bin/Release/net8.0-android/publish/com.mycompany.myapp-Signed.apk
Publishing to Google Play Store
To publish your app to the Google Play Store:
- Create a Google Play Developer account (one-time $25 fee)
- Create a new app in the Google Play Console
- Upload your signed AAB or APK
- Fill in the store listing (app details, screenshots, etc.)
- Set up pricing and distribution
- Submit for review
Google Play has specific requirements for apps, including:
- Privacy policy
- Content rating
- App icon requirements
- Screenshot requirements
- Target API level requirements
Make sure your app complies with all Google Play policies before submission.
iOS Deployment
Deploying Avalonia UI applications to iOS involves building an iOS app package that can be installed on iOS devices or published to the Apple App Store. iOS deployment requires a Mac computer and an Apple Developer account.
Prerequisites
Before deploying to iOS, ensure you have:
- macOS operating system (required for iOS development)
- Xcode installed (latest version recommended)
- Apple Developer Account (paid subscription required for App Store distribution)
- iOS workload for .NET:
dotnet workload install ios
Building for iOS
The iOS build process involves several steps:
- Build the iOS project:
# Build the iOS project in Debug configuration
dotnet build -c Debug MyApp.iOS
# Build the iOS project in Release configuration
dotnet build -c Release MyApp.iOS
- Create an app package:
# Create a debug app package (for testing)
dotnet publish -c Debug -f net8.0-ios MyApp.iOS
# Create a release app package (for distribution)
dotnet publish -c Release -f net8.0-ios MyApp.iOS
The resulting app package will be located in:
MyApp.iOS/bin/Release/net8.0-ios/publish/
iOS Info.plist Configuration
The Info.plist
file defines essential information about your app for iOS. You can customize it in your project:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Basic application information -->
<key>CFBundleIdentifier</key>
<string>com.mycompany.myapp</string>
<key>CFBundleName</key>
<string>MyAvalonia App</string>
<key>CFBundleDisplayName</key>
<string>MyAvalonia App</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- Application icon -->
<key>CFBundleIconName</key>
<string>AppIcon</string>
<!-- Device orientation support -->
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<!-- Minimum iOS version -->
<key>MinimumOSVersion</key>
<string>13.0</string>
<!-- Device capabilities -->
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<!-- App appearance -->
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<!-- Privacy descriptions (required for certain features) -->
<key>NSCameraUsageDescription</key>
<string>This app needs access to the camera to take photos.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to photos to select images.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app needs access to microphone for voice recording.</string>
</dict>
</plist>
Signing and Provisioning
iOS apps must be signed with a valid certificate and provisioning profile:
- Create an App ID in the Apple Developer Portal
- Create a Development Certificate for testing on devices
- Create a Distribution Certificate for App Store submission
- Create Provisioning Profiles for development and distribution
You can configure signing in your project file:
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<CodesignKey>iPhone Distribution: Your Company Name (TEAM_ID)</CodesignKey>
<CodesignProvision>Your Provisioning Profile Name</CodesignProvision>
</PropertyGroup>
Building with Xcode
While you can build using the .NET CLI, using Xcode provides more control:
- Generate an Xcode project:
dotnet build -c Release MyApp.iOS -p:BuildIpa=false -p:_DotNetRootRemoteDirectory=/Users/username/Library/Caches/Xamarin/XMA/SDKs/dotnet/
- Open the generated Xcode project:
open MyApp.iOS/obj/Release/net8.0-ios/ios-arm64/MyApp.iOS.xcodeproj
-
Configure signing in Xcode:
- Select your project in the Project Navigator
- Go to the "Signing & Capabilities" tab
- Select your team and provisioning profile
-
Build and archive in Xcode:
- Select "Product" > "Archive" from the menu
- Once archived, click "Distribute App"
- Choose the appropriate distribution method
Creating an IPA File
An IPA (iOS App Store Package) file is required for distribution:
# Create an IPA file
dotnet publish -c Release -f net8.0-ios MyApp.iOS -p:BuildIpa=true
The IPA file will be located in:
MyApp.iOS/bin/Release/net8.0-ios/ios-arm64/publish/
Testing on iOS Devices
To test your app on an iOS device:
- Register your device in the Apple Developer Portal
- Include your device in the development provisioning profile
- Connect your device to your Mac
- Install the app using Xcode or the .NET CLI:
# Install using .NET CLI
dotnet build -c Debug MyApp.iOS -t:Run
Publishing to the App Store
To publish your app to the Apple App Store:
- Create an App Record in App Store Connect
- Configure app metadata (description, screenshots, etc.)
- Upload your build using Xcode or Application Loader
- Submit for review
Apple has specific requirements for apps, including:
- Privacy policy
- App Store screenshots in various sizes
- App icon requirements
- Content rating information
- Privacy declarations (App Privacy section)
Automating iOS Deployment
You can automate parts of the iOS deployment process using a script:
#!/bin/bash
# iOS deployment script for Avalonia UI application
# Configuration
APP_NAME="MyAvalonia App"
BUNDLE_ID="com.mycompany.myapp"
VERSION="1.0.0"
BUILD_NUMBER="1"
TEAM_ID="YOUR_TEAM_ID"
PROVISIONING_PROFILE="Your Provisioning Profile Name"
# Build the app
echo "Building iOS application..."
dotnet publish -c Release -f net8.0-ios \
/p:BuildIpa=true \
/p:CodesignKey="iPhone Distribution: Your Company Name ($TEAM_ID)" \
/p:CodesignProvision="$PROVISIONING_PROFILE" \
/p:CFBundleIdentifier="$BUNDLE_ID" \
/p:CFBundleShortVersionString="$VERSION" \
/p:CFBundleVersion="$BUILD_NUMBER" \
MyApp.iOS
echo "iOS deployment complete!"
TestFlight Distribution
TestFlight allows you to distribute beta versions of your app to testers:
- Upload your build to App Store Connect
- Configure TestFlight settings
- Add internal or external testers
- Release the build to testers
External testers require app review by Apple, while internal testers (members of your team) can access builds immediately.
Ad Hoc Distribution
For distributing to specific devices without using the App Store:
# Create an Ad Hoc IPA
dotnet publish -c Release -f net8.0-ios MyApp.iOS \
-p:BuildIpa=true \
-p:CodesignKey="iPhone Distribution: Your Company Name (TEAM_ID)" \
-p:CodesignProvision="Your Ad Hoc Provisioning Profile"
Ad Hoc distribution requires:
- Registering all target devices in the Apple Developer Portal
- Including all device UDIDs in the Ad Hoc provisioning profile
- Distributing the IPA file to users (via email, download link, or MDM)
WebAssembly Deployment
WebAssembly (WASM) allows Avalonia UI applications to run directly in web browsers without plugins. This enables you to deploy your application as a web app while maintaining most of the same codebase used for desktop and mobile platforms.
Prerequisites
Before deploying to WebAssembly, ensure you have:
-
WebAssembly workload for .NET:
dotnet workload install wasm-tools
-
A web server or hosting service for deployment
Building for WebAssembly
The WebAssembly build process involves these steps:
- Create a Browser project in your solution:
# Add a Browser project to your solution
dotnet new avalonia.template -n MyApp.Browser --type browser
- Build the WebAssembly project:
# Build in Debug configuration
dotnet build -c Debug MyApp.Browser
# Build in Release configuration
dotnet build -c Release MyApp.Browser
- Publish the WebAssembly project:
# Create a production-ready build
dotnet publish -c Release MyApp.Browser
The published files will be located in:
MyApp.Browser/bin/Release/net8.0/browser-wasm/publish/wwwroot
WebAssembly Project Structure
A typical WebAssembly project for Avalonia UI has this structure:
MyApp.Browser/
├── Program.cs # Entry point for the browser app
├── app.manifest # Web app manifest
├── AppBundle/ # Static assets
│ ├── favicon.ico # Website favicon
│ ├── index.html # Main HTML page
│ └── main.js # JavaScript interop
├── Properties/
│ └── launchSettings.json # Development server settings
└── MyApp.Browser.csproj # Project file
Customizing the HTML Template
You can customize the HTML template that hosts your WebAssembly application:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>MyAvalonia App</title>
<base href="/" />
<!-- Application metadata -->
<meta name="description" content="A cross-platform application built with Avalonia UI" />
<meta name="theme-color" content="#2196F3" />
<!-- Favicon and app icons -->
<link rel="icon" type="image/png" href="favicon.png" />
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
<!-- Web app manifest for PWA support -->
<link rel="manifest" href="manifest.json" />
<!-- Custom styles -->
<link href="css/app.css" rel="stylesheet" />
<!-- Loading indicator styles -->
<style>
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
flex-direction: column;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<!-- Loading indicator shown while WASM loads -->
<div id="app-loading" class="loading-container">
<div class="loading-spinner"></div>
<p>Loading application...</p>
</div>
<!-- Canvas where Avalonia UI will render -->
<div id="app" style="position: absolute; width: 100%; height: 100%;"></div>
<!-- Error message for browsers without WebAssembly support -->
<div id="blazor-error-ui" style="display: none;">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<!-- Main WebAssembly script -->
<script src="_framework/blazor.webassembly.js"></script>
<!-- Custom JavaScript -->
<script src="js/app.js"></script>
<!-- Hide loading indicator when app is loaded -->
<script>
window.addEventListener('avalonia-loaded', function() {
document.getElementById('app-loading').style.display = 'none';
});
</script>
</body>
</html>
Web App Manifest
For Progressive Web App (PWA) support, create a manifest.json
file:
{
"name": "MyAvalonia App",
"short_name": "MyApp",
"start_url": "./",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2196F3",
"icons": [
{
"src": "icon-192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "icon-512.png",
"type": "image/png",
"sizes": "512x512"
}
]
}
JavaScript Interop
You can interact with JavaScript from your C# code:
/// <summary>
/// Provides methods for interacting with the browser's JavaScript environment.
/// </summary>
public static class JsInterop
{
/// <summary>
/// Shows a browser alert dialog with the specified message.
/// </summary>
/// <param name="message">The message to display in the alert.</param>
public static void ShowAlert(string message)
{
// Call JavaScript alert function
_ = Runtime.InvokeJS($"alert('{message.Replace("'", "\\'")}')");
}
/// <summary>
/// Gets the current URL from the browser.
/// </summary>
/// <returns>The current URL as a string.</returns>
public static string GetCurrentUrl()
{
// Get the current URL from JavaScript
return Runtime.InvokeJS("window.location.href");
}
/// <summary>
/// Stores data in the browser's local storage.
/// </summary>
/// <param name="key">The key to store the data under.</param>
/// <param name="value">The value to store.</param>
public static void StoreLocalData(string key, string value)
{
// Store data in localStorage
_ = Runtime.InvokeJS($"localStorage.setItem('{key}', '{value.Replace("'", "\\'")}')");
}
/// <summary>
/// Retrieves data from the browser's local storage.
/// </summary>
/// <param name="key">The key to retrieve data for.</param>
/// <returns>The stored value, or null if not found.</returns>
public static string GetLocalData(string key)
{
// Get data from localStorage
return Runtime.InvokeJS($"localStorage.getItem('{key}')");
}
}
Optimizing WebAssembly Performance
WebAssembly applications can be optimized for better performance:
- Enable AOT compilation:
<!-- In your .csproj file -->
<PropertyGroup>
<WasmEnableThreads>true</WasmEnableThreads>
<RunAOTCompilation>true</RunAOTCompilation>
<WasmOptimization>true</WasmOptimization>
</PropertyGroup>
- Reduce initial load size:
<!-- In your .csproj file -->
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>link</TrimMode>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
- Implement lazy loading for non-critical components:
// Load components only when needed
private async Task LoadResourcesAsync()
{
// Simulate lazy loading
await Task.Delay(100);
// Load resources
var resource = await ResourceLoader.LoadAsync("large-resource.json");
// Update UI
this.Resources = resource;
}
Deployment Options
There are several options for deploying WebAssembly applications:
1. Static Web Hosting
Deploy to any static web hosting service:
# Copy the published files to your web server
cp -r MyApp.Browser/bin/Release/net8.0/browser-wasm/publish/wwwroot/* /var/www/html/
2. Azure Static Web Apps
# Install the Azure CLI
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
# Login to Azure
az login
# Create a static web app
az staticwebapp create \
--name MyAvaloniaApp \
--resource-group MyResourceGroup \
--source MyApp.Browser/bin/Release/net8.0/browser-wasm/publish/wwwroot \
--location "West US 2" \
--sku Free
3. GitHub Pages
# Create a GitHub Pages deployment script
cat > deploy-gh-pages.sh << EOF
#!/bin/bash
# Build the WebAssembly app
dotnet publish -c Release MyApp.Browser
# Create a temporary directory for GitHub Pages
mkdir -p gh-pages
cp -r MyApp.Browser/bin/Release/net8.0/browser-wasm/publish/wwwroot/* gh-pages/
# Create a .nojekyll file to prevent Jekyll processing
touch gh-pages/.nojekyll
# Initialize Git repository if it doesn't exist
cd gh-pages
git init
git add .
git commit -m "Deploy to GitHub Pages"
# Push to GitHub Pages
git remote add origin https://github.com/yourusername/yourrepository.git
git push -f origin master:gh-pages
cd ..
rm -rf gh-pages
EOF
chmod +x deploy-gh-pages.sh
./deploy-gh-pages.sh
4. Netlify
Create a netlify.toml
file:
[build]
publish = "MyApp.Browser/bin/Release/net8.0/browser-wasm/publish/wwwroot"
command = "dotnet publish -c Release MyApp.Browser"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
5. Docker Container
Create a Dockerfile
:
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release MyApp.Browser
FROM nginx:alpine
COPY --from=build /src/MyApp.Browser/bin/Release/net8.0/browser-wasm/publish/wwwroot /usr/share/nginx/html
EXPOSE 80
Build and run the Docker container:
docker build -t myavaloniaapp .
docker run -p 8080:80 myavaloniaapp
WebAssembly Limitations
When deploying to WebAssembly, be aware of these limitations:
- Browser API Restrictions: WebAssembly runs in the browser sandbox with limited access to system resources
- File System Access: Limited compared to desktop applications
- Performance: While fast, WebAssembly is typically slower than native code
- Initial Load Time: WebAssembly applications may have longer initial load times
- Browser Compatibility: Older browsers may not support WebAssembly
Progressive Web App (PWA) Features
You can enhance your WebAssembly application with PWA features:
- Add a service worker for offline support:
// service-worker.js
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('myapp-cache-v1').then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/css/app.css',
'/js/app.js',
'/favicon.ico',
'/_framework/blazor.webassembly.js',
// Add other assets to cache
]);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
- Register the service worker in your HTML:
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then((registration) => {
console.log('Service Worker registered with scope:', registration.scope);
})
.catch((error) => {
console.error('Service Worker registration failed:', error);
});
}
</script>
Cross-Origin Resource Sharing (CORS)
If your WebAssembly application needs to access resources from different domains, you'll need to configure CORS:
// In your server-side code
app.UseCors(policy => policy
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
Or configure your web server to send the appropriate CORS headers.
Performance Optimization
Optimizing performance is crucial for cross-platform applications, especially on mobile and web platforms.
General Optimization Techniques
1. Minimize UI Updates
Avoid unnecessary UI updates:
// Batch multiple property changes
this.BeginBatchUpdate();
try
{
this.Property1 = value1;
this.Property2 = value2;
this.Property3 = value3;
}
finally
{
this.EndBatchUpdate();
}
2. Use Virtualization for Large Collections
Use virtualization to render only visible items:
<ScrollViewer>
<ItemsRepeater Items="{Binding LargeCollection}">
<ItemsRepeater.Layout>
<StackLayout />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
3. Optimize Images
Optimize images for each platform:
public string GetOptimizedImagePath(string imageName)
{
if (OperatingSystem.IsAndroid() || OperatingSystem.IsIOS())
{
// Use lower resolution images for mobile
return $"avares://MyApp/Assets/Images/Mobile/{imageName}";
}
else
{
// Use higher resolution images for desktop
return $"avares://MyApp/Assets/Images/Desktop/{imageName}";
}
}
4. Lazy Loading
Implement lazy loading for expensive resources:
private Lazy<BitmapImage> _lazyImage;
public BitmapImage Image => _lazyImage.Value;
public MyViewModel()
{
_lazyImage = new Lazy<BitmapImage>(() => new BitmapImage(new Uri("avares://MyApp/Assets/large-image.png")));
}
5. Reduce Binding Complexity
Simplify binding expressions and avoid complex paths:
<!-- Instead of this -->
<TextBlock Text="{Binding Items[0].SubItems[0].Name}" />
<!-- Expose a simpler property -->
<TextBlock Text="{Binding FirstItemName}" />
public string FirstItemName => Items.FirstOrDefault()?.SubItems.FirstOrDefault()?.Name;
Platform-Specific Optimizations
Desktop Optimizations
For desktop platforms:
- Use hardware acceleration:
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.With(new SkiaOptions { MaxGpuResourceSizeBytes = 8096000 })
.With(new Win32PlatformOptions { AllowEglInitialization = true })
.With(new X11PlatformOptions { UseGpu = true })
.With(new AvaloniaNativePlatformOptions { UseGpu = true })
.LogToTrace();
- Optimize for high DPI displays:
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.With(new Win32PlatformOptions { EnableMultitouch = true, HighDpiMode = HighDpiMode.PerMonitorV2 })
.LogToTrace();
Mobile Optimizations
For mobile platforms:
- Reduce layout complexity:
<!-- Instead of deep nesting -->
<Grid>
<StackPanel>
<Border>
<StackPanel>
<TextBlock Text="Deeply nested" />
</StackPanel>
</Border>
</StackPanel>
</Grid>
<!-- Use simpler layouts -->
<StackPanel>
<TextBlock Text="Simpler layout" Margin="10" />
</StackPanel>
- Optimize touch interactions:
<Button Content="Touch-Friendly Button" Padding="20,15" />
- Reduce memory usage:
// Unload resources when navigating away
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Clear large collections
LargeCollection.Clear();
// Dispose expensive resources
_expensiveResource?.Dispose();
_expensiveResource = null;
// Force garbage collection in extreme cases
GC.Collect();
}
WebAssembly Optimizations
For WebAssembly:
- Minimize initial load size:
// Use trimming to reduce app size
// In .csproj
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>link</TrimMode>
<TrimmerDefaultAction>link</TrimmerDefaultAction>
</PropertyGroup>
- Use asynchronous loading:
public async Task InitializeAsync()
{
// Load data asynchronously
var data = await LoadDataAsync();
// Update UI after data is loaded
this.Data = data;
}
Testing Across Platforms
Testing cross-platform applications requires a systematic approach to ensure consistent behavior across all target platforms.
Unit Testing
Unit tests should be platform-independent and focus on testing the core logic:
[Fact]
public void ViewModel_WhenPropertyChanged_ShouldUpdateDependentProperty()
{
// Arrange
var viewModel = new MyViewModel();
viewModel.Property1 = "Initial";
// Act
viewModel.Property1 = "Updated";
// Assert
Assert.Equal("Updated", viewModel.Property1);
Assert.Equal("UPDATED", viewModel.DependentProperty);
}
UI Testing
For UI testing, you can use Avalonia's UI testing framework:
[Fact]
public async Task Button_WhenClicked_ShouldUpdateCounter()
{
// Arrange
var window = new CounterWindow();
var button = window.FindControl<Button>("incrementButton");
var counterText = window.FindControl<TextBlock>("counterText");
// Act
button.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
await Task.Delay(50); // Allow UI to update
// Assert
Assert.Equal("Counter: 1", counterText.Text);
}
Platform-Specific Testing
For platform-specific features, you can use conditional compilation:
[Fact]
public void FilePickerService_ShouldReturnCorrectPath()
{
#if WINDOWS
// Test Windows-specific behavior
var service = new WindowsFilePickerService();
// ...
#elif ANDROID
// Test Android-specific behavior
var service = new AndroidFilePickerService();
// ...
#elif IOS
// Test iOS-specific behavior
var service = new IOSFilePickerService();
// ...
#else
// Skip test on unsupported platforms
Skip.If(true, "Test not supported on this platform");
#endif
}
Automated UI Testing
For automated UI testing across platforms, you can use tools like Appium:
[Fact]
public void AppiumTest_LoginScreen_ShouldLogin()
{
// Initialize Appium driver for the target platform
AppiumDriver driver = InitializeAppiumDriver();
try
{
// Find elements
var usernameField = driver.FindElementById("usernameTextBox");
var passwordField = driver.FindElementById("passwordTextBox");
var loginButton = driver.FindElementById("loginButton");
// Interact with elements
usernameField.SendKeys("testuser");
passwordField.SendKeys("password");
loginButton.Click();
// Verify result
var welcomeMessage = driver.FindElementById("welcomeMessage");
Assert.Equal("Welcome, testuser!", welcomeMessage.Text);
}
finally
{
driver.Quit();
}
}
Testing Matrix
Create a testing matrix to ensure coverage across platforms:
Test Case | Windows | macOS | Linux | Android | iOS | WebAssembly |
---|---|---|---|---|---|---|
Core Logic | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
UI Layout | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
File Operations | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Platform Services | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Performance | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Example: Cross-Platform Note-Taking App
Let's put everything together in a simple cross-platform note-taking app:
Core Project Structure
MyNotes/
├── MyNotes/
│ ├── App.axaml
│ ├── App.axaml.cs
│ ├── Assets/
│ │ ├── logo.png
│ ├── Models/
│ │ ├── Note.cs
│ ├── Services/
│ │ ├── IFileService.cs
│ │ ├── IDialogService.cs
│ ├── ViewModels/
│ │ ├── MainViewModel.cs
│ │ ├── NoteViewModel.cs
│ ├── Views/
│ │ ├── MainView.axaml
│ │ ├── MainView.axaml.cs
│ │ ├── NoteView.axaml
│ │ ├── NoteView.axaml.cs
│ └── Program.cs
Model
// Models/Note.cs
public class Note
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Title { get; set; }
public string Content { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.Now;
public DateTime UpdatedAt { get; set; } = DateTime.Now;
}
Services
// Services/IFileService.cs
public interface IFileService
{
Task<string> ReadTextAsync(string filePath);
Task WriteTextAsync(string filePath, string content);
Task<string[]> GetFilesAsync(string directoryPath);
Task<string> PickFileAsync();
Task<string> SaveFileAsync(string defaultFileName);
string GetNotesDirectory();
}
// Services/IDialogService.cs
public interface IDialogService
{
Task<string> ShowInputDialogAsync(string title, string message, string defaultValue = "");
Task<bool> ShowConfirmDialogAsync(string title, string message);
Task ShowAlertAsync(string title, string message);
}
Platform-Specific Implementations
// Desktop implementation
public class DesktopFileService : IFileService
{
public async Task<string> ReadTextAsync(string filePath)
{
return await File.ReadAllTextAsync(filePath);
}
public async Task WriteTextAsync(string filePath, string content)
{
await File.WriteAllTextAsync(filePath, content);
}
public async Task<string[]> GetFilesAsync(string directoryPath)
{
return Directory.GetFiles(directoryPath, "*.note");
}
public async Task<string> PickFileAsync()
{
var dialog = new OpenFileDialog
{
Title = "Open Note",
Filters = new List<FileDialogFilter>
{
new FileDialogFilter { Name = "Note Files", Extensions = new List<string> { "note" } }
}
};
var result = await dialog.ShowAsync(App.MainWindow);
return result?.FirstOrDefault();
}
public async Task<string> SaveFileAsync(string defaultFileName)
{
var dialog = new SaveFileDialog
{
Title = "Save Note",
InitialFileName = defaultFileName,
Filters = new List<FileDialogFilter>
{
new FileDialogFilter { Name = "Note Files", Extensions = new List<string> { "note" } }
}
};
return await dialog.ShowAsync(App.MainWindow);
}
public string GetNotesDirectory()
{
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var notesPath = Path.Combine(appDataPath, "MyNotes");
if (!Directory.Exists(notesPath))
{
Directory.CreateDirectory(notesPath);
}
return notesPath;
}
}
// Android implementation
public class AndroidFileService : IFileService
{
private readonly Context _context;
public AndroidFileService(Context context)
{
_context = context;
}
public async Task<string> ReadTextAsync(string filePath)
{
using var stream = _context.Assets.Open(filePath);
using var reader = new StreamReader(stream);
return await reader.ReadToEndAsync();
}
// Implement other methods...
}
ViewModels
// ViewModels/MainViewModel.cs
public class MainViewModel : ViewModelBase
{
private readonly IFileService _fileService;
private readonly IDialogService _dialogService;
private ObservableCollection<NoteViewModel> _notes;
private NoteViewModel _selectedNote;
public MainViewModel(IFileService fileService, IDialogService dialogService)
{
_fileService = fileService;
_dialogService = dialogService;
_notes = new ObservableCollection<NoteViewModel>();
CreateNoteCommand = ReactiveCommand.CreateFromTask(ExecuteCreateNote);
DeleteNoteCommand = ReactiveCommand.CreateFromTask(ExecuteDeleteNote,
this.WhenAnyValue(x => x.SelectedNote, note => note != null));
OpenNoteCommand = ReactiveCommand.CreateFromTask(ExecuteOpenNote);
LoadNotes();
}
public ObservableCollection<NoteViewModel> Notes
{
get => _notes;
set => this.RaiseAndSetIfChanged(ref _notes, value);
}
public NoteViewModel SelectedNote
{
get => _selectedNote;
set => this.RaiseAndSetIfChanged(ref _selectedNote, value);
}
public ReactiveCommand<Unit, Unit> CreateNoteCommand { get; }
public ReactiveCommand<Unit, Unit> DeleteNoteCommand { get; }
public ReactiveCommand<Unit, Unit> OpenNoteCommand { get; }
private async void LoadNotes()
{
try
{
var notesDirectory = _fileService.GetNotesDirectory();
var noteFiles = await _fileService.GetFilesAsync(notesDirectory);
Notes.Clear();
foreach (var noteFile in noteFiles)
{
var content = await _fileService.ReadTextAsync(noteFile);
var note = JsonSerializer.Deserialize<Note>(content);
if (note != null)
{
Notes.Add(new NoteViewModel(note, _fileService, _dialogService));
}
}
}
catch (Exception ex)
{
await _dialogService.ShowAlertAsync("Error", $"Failed to load notes: {ex.Message}");
}
}
private async Task ExecuteCreateNote()
{
var title = await _dialogService.ShowInputDialogAsync("New Note", "Enter note title:");
if (string.IsNullOrEmpty(title))
return;
var note = new Note
{
Title = title,
Content = ""
};
var noteViewModel = new NoteViewModel(note, _fileService, _dialogService);
Notes.Add(noteViewModel);
SelectedNote = noteViewModel;
await noteViewModel.SaveAsync();
}
private async Task ExecuteDeleteNote()
{
if (SelectedNote == null)
return;
var confirm = await _dialogService.ShowConfirmDialogAsync("Delete Note",
$"Are you sure you want to delete '{SelectedNote.Title}'?");
if (confirm)
{
await SelectedNote.DeleteAsync();
Notes.Remove(SelectedNote);
SelectedNote = null;
}
}
private async Task ExecuteOpenNote()
{
var filePath = await _fileService.PickFileAsync();
if (string.IsNullOrEmpty(filePath))
return;
try
{
var content = await _fileService.ReadTextAsync(filePath);
var note = JsonSerializer.Deserialize<Note>(content);
if (note != null)
{
var existingNote = Notes.FirstOrDefault(n => n.Id == note.Id);
if (existingNote != null)
{
SelectedNote = existingNote;
}
else
{
var noteViewModel = new NoteViewModel(note, _fileService, _dialogService);
Notes.Add(noteViewModel);
SelectedNote = noteViewModel;
}
}
}
catch (Exception ex)
{
await _dialogService.ShowAlertAsync("Error", $"Failed to open note: {ex.Message}");
}
}
}
// ViewModels/NoteViewModel.cs
public class NoteViewModel : ViewModelBase
{
private readonly Note _note;
private readonly IFileService _fileService;
private readonly IDialogService _dialogService;
public NoteViewModel(Note note, IFileService fileService, IDialogService dialogService)
{
_note = note;
_fileService = fileService;
_dialogService = dialogService;
SaveCommand = ReactiveCommand.CreateFromTask(SaveAsync);
}
public string Id => _note.Id;
public string Title
{
get => _note.Title;
set
{
if (_note.Title != value)
{
_note.Title = value;
_note.UpdatedAt = DateTime.Now;
this.RaisePropertyChanged();
}
}
}
public string Content
{
get => _note.Content;
set
{
if (_note.Content != value)
{
_note.Content = value;
_note.UpdatedAt = DateTime.Now;
this.RaisePropertyChanged();
}
}
}
public DateTime CreatedAt => _note.CreatedAt;
public DateTime UpdatedAt => _note.UpdatedAt;
public ReactiveCommand<Unit, Unit> SaveCommand { get; }
public async Task SaveAsync()
{
try
{
var notesDirectory = _fileService.GetNotesDirectory();
var filePath = Path.Combine(notesDirectory, $"{Id}.note");
var content = JsonSerializer.Serialize(_note);
await _fileService.WriteTextAsync(filePath, content);
}
catch (Exception ex)
{
await _dialogService.ShowAlertAsync("Error", $"Failed to save note: {ex.Message}");
}
}
public async Task DeleteAsync()
{
try
{
var notesDirectory = _fileService.GetNotesDirectory();
var filePath = Path.Combine(notesDirectory, $"{Id}.note");
if (File.Exists(filePath))
{
File.Delete(filePath);
}
}
catch (Exception ex)
{
await _dialogService.ShowAlertAsync("Error", $"Failed to delete note: {ex.Message}");
}
}
}
Views
<!-- Views/MainView.axaml -->
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MyNotes.ViewModels"
xmlns:views="using:MyNotes.Views"
x:Class="MyNotes.Views.MainView">
<Grid ColumnDefinitions="300,*">
<!-- Notes List -->
<Grid Grid.Column="0" RowDefinitions="Auto,*">
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="10">
<Button Content="New Note" Command="{Binding CreateNoteCommand}" Margin="0,0,5,0" />
<Button Content="Open" Command="{Binding OpenNoteCommand}" Margin="0,0,5,0" />
<Button Content="Delete" Command="{Binding DeleteNoteCommand}" />
</StackPanel>
<ListBox Grid.Row="1"
Items="{Binding Notes}"
SelectedItem="{Binding SelectedNote}"
Margin="10">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Title}" FontWeight="Bold" />
<TextBlock Text="{Binding UpdatedAt, StringFormat=\{0:g\}}" FontSize="12" Opacity="0.7" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
<!-- Note Editor -->
<views:NoteView Grid.Column="1" DataContext="{Binding SelectedNote}" />
</Grid>
</UserControl>
<!-- Views/NoteView.axaml -->
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MyNotes.Views.NoteView">
<Grid RowDefinitions="Auto,*,Auto" Margin="10">
<TextBox Grid.Row="0"
Text="{Binding Title}"
Watermark="Note Title"
FontSize="18"
FontWeight="Bold"
Margin="0,0,0,10" />
<TextBox Grid.Row="1"
Text="{Binding Content}"
Watermark="Note Content"
AcceptsReturn="True"
TextWrapping="Wrap"
VerticalAlignment="Stretch" />
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,10,0,0">
<Button Content="Save" Command="{Binding SaveCommand}" />
<TextBlock Text="{Binding UpdatedAt, StringFormat=Last updated: \{0:g\}}"
VerticalAlignment="Center"
Margin="10,0,0,0"
Opacity="0.7" />
</StackPanel>
</Grid>
</UserControl>
App Initialization
// App.axaml.cs
public partial class App : Application
{
public static Window MainWindow { get; private set; }
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
// Register services
var services = new ServiceCollection();
// Register platform-specific services
if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())
{
services.AddSingleton<IFileService, DesktopFileService>();
services.AddSingleton<IDialogService, DesktopDialogService>();
}
else if (OperatingSystem.IsAndroid())
{
services.AddSingleton<IFileService, AndroidFileService>();
services.AddSingleton<IDialogService, AndroidDialogService>();
}
else if (OperatingSystem.IsIOS())
{
services.AddSingleton<IFileService, IOSFileService>();
services.AddSingleton<IDialogService, IOSDialogService>();
}
else
{
services.AddSingleton<IFileService, WebFileService>();
services.AddSingleton<IDialogService, WebDialogService>();
}
// Register view models
services.AddSingleton<MainViewModel>();
var serviceProvider = services.BuildServiceProvider();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
MainWindow = new MainWindow
{
DataContext = serviceProvider.GetRequiredService<MainViewModel>()
};
desktop.MainWindow = MainWindow;
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
{
singleView.MainView = new MainView
{
DataContext = serviceProvider.GetRequiredService<MainViewModel>()
};
}
base.OnFrameworkInitializationCompleted();
}
}
This example demonstrates:
- A shared core project with platform-independent logic
- Platform-specific service implementations
- Dependency injection for service resolution
- Responsive UI that works across platforms
- File operations with platform-specific implementations
- MVVM pattern with ReactiveUI
Understanding Cross-Platform Development with Avalonia UI
Cross-platform development with Avalonia UI offers a powerful approach to building applications that run on multiple platforms from a single codebase. This section provides a comprehensive explanation of the key concepts, strategies, and best practices for successful cross-platform development.
Key Concepts in Cross-Platform Development
1. Shared Code Architecture
The foundation of effective cross-platform development is a well-structured architecture that maximizes code sharing while accommodating platform-specific requirements:
┌─────────────────────────────────────────────────────┐
│ Shared Core Code │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ Models │ │ ViewModels │ │ Business Logic │ │
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
└─────────────────────────────── ──────────────────────┘
│ │ │
┌─────────┴───────┐ ┌────┴────────┐ ┌────┴────────┐
│ Platform- │ │ Platform- │ │ Platform- │
│ Specific UI │ │ Specific │ │ Specific │
│ (Windows/macOS/│ │ Services │ │ Features │
│ Linux/etc.) │ │ │ │ │
└─────────────────┘ └─────────────┘ └─────────────┘
Benefits of this architecture:
- Maintainability: Changes to core logic only need to be made once
- Consistency: Business rules remain consistent across platforms
- Efficiency: Development time is reduced by avoiding duplication
2. Platform Abstraction
Platform abstraction involves creating interfaces for platform-specific functionality and implementing them separately for each platform:
/// <summary>
/// Interface defining platform-agnostic file operations.
/// This abstraction allows the core application to work with files
/// without knowing the platform-specific implementation details.
/// </summary>
public interface IFileOperations
{
/// <summary>
/// Reads a file from the file system.
/// </summary>
/// <param name="path">Path to the file</param>
/// <returns>The file contents as a string</returns>
Task<string> ReadFileAsync(string path);
/// <summary>
/// Writes content to a file.
/// </summary>
/// <param name="path">Path where the file should be written</param>
/// <param name="content">Content to write to the file</param>
/// <returns>A task representing the asynchronous operation</returns>
Task WriteFileAsync(string path, string content);
/// <summary>
/// Picks a file using the platform's native file picker.
/// </summary>
/// <returns>The selected file path or null if canceled</returns>
Task<string> PickFileAsync();
}
Each platform then provides its own implementation of this interface, handling the platform-specific details while maintaining a consistent API for the application code.
3. Dependency Injection
Dependency injection is crucial for managing platform-specific implementations:
// In your application startup
public void ConfigureServices(IServiceCollection services)
{
// Register platform-specific services
if (OperatingSystem.IsWindows())
{
services.AddSingleton<IFileOperations, WindowsFileOperations>();
services.AddSingleton<INotificationService, WindowsNotificationService>();
}
else if (OperatingSystem.IsAndroid())
{
services.AddSingleton<IFileOperations, AndroidFileOperations>();
services.AddSingleton<INotificationService, AndroidNotificationService>();
}
else if (OperatingSystem.IsIOS())
{
services.AddSingleton<IFileOperations, IOSFileOperations>();
services.AddSingleton<INotificationService, IOSNotificationService>();
}
else
{
services.AddSingleton<IFileOperations, DefaultFileOperations>();
services.AddSingleton<INotificationService, DefaultNotificationService>();
}
// Register shared services
services.AddSingleton<IDataService, DataService>();
services.AddSingleton<IAuthenticationService, AuthenticationService>();
}
Best Practices for Cross-Platform Development
1. Design for the Lowest Common Denominator First
Start by designing your application to work within the constraints of the most limited platform, then enhance for platforms with more capabilities:
public class DocumentManager
{
private readonly IFileOperations _fileOperations;
private readonly IPlatformCapabilities _platformCapabilities;
public DocumentManager(IFileOperations fileOperations, IPlatformCapabilities platformCapabilities)
{
_fileOperations = fileOperations;
_platformCapabilities = platformCapabilities;
}
public async Task SaveDocumentAsync(Document document)
{
// Basic functionality that works on all platforms
await _fileOperations.WriteFileAsync(document.Path, document.Content);
// Enhanced functionality for platforms that support it
if (_platformCapabilities.SupportsVersioning)
{
await CreateVersionHistoryAsync(document);
}
if (_platformCapabilities.SupportsCloudBackup)
{
await BackupToCloudAsync(document);
}
}
}
2. Responsive UI Design
Design your UI to adapt to different screen sizes and input methods:
<Grid>
<!-- Responsive layout using Grid and proportional sizing -->
<Grid.ColumnDefinitions>
<!-- Sidebar takes 30% on desktop, but collapses on small screens -->
<ColumnDefinition Width="{OnPlatform Default='0.3*',
Android='0*',
iOS='0*'}" />
<!-- Main content area -->
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Sidebar (visible on desktop) -->
<StackPanel Grid.Column="0" IsVisible="{OnPlatform Default=true, Android=false, iOS=false}">
<!-- Sidebar content -->
</StackPanel>
<!-- Main content -->
<Panel Grid.Column="1">
<!-- Content with platform-specific margins -->
<ContentControl Margin="{OnPlatform Default='20',
Android='10',
iOS='15'}">
<!-- Main content -->
</ContentControl>
<!-- Floating action button on mobile -->
<Button Classes="fab"
IsVisible="{OnPlatform Default=false, Android=true, iOS=true}"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
Margin="20">
<Image Source="plus.png" Width="24" Height="24" />
</Button>
</Panel>
</Grid>
3. Platform-Specific Feature Detection
Use runtime checks to detect platform capabilities:
/// <summary>
/// Provides information about platform-specific capabilities.
/// </summary>
public class PlatformCapabilities : IPlatformCapabilities
{
/// <summary>
/// Checks if the current platform supports biometric authentication.
/// </summary>
/// <returns>True if biometric authentication is supported</returns>
public bool SupportsBiometricAuth()
{
if (OperatingSystem.IsAndroid() || OperatingSystem.IsIOS())
{
// Check for hardware support on mobile platforms
return CheckBiometricHardwareAvailable();
}
else if (OperatingSystem.IsWindows())
{
// Check for Windows Hello support
return CheckWindowsHelloAvailable();
}
return false;
}
/// <summary>
/// Checks if the current platform supports offline file storage.
/// </summary>
/// <returns>True if offline storage is supported</returns>
public bool SupportsOfflineStorage()
{
// All platforms except WebAssembly support offline storage
return !OperatingSystem.IsBrowser();
}
// Implementation details...
}
4. Graceful Degradation
Implement fallbacks for when platform-specific features are unavailable:
/// <summary>
/// Provides location services with appropriate fallbacks.
/// </summary>
public class LocationService : ILocationService
{
private readonly IPlatformCapabilities _platformCapabilities;
public LocationService(IPlatformCapabilities platformCapabilities)
{
_platformCapabilities = platformCapabilities;
}
/// <summary>
/// Gets the current location using the best available method.
/// </summary>
/// <returns>The current location or null if unavailable</returns>
public async Task<GeoLocation> GetCurrentLocationAsync()
{
try
{
// Try to get precise location if available
if (_platformCapabilities.SupportsPreciseLocation)
{
var preciseLocation = await GetPreciseLocationAsync();
if (preciseLocation != null)
return preciseLocation;
}
// Fall back to approximate location
if (_platformCapabilities.SupportsApproximateLocation)
{
var approximateLocation = await GetApproximateLocationAsync();
if (approximateLocation != null)
return approximateLocation;
}
// Last resort: IP-based location (works on all platforms with internet)
return await GetIpBasedLocationAsync();
}
catch (Exception ex)
{
// Log the error
Logger.LogError($"Failed to get location: {ex.Message}");
// Return null or a default location
return null;
}
}
// Implementation details...
}
5. Testing Across Platforms
Implement a comprehensive testing strategy that covers all target platforms:
/// <summary>
/// Tests that verify cross-platform functionality.
/// </summary>
[TestClass]
public class CrossPlatformTests
{
/// <summary>
/// Tests that file operations work correctly across platforms.
/// </summary>
[TestMethod]
[TestCategory("CrossPlatform")]
public async Task FileOperations_ShouldWorkOnAllPlatforms()
{
// Arrange
var fileOperations = GetPlatformSpecificFileOperations();
var testContent = "Test content for cross-platform file operations";
var testPath = GetPlatformSpecificTestPath();
// Act
await fileOperations.WriteFileAsync(testPath, testContent);
var readContent = await fileOperations.ReadFileAsync(testPath);
// Assert
Assert.AreEqual(testContent, readContent, "File content should match across platforms");
// Cleanup
await CleanupTestFileAsync(testPath);
}
/// <summary>
/// Tests that UI rendering is consistent across platforms.
/// </summary>
[TestMethod]
[TestCategory("CrossPlatform")]
public void UIRendering_ShouldBeConsistentAcrossPlatforms()
{
// Arrange
var testView = new TestView();
var visualRoot = GetPlatformSpecificVisualRoot();
// Act
visualRoot.Content = testView;
visualRoot.Measure(new Size(1000, 1000));
visualRoot.Arrange(new Rect(0, 0, 1000, 1000));
// Assert
Assert.IsTrue(testView.IsArranged, "View should be arranged on all platforms");
Assert.IsTrue(testView.IsMeasured, "View should be measured on all platforms");
// Platform-specific assertions can be conditionally included
if (OperatingSystem.IsWindows())
{
// Windows-specific assertions
}
}
// Helper methods...
}
Common Challenges and Solutions
1. UI Consistency
Challenge: Maintaining a consistent look and feel across platforms while respecting platform conventions.
Solution: Use a design system with platform-specific adaptations:
/// <summary>
/// Provides platform-specific styling while maintaining a consistent design language.
/// </summary>
public static class ThemeManager
{
/// <summary>
/// Applies the appropriate theme for the current platform.
/// </summary>
public static void ApplyTheme(Application app)
{
// Load base styles that apply to all platforms
app.Styles.Add(new StyleInclude(new Uri("avares://MyApp/Styles/Base.axaml")));
// Apply platform-specific styles
if (OperatingSystem.IsWindows())
{
app.Styles.Add(new StyleInclude(new Uri("avares://MyApp/Styles/Windows.axaml")));
}
else if (OperatingSystem.IsMacOS())
{
app.Styles.Add(new StyleInclude(new Uri("avares://MyApp/Styles/MacOS.axaml")));
}
else if (OperatingSystem.IsLinux())
{
app.Styles.Add(new StyleInclude(new Uri("avares://MyApp/Styles/Linux.axaml")));
}
else if (OperatingSystem.IsAndroid() || OperatingSystem.IsIOS())
{
app.Styles.Add(new StyleInclude(new Uri("avares://MyApp/Styles/Mobile.axaml")));
}
else
{
app.Styles.Add(new StyleInclude(new Uri("avares://MyApp/Styles/Web.axaml")));
}
}
}
2. Performance Optimization
Challenge: Different platforms have varying performance characteristics and constraints.
Solution: Implement platform-specific optimizations:
/// <summary>
/// Manages image loading with platform-specific optimizations.
/// </summary>
public class OptimizedImageLoader : IImageLoader
{
private readonly IPlatformCapabilities _platformCapabilities;
public OptimizedImageLoader(IPlatformCapabilities platformCapabilities)
{
_platformCapabilities = platformCapabilities;
}
/// <summary>
/// Loads an image with platform-specific optimizations.
/// </summary>
/// <param name="source">Image source URI</param>
/// <returns>The loaded image</returns>
public async Task<IImage> LoadImageAsync(string source)
{
// Determine optimal image resolution based on platform
var targetSize = GetOptimalImageSize();
// Determine if we should use hardware acceleration
bool useHardwareAcceleration = ShouldUseHardwareAcceleration();
// Determine caching strategy
var cacheStrategy = GetOptimalCacheStrategy();
// Load the image with the optimized parameters
return await LoadImageWithParametersAsync(source, targetSize, useHardwareAcceleration, cacheStrategy);
}
private Size GetOptimalImageSize()
{
if (OperatingSystem.IsAndroid() || OperatingSystem.IsIOS())
{
// Mobile devices: consider screen density and memory constraints
return _platformCapabilities.IsLowMemoryDevice
? new Size(640, 640) // Lower resolution for low-memory devices
: new Size(1280, 1280); // Higher resolution for standard devices
}
else if (OperatingSystem.IsBrowser())
{
// Web: optimize for bandwidth
return new Size(1024, 1024);
}
else
{
// Desktop: can handle larger images
return new Size(1920, 1920);
}
}
private bool ShouldUseHardwareAcceleration()
{
// Hardware acceleration is beneficial on most platforms except low-end devices
return !_platformCapabilities.IsLowEndDevice;
}
// Implementation details...
}
3. Platform-Specific APIs
Challenge: Accessing platform-specific APIs while maintaining a clean architecture.
Solution: Use platform-specific compilation and dependency injection:
/// <summary>
/// Factory for creating platform-specific notification services.
/// </summary>
public static class NotificationServiceFactory
{
/// <summary>
/// Creates the appropriate notification service for the current platform.
/// </summary>
/// <returns>A platform-specific implementation of INotificationService</returns>
public static INotificationService Create()
{
#if WINDOWS
return new WindowsNotificationService();
#elif ANDROID
return new AndroidNotificationService();
#elif IOS
return new IOSNotificationService();
#elif BROWSER
return new WebNotificationService();
#else
return new FallbackNotificationService();
#endif
}
}
Conclusion
By following these patterns and best practices, you can build cross-platform applications with Avalonia UI that provide a consistent user experience across desktop, mobile, and web platforms. The key to success lies in:
- Thoughtful architecture that maximizes code sharing while accommodating platform differences
- Abstraction layers that isolate platform-specific code
- Responsive design that adapts to different screen sizes and input methods
- Graceful degradation when platform-specific features are unavailable
- Comprehensive testing across all target platforms
With Avalonia UI's cross-platform capabilities, you can deliver high-quality applications to users on virtually any device, all from a single codebase.