为什么选 MAUI

ContestScoring 项目的桌面端已经基于 WinUI 3 (C# .NET 8) 构建,移动端自然希望复用现有 C# 业务逻辑和模型定义。摆在面前的选择有三个:

方案语言平台覆盖代码复用原生体验
FlutterDart全平台0%(需全重写)Skia 自绘
React NativeTS/JSiOS/Android0%(需全重写)原生桥接
.NET MAUIC#全平台 + Windows80%+原生控件

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>

构建与部署

四平台构建命令

平台命令
Windowsdotnet build -f net10.0-windows10.0.19041
Androiddotnet build -f net10.0-android
iOSdotnet build -f net10.0-ios(需 macOS)
macOSdotnet build -f net10.0-maccatalyst(需 macOS)

一条关键经验:iOS/macOS 构建必须在 macOS 上完成(需要 Xcode 工具链),而 Android 和 Windows 可以在 Windows 开发机上直接构建。对于小团队,这意味着要么用 Mac 作为 CI 构建机,要么用 GitHub Actions 的 macOS runner。

最终效果

经过上述适配,ContestScoring Mobile 在四个平台上实现了统一体验:

经验总结

  1. MAUI 适合 .NET 技术栈团队:代码复用率 80%+,共享业务逻辑和模型层
  2. 5% 的平台特化不可避免:网络检测、权限申请、后台任务等必须分平台处理
  3. XAML 性能敏感:避免深嵌套,善用虚拟化 CollectionView,ListView 只用于短列表
  4. macOS 构建是硬门槛:iOS/macOS 部署必须有 Mac 设备(或云 Mac CI)
  5. Handler 定制比 Custom Renderer 好:MAUI Handler 架构更轻量,未来 vs Xamarin 迁移也更简单