.NET MAUI is right around the corner. Like its predecessor it offers sharing most code while building for multiple platforms. This includes the UI part. Of course there are still elements that require access to the platform, like accessing the camera, or something less common like customizing HttpMessageHandler behavior on iOS.
For common use-cases Xamarin.Essentials was offered to Xamarin Forms users. Now with MAUI this library will be included in your application by default (named MAUI Essentials), making it easy to use SecureStorage or implement permissions on Android and iOS. Additionally there is a MAUI Community Toolkit which, as the name implies, provides free tools: effects, converters, controls and more.
In some apps .NET MAUI with the Essentials pack and the Xamarin Community Toolkit can be sufficient. For most apps though, there is a point where some custom adjustment is necessary. A good example would be control adjustment using Handlers or Effects. We’ll describe Handlers and their value in more detail in a future blogpost.
There are several ways to go about defining platform specific behavior in MAUI. Because of multi-targeting the code will be in the single shared project. With Xamarin Forms this was also possible using the Shared Project architecture, but the recommended and most common approach was using a PCL and implementing platform behavior using dependency injection. Let’s run through some options!
#if __IOS__.
This is a quick and easy solution and keeps the related code close together, in the same file. For smaller handler mappings this could be a nice and easy to manage approach. It could also make Effects easier, because we don’t need the routed effects and DI registration of every platform implementation. With larger sections of platform-specific code this can get messy and harder to read though.
//SecureSettingsProvider.cs
namespace MyApp.Settings;
public class SecureSettingsProvider
{
public string GetSecureSetting(string context, string key)
{
return GetLocalSetting($"{context}-{key}");
}
private string GetLocalSetting(string contextSpecificKey)
{
#if __IOS__
// retrieve value from secure storage / keychain
#elif __ANDROID__
// do the same for android
#endif
}
}
MAUI projects using the single project structure will provide platform folders out-of-the-box. They will be compiled conditionally by default, e.g. the Android folder will only be included when building for Android. Within this option there are still 2 distinctive approaches:
//SecureSettingsProvider.cs in shared code
namespace MyApp.Settings;
public partial class SecureSettingsProvider.cs
{
public string GetSecureSetting(string context, string key)
{
var settingValue = GetLocalSetting($"{context}-{key}");
// do some work that's shared between implementations
if(String.IsNullOrEmtpy(something))
return "nothing";
else
return settingValue;
}
}
//SecureSettingsProvider.cs in Android platform folder
namespace MyApp.Settings;
public partial class SecureSettingsProvider.cs
{
private string GetLocalSetting(string contextSpecificKey)
{
// retrieve secure setting value for Android
}
}
This pattern is used in the MAUI github repository itself. Concretely this again means using partial classes, but this time the platform specific implementation is defined in the same folder in a separate file. For example for our SecureS
ettingsProvider
, we would have a partial class in SecureSettingsProvider.cs
and another one in SecureSettingsProvider.Android.cs
, SecureSettingsProvider.iOS.cs
, etc.
A potential advantage of this is that platform implementations are directly next to the shared part, which improves discoverability and can reduce cognitive load.
*.iOS.cs
files in the project unless the current build target is Xamarin.iOS, net6.0-ios or .net6.0-maccatalyst. Similar project configuration can also be used to allow for custom platform folders thoughout the code:
As always in software development, the answer here is “it depends”. For multitargeting libraries, like MAUI itself, it is likely a good option to look at the the filename or nested folder approach we discussed. With a little project setup this allows you to group code by functionality, keeping related code close together.
For apps, it depends on how much platform-specific code really needs to be written. For some apps only a few adjustments may be needed using Effects, Behaviors and handler mappings. It is very possible for these to be only a few lines which allows for keeping everying in one file using #if directives. If these grow it is convenient to use the existing platform folders. In both these alternatives you can forego the need to use interfaces and register the platform services for dependency injection which can be convenient.
Please note that we’ve been looking at these patterns fairly strictly from MAUI perspective. Of course it is possible that you prefer to use interfaces to making testing and mocking easier. In that case you will be setting up the DI container anyway. It may still be easier to register one implementation and use above options, instead of registering every platform service separately, but keep in mind your overall architecture before choosing any of these patterns.