.NET MAUI 正式版初体验

thumbnail

.NET Multi-platform App UI (MAUI) 现已加入 Visual Studio 2022 17.3,成为工作负载(Workload)之一,遥想上次 VS 增加新 workload 还是在上一次。VS 的默认项目模板支持 Android、iOS、MacCatalyst、Tizen 和 Windows。Linux仅有社区支持。

项目结构

默认项目的目录结构是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.
└── MauiApp1
   ├── App.xaml
   ├── App.xaml.cs
   ├── AppShell.xaml
   ├── AppShell.xaml.cs
   ├── MainPage.xaml
   ├── MainPage.xaml.cs
   ├── MauiApp1.csproj
   ├── MauiProgram.cs
   ├── Platforms
   │   ├── Android
   │   ├── MacCatalyst
   │   ├── Tizen
   │   ├── Windows
   │   └── iOS
   ├── Properties
   │   └── launchSettings.json
   └── Resources

结构说明

MauiProgram.cs 中定义了一个静态类和一个名为CreateMauiApp的静态方法,方法内部使用 Generic Hosting 模式创建了一个 MauiApp 并配置依赖注入,就像 ASP.NET Core 一样。

编译时,对应平台会使用Platforms文件夹中的配置,这些文件夹中的内容是特定于平台的。

例如 Windows 平台是一个 WinUI3 项目的结构,它的 App 类继承自 MauiWinUIApplication, 而后者继承自Microsoft.UI.Xaml.Application,也就是 windows app sdk 提供的 WinUI3 程序的基类。这个App类便会调用MauiProgram.CreateMauiApp

Android 平台则是 一个MainApplication类,它继承自MauiApplication,而后者继承自Android.App.Application,这是 Android 应用程序的基类。

这使得可以修改应用程序对于特定平台的配置, 例如可以分别配置 Android 平台的 AndroidMainifest 和 WinUI 的 app.mainifest与以及MSIX打包的 Package.appxmanifest。

MAUI 使用平台原生UI框架

xaml文件最终会被编译为各个平台的原生UI。这里我就在默认实例上加几个控件。
MAUI WinUI 和 Android 控件对比

可以看到,尽管控件样式基本一致,但都渲染为平台自己的UI框架对应的控件。比如 Switch 渲染为了 WinUI 的 ToggleSwitch,它会在旁边显示 OnOff 的文字;而 Android 的 Switch 控件则不会,而且还在左侧空出了空间。

更明显的,可以看看两个平台的日期选择控件:
MAUI WinUI 和 Android 日期选择控件对比

这就像是同样的 html input 标签,不同的浏览器会使用不同默认样式。同样的 xaml 会被编译为平台本机的 UI。

就像用css来统一html样式,指定 Style 可以是 MAUI 在各个平台上获得几乎一样的表现,同时使用平台原生的UI交互。

开发

特定于平台的代码

尽管 MAUI 在于统一各个平台,但是仍然可以指定不同平台的不同实现。文档的 Platform Integration一节介绍了 MAUI 库提供的不同平台 API 的统一抽象,但是对于这些 API 没有覆盖到的地方,仍然要自行编写针对不同平台的实现。

这是一个常见的场景,例如要获取当前应用是否处于流量计费的网络,目前 MAUI 平台库没有现成的封装,要自己写的话,很显然 Windows 上的实现和 Android/iOS 上是不同的(而我甚至不知道 Mac 有没有这个功能)。

实例:统一接口和平台特定的实现

配置项目多目标

借由依赖注入模式,可以让一个接口在不同平台对应不同的实现。
首先你参考官方文档配置多目标。这里就直接用最后一项方案,结合基于目录和基于文件名的多目标。在项目配置里加上这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<ItemGroup Condition="$(TargetFramework.StartsWith('net6.0-android')) != true">
<Compile Remove="**\**\*.Android.cs" />
<None Include="**\**\*.Android.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
<Compile Remove="**\Android\**\*.cs" />
<None Include="**\Android\**\*.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

<ItemGroup Condition="$(TargetFramework.StartsWith('net6.0-ios')) != true AND $(TargetFramework.StartsWith('net6.0-maccatalyst')) != true">
<Compile Remove="**\**\*.iOS.cs" />
<None Include="**\**\*.iOS.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
<Compile Remove="**\iOS\**\*.cs" />
<None Include="**\iOS\**\*.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

<ItemGroup Condition="$(TargetFramework.Contains('-windows')) != true ">
<Compile Remove="**\*.Windows.cs" />
<None Include="**\*.Windows.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
<Compile Remove="**\Windows\**\*.cs" />
<None Include="**\Windows\**\*.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

效果是:例如,当编译 Android 平台时,所有 *.Windows.cs*.iOS.cs 文件,或是任何 WindowsiOS 文件夹下的文件,都会被忽略并跳过编译,Roslyn也不会分析对应的代码,从而保证 IntelliSense 的正确性。

定义接口

创建一个INetStatusService.cs文件,内容如下:

1
2
3
4
5
6
7
namespace MauiApp1
{
public interface INetStatusService
{
public bool IsMeteredConnection();
}
}

如果你在这个共享文件里直接尝试访问平台命名空间,IntelliSense会给出这样的提示:
IntelliSense提示:平台可用性
所以,必须分别编写三个平台的代码,实现这个接口。

分平台实现接口

Windows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using Windows.Networking.Connectivity;
namespace MauiApp1
{
public class NetStatusService : INetStatusService
{
public bool IsMeteredConnection()
{
var profile = NetworkInformation.GetInternetConnectionProfile();
var cost = profile.GetConnectionCost();
return cost.NetworkCostType switch
{
NetworkCostType.Unrestricted => false,
NetworkCostType.Unknown => false,
_ => true
};
}
}
}

另外,你还会注意到这里的switch表达式模板匹配语法,这也是 MAUI 的好处之一,它使用最新的 .NET SDK,支持最新的 C# 语法。

Android:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using Android.Content;
using Android.Net;

namespace MauiApp1
{
public class NetStatusService : INetStatusService
{
public bool IsMeteredConnection()
{
var manager = (ConnectivityManager)Android.App.Application.Context.GetSystemService(Context.ConnectivityService);
return manager.IsActiveNetworkMetered;
}
}
}

iOS (我不会(因为没Mac),那就 throw not impl吧)

1
2
3
4
5
6
7
8
9
10
namespace MauiApp1
{
public class NetStatusService : INetStatusService
{
public bool IsMeteredConnection()
{
throw new NotImplementedException();
}
}
}
依赖注入

由于 Maui App 使用 Generic Host 和 依赖注入模式。所以可以轻松地为这个接口配置多实现。

MauiProgram.cs 加一句

1
builder.Services.AddTransient<INetStatusService, NetStatusService>();

虽然构造函数注入很好用,但是对于这个简单的示例,或是一些特殊情况,还是要拿到 IServiceProvider 的。由于各个平台的App基类不一样,所以给 App.xaml.cs 加一个属性方便使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace MauiApp1;

public partial class App : Application
{
public App()
{
InitializeComponent();

MainPage = new AppShell();
}
public static IServiceProvider Services =>
#if ANDROID
MauiApplication.Current.Services;
#elif WINDOWS10_0_17763_0_OR_GREATER
MauiWinUIApplication.Current.Services;
#elif MACCATALYST || IOS
MauiUIApplicationDelegate.Current.Services;
#else
null;
#endif
}

这样就可以随时通过App.Services来得到 DI 容器了。

MainPage.xaml.cs 设置一个属性用于绑定,并在构造函数里设置默认的BindingContext,实际项目应该用ViewModel实现MVVM的,不过这个示例就从简了。

1
2
3
4
5
6
7
public MainPage()
{
InitializeComponent();
this.BindingContext = this;
}
private readonly INetStatusService _netStatus = App.Services.GetRequiredService<INetStatusService>();
public bool IsMeteredConnection => _netStatus.IsMeteredConnection();

然后我们在示例页面加个控件测试一下:

1
2
3
4
5
6
<StackLayout Orientation="Horizontal">
<CheckBox
IsChecked="{x:Binding IsMeteredConnection}"
IsEnabled="False" />
<Label Text="使用按流量计费的网络" />
</StackLayout>

在 Windows 上的效果:

Windows示例

在 Android 上的效果:
Android示例

其它

MAUI 还有强大的热重载,包括UI和后台代码的即时加载,而且在所有平台都可用。

MAUI 短期内不会添加官方的Linux支持,这是因为Linux桌面环境碎片严重。

比起早先的 Xamarin Forms, MAUI 生成的 Android 应用体积小了不少。另外对于 Windows 平台的 WinUI3,可以像其它 WinUI3 应用一样配置为不使用MSIX打包发布,这样最终发布的应用是一个可以直接执行的 .exe 文件,就像传统win32应用程序一样。

参考资料

.NET Multi-platform App UI documentation
.NET MAUI Roadmap
Introducing .NET MAUI - One Codebase, Many Platforms - .NET Blog

Permalink: http://blog.artiga.top/2022/maui-taste/

本文采用CC BY-NC-SA 4.0许可

Comments