为什么选 MAUI
ContestScoring 项目的桌面端已经基于 WinUI 3 (C# .NET 8) 构建,移动端自然希望复用现有 C# 业务逻辑和模型定义。摆在面前的选择有三个:
| 方案 | 语言 | 平台覆盖 | 代码复用 | 原生体验 |
|---|---|---|---|---|
| Flutter | Dart | 全平台 | 0%(需全重写) | Skia 自绘 |
| React Native | TS/JS | iOS/Android | 0%(需全重写) | 原生桥接 |
| .NET MAUI | C# | 全平台 + Windows | 80%+ | 原生控件 |
MAUI 允许我们直接引用 ContestScoring.Shared 项目,所有模型(Competition、Judge、Score)、协议(IScoringHub)和计算逻辑(ScoringAlgorithm)零修改复用。这对于一个小团队来说,是决定性的优势。
四大平台渲染管线
MAUI 的跨平台魔法在于:一套 XAML + 一套 C# 逻辑,底层映射到各平台的原生控件。
┌─────────────────────────────────────────┐
│ .NET MAUI App │
│ Pages/ Services/ │
│ ConnectPage ConnectionService │
│ JudgePage ScoringService │
│ VotePage ... │
│ (C# + XAML — Write Once) │
├──────────┬──────────┬──────────┬───────┤
│ WinUI 3 │ Android │ iOS │ macOS │
│ (WinApp │ (Android │ (UIKit) │ (App │
│ SDK) │ Views) │ │ Kit) │
│ │ │ │ │
│ Windows │ Samsung │ iPhone │ Mac │
│ 10/11 │ Pixel │ iPad │ Mini │
└──────────┴──────────┴──────────┴───────┘
这意味着:一个 Button 在 Windows 上渲染为 WinUI Button,在 Android 上渲染为 AppCompatButton,在 iOS 上渲染为 UIButton。开发者无需关心底层实现——除非你需要定制。
实战:ConnectionService 跨平台适配
移动端最核心的功能是 LAN 无线评分。评委连接到裁判长桌面端,建立 SignalR 实时通信。来看看这一功能如何在不同平台上实现:
共享代码(95%)
// ConnectionService.cs — 所有平台共享
public class ConnectionService : IConnectionService
{
private HubConnection? _hub;
public async Task<bool> ConnectAsync(string url, string judgeId)
{
_hub = new HubConnectionBuilder()
.WithUrl($"{url}/scoring", opts =>
{
// TLS 自签名证书信任(所有平台统一)
opts.HttpMessageHandlerFactory = _ => new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
};
})
.Build();
_hub.On<ScoreResult>("OnScoreSubmitted", HandleScoreSubmitted);
_hub.On<string, string>("OnJudgeOffline", HandleJudgeOffline);
await _hub.StartAsync();
return true;
}
}
这段代码在四个平台上一字不改就能运行。SignalR 客户端库(Microsoft.AspNetCore.SignalR.Client)自身处理了所有平台差异。
平台特化(5%)
唯一的平台差异在于 Wi-Fi 状态检测和证书存储:
// Platforms/Android/AndroidNetworkHelper.cs
public class AndroidNetworkHelper : INetworkHelper
{
public bool IsWiFiConnected()
{
var manager = (ConnectivityManager)
Android.App.Application.Context
.GetSystemService(Context.ConnectivityService)!;
var network = manager.ActiveNetwork;
var caps = manager.GetNetworkCapabilities(network);
return caps?.HasTransport(TransportType.Wifi) == true;
}
}
// Platforms/iOS/iOSNetworkHelper.cs
public class iOSNetworkHelper : INetworkHelper
{
public bool IsWiFiConnected()
{
return Reachability.InternetConnectionStatus() ==
NetworkStatus.ReachableViaWiFiNetwork;
}
}
这些平台特化代码通过依赖注入注册,MAUI 在运行时自动选择正确的实现:
// MauiProgram.cs
builder.Services.AddSingleton<INetworkHelper>(sp =>
{
#if ANDROID
return new AndroidNetworkHelper();
#elif IOS
return new iOSNetworkHelper();
#elif MACCATALYST
return new MacNetworkHelper();
#else
return new WindowsNetworkHelper();
#endif
});
遇到的坑
坑 1:Android 启动白屏
Android 端冷启动时会有 2-3 秒的白屏,这是因为 MAUI 需要初始化 .NET 运行时并加载 MAUI 框架。
// 解决方案:自定义 Splash Screen
// Platforms/Android/Resources/values/styles.xml
<style name="Maui.SplashTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:windowBackground">@drawable/splash_screen</item>
</style>
坑 2:iOS 安全区域 (Safe Area)
iPhone X 及更新机型的刘海屏需要正确处理安全区域,否则按钮会被刘海遮挡:
// XAML 中声明安全区域
<ContentPage xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration
.iOSSpecific;assembly=Microsoft.Maui.Controls"
ios:Page.UseSafeArea="True">
...
</ContentPage>
坑 3:TableView 性能
选手列表使用 CollectionView,但 Android 上滚动到 100+ 项时出现明显卡顿。排查发现是 MAUI 默认的 DataTemplate 为每一项创建了过深的视觉树。
// 优化:扁平化视觉树 + 虚拟化
<CollectionView ItemsLayout="VerticalList"
RemainingItemsThreshold="5"
RemainingItemsThresholdReached="OnLoadMore">
<CollectionView.ItemTemplate>
<DataTemplate>
<!-- 只用一层 Grid,不用嵌套 StackLayout -->
<Grid ColumnDefinitions="Auto,*,Auto"
Padding="12,8" HeightRequest="48">
...
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
构建与部署
四平台构建命令
| 平台 | 命令 |
|---|---|
| Windows | dotnet build -f net10.0-windows10.0.19041 |
| Android | dotnet build -f net10.0-android |
| iOS | dotnet build -f net10.0-ios(需 macOS) |
| macOS | dotnet build -f net10.0-maccatalyst(需 macOS) |
一条关键经验:iOS/macOS 构建必须在 macOS 上完成(需要 Xcode 工具链),而 Android 和 Windows 可以在 Windows 开发机上直接构建。对于小团队,这意味着要么用 Mac 作为 CI 构建机,要么用 GitHub Actions 的 macOS runner。
最终效果
经过上述适配,ContestScoring Mobile 在四个平台上实现了统一体验:
- Windows:评委可在一台 Windows 平板上打开 MAUI 应用,用触摸屏快速打分
- Android:支持 Android 8.0+,涵盖 95% 的活跃设备
- iOS:支持 iOS 16+,iPhone + iPad 双适配(利用
OnIdiom自适应布局) - macOS:作为桌面端备选(桌面端主力仍是 WinUI 3 裁判长端)
经验总结
- MAUI 适合 .NET 技术栈团队:代码复用率 80%+,共享业务逻辑和模型层
- 5% 的平台特化不可避免:网络检测、权限申请、后台任务等必须分平台处理
- XAML 性能敏感:避免深嵌套,善用虚拟化 CollectionView,ListView 只用于短列表
- macOS 构建是硬门槛:iOS/macOS 部署必须有 Mac 设备(或云 Mac CI)
- Handler 定制比 Custom Renderer 好:MAUI Handler 架构更轻量,未来 vs Xamarin 迁移也更简单