Platform-Specific Code in .NET MAUI with Partial Classes
Cross-platform development is brilliant until you need to do something that only exists on one platform. Accessing the Android notification system, using iOS-specific haptics, or tapping into Windows registry values — these require platform-specific code. MAUI gives you several approaches, but partial classes with multi-targeting are the cleanest.
The Problem
You want to call platform-specific APIs from your shared code. In Xamarin.Forms, you'd typically use DependencyService or conditional compilation with #if. Both approaches have drawbacks: DependencyService adds runtime overhead and indirection, while #if blocks make your code a mess of preprocessor directives.
Partial Classes to the Rescue
MAUI's single-project structure, combined with C#'s partial keyword, gives you a far better option. Define a partial class in your shared code with the contract, then provide platform-specific implementations in the Platforms folders.
Start with the shared definition:
namespace MyApp.Services;
public partial class DeviceOrientationService
{
public partial DeviceOrientation GetOrientation();
}
public enum DeviceOrientation
{
Undefined,
Landscape,
Portrait
}
Now provide the Android implementation:
using Android.Content;
using Android.Views;
using Android.Runtime;
namespace MyApp.Services;
public partial class DeviceOrientationService
{
public partial DeviceOrientation GetOrientation()
{
var context = Platform.CurrentActivity ?? Platform.AppContext;
var windowManager = context.GetSystemService(Context.WindowService)
.JavaCast<IWindowManager>();
var rotation = windowManager.DefaultDisplay.Rotation;
return rotation is SurfaceOrientation.Rotation90
or SurfaceOrientation.Rotation270
? DeviceOrientation.Landscape
: DeviceOrientation.Portrait;
}
}
And the iOS implementation:
using UIKit;
namespace MyApp.Services;
public partial class DeviceOrientationService
{
public partial DeviceOrientation GetOrientation()
{
var orientation = UIDevice.CurrentDevice.Orientation;
return orientation is UIDeviceOrientation.LandscapeLeft
or UIDeviceOrientation.LandscapeRight
? DeviceOrientation.Landscape
: DeviceOrientation.Portrait;
}
}
How Multi-Targeting Makes This Work
The key is in the .csproj file. MAUI projects target multiple frameworks simultaneously:
<TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks>
When building for Android, MSBuild includes files from Platforms/Android/ and excludes other platform folders. The compiler sees the shared partial class plus only the relevant platform implementation. No preprocessor directives needed.
The default file inclusion rules handle this automatically, but you can customise them if needed:
<!-- Default behaviour — already set by the MAUI SDK -->
<ItemGroup>
<Compile Remove="Platforms\**\*.cs" />
<Compile Include="Platforms\Android\**\*.cs"
Condition="$(TargetFramework.Contains('android'))" />
<Compile Include="Platforms\iOS\**\*.cs"
Condition="$(TargetFramework.Contains('ios'))" />
</ItemGroup>
Registering with Dependency Injection
Because the partial class is a concrete type (not hidden behind an interface you need to resolve at runtime), registration is straightforward:
builder.Services.AddSingleton<DeviceOrientationService>();
If you prefer coding to an interface for testability, add one:
public interface IDeviceOrientationService
{
DeviceOrientation GetOrientation();
}
// The partial class implements it
public partial class DeviceOrientationService : IDeviceOrientationService
{
public partial DeviceOrientation GetOrientation();
}
// Register against the interface
builder.Services.AddSingleton<IDeviceOrientationService, DeviceOrientationService>();
When to Use Each Approach
Partial classes are best when you have a service or component with platform-specific logic that needs to be cleanly separated. They compile down to a single class with no runtime cost.
Conditional compilation (#if ANDROID) is fine for small, one-off checks in shared code:
public void ConfigurePadding()
{
#if IOS
Padding = new Thickness(0, 20, 0, 0);
#elif ANDROID
Padding = new Thickness(0);
#endif
}
But once you have more than two or three lines of platform-specific code, move it to a partial class.
DeviceInfo.Platform runtime checks are useful when behaviour differs slightly but uses the same APIs:
if (DeviceInfo.Platform == DevicePlatform.iOS)
margin = 20;
else
margin = 0;
A Real-World Example: Platform Notifications
Here's a more substantial example — a notification service:
public interface INotificationService
{
Task ShowLocalNotification(string title, string body);
}
// Services/NotificationService.cs
public partial class NotificationService : INotificationService
{
public partial Task ShowLocalNotification(string title, string body);
}
Each platform folder then contains the implementation using its native notification APIs. The shared code never knows or cares about the platform specifics — it just calls ShowLocalNotification and trusts that the right implementation is compiled in.
This pattern scales well. As your app grows, your Platforms/ folders naturally organise platform-specific code while your shared project remains clean and focused on business logic.