.NET MAUI推送通知与本地通知完全指南:FCM、APNs到Azure Notification Hubs实战

全面介绍.NET MAUI应用中推送通知和本地通知的实现方法,涵盖Plugin.LocalNotification、Android通知渠道、FCM集成、APNs配置、Azure Notification Hubs跨平台推送、权限管理、富媒体通知和深度链接,附实战代码和最佳实践。

移动应用通知简介:为什么通知这么重要?

说实话,做移动应用开发这些年,我越来越觉得通知系统就是应用和用户之间那根看不见的线。想想看——社交媒体的新消息提醒、电商的促销推送、健康App的运动打卡……这些通知在悄无声息地拉着用户回到应用里来。

通知主要就两大类:本地通知推送通知

本地通知由应用本身在设备上触发,不需要联网就能工作,适合做闹钟、提醒事项这类场景。推送通知则通过远程服务器发送,即使应用没在运行也能送达用户——实时消息、营销活动这类需要服务器主动推的场景就得靠它了。

.NET MAUI(Multi-platform App UI)作为微软的跨平台开发框架,给开发者提供了统一的API来构建iOS、Android、Windows和macOS应用。不过呢,通知这块涉及各平台的原生特性,平台差异还挺多的,需要特别注意。

这篇文章会从头到尾带你走一遍在.NET MAUI中实现本地通知和推送通知的完整流程。准备好了吗?那我们开始吧。

本地通知基础:原生方法与插件选择

.NET MAUI实现本地通知有几种方式。你可以直接调平台原生API,也可以借助社区插件来简化开发。目前最流行的方案是Plugin.LocalNotification,它提供了统一的API接口,帮你把底层平台差异给屏蔽掉了(这一点真的很香)。

Plugin.LocalNotification的安装与配置

首先,通过NuGet安装:

dotnet add package Plugin.LocalNotification --version 11.1.0

然后在MauiProgram.cs里注册服务:

using Plugin.LocalNotification;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .UseLocalNotification() // 注册本地通知服务
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

        return builder.Build();
    }
}

发送简单的本地通知

用Plugin.LocalNotification发通知真的很简单,看一下这个基本示例就明白了:

using Plugin.LocalNotification;

public async Task SendSimpleNotification()
{
    var notification = new NotificationRequest
    {
        NotificationId = 1001,
        Title = "欢迎使用",
        Subtitle = "新功能上线",
        Description = "查看我们为您准备的全新功能和优惠活动!",
        BadgeNumber = 1,
        Schedule = new NotificationRequestSchedule
        {
            NotifyTime = DateTime.Now.AddSeconds(5) // 5秒后显示
        }
    };

    await LocalNotificationCenter.Current.Show(notification);
}

立即显示通知

如果不需要延迟,直接省掉Schedule属性就行了:

public async Task SendImmediateNotification()
{
    var notification = new NotificationRequest
    {
        NotificationId = 1002,
        Title = "消息提醒",
        Description = "您有一条新消息待查看",
        ReturningData = "NavigateToMessages" // 用户点击时返回的数据
    };

    await LocalNotificationCenter.Current.Show(notification);
}

取消和管理通知

通知的管理也很灵活,该取消取消,该清除清除:

// 取消特定ID的通知
await LocalNotificationCenter.Current.Cancel(1001);

// 取消所有待处理的通知
await LocalNotificationCenter.Current.CancelAll();

// 清除所有已显示的通知
await LocalNotificationCenter.Current.Clear();

// 获取所有待处理的通知
var pendingNotifications = await LocalNotificationCenter.Current.GetPendingNotificationList();

// 获取所有已送达的通知
var deliveredNotifications = await LocalNotificationCenter.Current.GetDeliveredNotificationList();

Android通知渠道:精细化通知管理

从Android 8.0(API 26)开始,Google搞了个叫通知渠道(Notification Channels)的东西。简单来说,就是让用户可以对不同类型的通知进行精细化控制——可以单独设置每个渠道的重要性、声音、震动等等。

理解通知渠道的重要性

举个例子,一个新闻App可以创建"突发新闻"、"体育赛事"、"娱乐资讯"等不同渠道,用户能选择只收突发新闻的通知,把其他的都关掉。这个设计思路其实挺人性化的。

重要的是:在Android 8.0及以上版本,如果不创建通知渠道,通知就压根显示不出来。所以这一步千万不能跳过。

创建通知渠道

用Plugin.LocalNotification创建渠道的代码如下:

public void CreateNotificationChannels()
{
    if (DeviceInfo.Platform == DevicePlatform.Android)
    {
        // 创建重要通知渠道
        var urgentChannel = new NotificationChannel
        {
            Id = "urgent_channel",
            Name = "重要通知",
            Description = "用于显示紧急和重要的通知消息",
            Importance = NotificationImportance.High,
            Sound = "notification_urgent.mp3"
        };

        // 创建常规通知渠道
        var generalChannel = new NotificationChannel
        {
            Id = "general_channel",
            Name = "常规通知",
            Description = "用于显示一般性的通知消息",
            Importance = NotificationImportance.Default
        };

        // 创建静默通知渠道
        var silentChannel = new NotificationChannel
        {
            Id = "silent_channel",
            Name = "静默通知",
            Description = "不会发出声音的通知",
            Importance = NotificationImportance.Low,
            Sound = null
        };

        LocalNotificationCenter.Current.CreateNotificationChannel(urgentChannel);
        LocalNotificationCenter.Current.CreateNotificationChannel(generalChannel);
        LocalNotificationCenter.Current.CreateNotificationChannel(silentChannel);
    }
}

通知重要性级别详解

Android一共定义了五个通知重要性级别,搞清楚它们的区别很有必要:

  • None(无):通知直接不显示,用户完全看不到
  • Min(最低):会出现在状态栏和通知抽屉里,但没声音也没震动
  • Low(低):通知会显示,不发声不振动,锁屏上也不会出现
  • Default(默认):发出声音,但不会弹窗
  • High(高):既发声又弹窗提醒(Heads-up notification),最醒目的那种

使用指定渠道发送通知

public async Task SendChannelNotification()
{
    var notification = new NotificationRequest
    {
        NotificationId = 2001,
        Title = "系统升级通知",
        Description = "系统将在今晚23:00进行维护升级,预计持续2小时",
        Android = new AndroidOptions
        {
            ChannelId = "urgent_channel", // 使用重要通知渠道
            Priority = AndroidPriority.High,
            VisibilityType = AndroidVisibilityType.Public
        }
    };

    await LocalNotificationCenter.Current.Show(notification);
}

自定义通知样式和行为

Android支持的自定义选项真的很丰富,大图标、颜色、进度条、LED灯、震动模式……基本上你想要的都能配:

public async Task SendCustomStyledNotification()
{
    var notification = new NotificationRequest
    {
        NotificationId = 2002,
        Title = "订单配送中",
        Description = "您的订单正在配送中,预计30分钟内送达",
        CategoryType = NotificationCategoryType.Status,
        Android = new AndroidOptions
        {
            ChannelId = "general_channel",
            Priority = AndroidPriority.High,

            // 设置大图标
            IconName = "delivery_icon",

            // 设置颜色
            Color = new AndroidColor(0, 122, 255),

            // 自动取消(用户点击后自动消失)
            AutoCancel = true,

            // 显示时间戳
            ShowTimestamp = true,

            // 设置进度条
            ProgressBarMax = 100,
            ProgressBarProgress = 65,
            ProgressBarIndeterminate = false,

            // LED灯效果
            LedColor = new AndroidColor(0, 255, 0),

            // 震动模式(单位:毫秒)
            VibrationPattern = new long[] { 0, 500, 200, 500 }
        }
    };

    await LocalNotificationCenter.Current.Show(notification);
}

定时通知:让应用在对的时间提醒用户

定时通知大概是本地通知最常用的场景了。Plugin.LocalNotification在这方面做得不错,支持一次性通知、重复通知以及基于日期时间的复杂调度。

一次性定时通知

public async Task ScheduleOneTimeNotification()
{
    var notification = new NotificationRequest
    {
        NotificationId = 3001,
        Title = "会议提醒",
        Description = "项目评审会议将在15分钟后开始,请准时参加",
        Schedule = new NotificationRequestSchedule
        {
            NotifyTime = DateTime.Now.AddMinutes(15)
        }
    };

    await LocalNotificationCenter.Current.Show(notification);
}

每日重复通知

public async Task ScheduleDailyNotification()
{
    var notification = new NotificationRequest
    {
        NotificationId = 3002,
        Title = "每日健康提醒",
        Description = "该起来活动一下了!保持健康的生活习惯。",
        Schedule = new NotificationRequestSchedule
        {
            NotifyTime = DateTime.Today.AddHours(9), // 每天上午9点
            RepeatType = NotificationRepeat.Daily
        }
    };

    await LocalNotificationCenter.Current.Show(notification);
}

每周重复通知

public async Task ScheduleWeeklyNotification()
{
    var notification = new NotificationRequest
    {
        NotificationId = 3003,
        Title = "周报提醒",
        Description = "本周工作周报截止日期为今天17:00,请及时提交。",
        Schedule = new NotificationRequestSchedule
        {
            NotifyTime = DateTime.Today.AddHours(10), // 上午10点
            RepeatType = NotificationRepeat.Weekly
        }
    };

    await LocalNotificationCenter.Current.Show(notification);
}

复杂的定时场景

遇到更复杂的需求怎么办?比如用药提醒一天要提醒三次。这时候可以用循环创建多个通知实例:

public async Task ScheduleMultipleMedicationReminders()
{
    // 用药提醒:每天早中晚三次
    var times = new[] { 8, 13, 20 }; // 8:00, 13:00, 20:00

    for (int i = 0; i < times.Length; i++)
    {
        var notification = new NotificationRequest
        {
            NotificationId = 4000 + i,
            Title = "用药提醒",
            Description = $"该服用{GetMealTime(i)}的药物了,请按时服药。",
            Schedule = new NotificationRequestSchedule
            {
                NotifyTime = DateTime.Today.AddHours(times[i]),
                RepeatType = NotificationRepeat.Daily
            },
            Android = new AndroidOptions
            {
                ChannelId = "urgent_channel",
                Priority = AndroidPriority.High
            }
        };

        await LocalNotificationCenter.Current.Show(notification);
    }
}

private string GetMealTime(int index)
{
    return index switch
    {
        0 => "早餐后",
        1 => "午餐后",
        2 => "晚餐后",
        _ => "餐后"
    };
}

Firebase Cloud Messaging:Android推送通知实现

接下来到推送通知了。Firebase Cloud Messaging(FCM)是Google提供的免费跨平台消息推送方案,支持Android、iOS和Web。老实说,对于大多数项目来说,FCM基本就是标配了。

Firebase项目配置

写代码之前,得先在Firebase控制台把东西配好:

  1. 访问Firebase Console(https://console.firebase.google.com)
  2. 创建新项目或选择现有项目
  3. 添加Android应用,包名要和.NET MAUI项目的ApplicationId一致(这点容易搞错,注意一下)
  4. 下载google-services.json文件
  5. google-services.json放到Android项目的根目录

安装Plugin.FirebasePushNotifications

这里用的是社区维护的Plugin.FirebasePushNotifications v3.x插件:

dotnet add package Plugin.FirebasePushNotifications --version 3.0.0

配置Android项目

编辑Platforms/Android/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application android:allowBackup="true"
                 android:icon="@mipmap/appicon"
                 android:roundIcon="@mipmap/appicon_round"
                 android:supportsRtl="true">
    </application>

    <!-- Firebase所需权限 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />

    <!-- Android 13+通知权限 -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest>

然后确保google-services.json的构建操作设置为GoogleServicesJson。在项目文件里加上:

<ItemGroup Condition="$(TargetFramework.Contains('android'))">
    <GoogleServicesJson Include="Platforms\Android\google-services.json" />
</ItemGroup>

注册Firebase推送通知服务

MauiProgram.cs中注册:

using Plugin.FirebasePushNotifications;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .UseLocalNotification()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

#if ANDROID || IOS
        builder.Services.AddSingleton<IFirebasePushNotification>(
            FirebasePushNotificationManager.Current);
#endif

        return builder.Build();
    }
}

初始化Firebase并获取设备令牌

App.xaml.cs或主页面中做初始化。这部分代码稍微长一点,但每一步都很关键:

public partial class App : Application
{
    public App()
    {
        InitializeComponent();
        InitializeFirebase();
        MainPage = new AppShell();
    }

    private void InitializeFirebase()
    {
        // 订阅令牌刷新事件
        CrossFirebasePushNotification.Current.OnTokenRefresh += (s, token) =>
        {
            System.Diagnostics.Debug.WriteLine($"FCM Token: {token}");
            // 将令牌发送到您的服务器
            SendTokenToServer(token);
        };

        // 订阅通知接收事件
        CrossFirebasePushNotification.Current.OnNotificationReceived += (s, data) =>
        {
            System.Diagnostics.Debug.WriteLine("通知已收到");
            foreach (var key in data.Data.Keys)
            {
                System.Diagnostics.Debug.WriteLine($"{key}: {data.Data[key]}");
            }
        };

        // 订阅通知打开事件
        CrossFirebasePushNotification.Current.OnNotificationOpened += (s, response) =>
        {
            System.Diagnostics.Debug.WriteLine("通知被用户打开");
            HandleNotificationNavigation(response.Data);
        };

        // 订阅通知删除事件
        CrossFirebasePushNotification.Current.OnNotificationDeleted += (s, data) =>
        {
            System.Diagnostics.Debug.WriteLine("通知被用户删除");
        };
    }

    private void SendTokenToServer(string token)
    {
        // 实现将设备令牌发送到您的后端服务器
        // 以便服务器可以向此设备发送推送通知
    }

    private void HandleNotificationNavigation(IDictionary<string, object> data)
    {
        if (data.ContainsKey("navigationTarget"))
        {
            var target = data["navigationTarget"].ToString();
            Shell.Current.GoToAsync($"//{target}");
        }
    }
}

订阅主题

FCM还支持主题订阅功能,可以向订阅了某个主题的所有用户发消息。这个在做新闻推送、活动通知的时候特别好用:

public async Task SubscribeToTopics()
{
    // 订阅"体育新闻"主题
    await CrossFirebasePushNotification.Current.SubscribeAsync("sports_news");

    // 订阅"促销活动"主题
    await CrossFirebasePushNotification.Current.SubscribeAsync("promotions");

    System.Diagnostics.Debug.WriteLine("已订阅主题");
}

public async Task UnsubscribeFromTopics()
{
    // 取消订阅
    await CrossFirebasePushNotification.Current.UnsubscribeAsync("promotions");

    System.Diagnostics.Debug.WriteLine("已取消订阅");
}

从服务器发送FCM消息

服务器端用Firebase Admin SDK或HTTP API来发消息。下面是用HTTP API的C#示例:

public async Task SendFcmNotification(string deviceToken, string title, string body)
{
    var serverKey = "YOUR_FIREBASE_SERVER_KEY"; // 从Firebase控制台获取
    var senderId = "YOUR_SENDER_ID";

    using var httpClient = new HttpClient();
    httpClient.DefaultRequestHeaders.TryAddWithoutValidation(
        "Authorization", $"key={serverKey}");
    httpClient.DefaultRequestHeaders.TryAddWithoutValidation(
        "Sender", $"id={senderId}");

    var payload = new
    {
        to = deviceToken,
        notification = new
        {
            title = title,
            body = body,
            sound = "default",
            badge = 1
        },
        data = new
        {
            navigationTarget = "MessagesPage",
            messageId = "12345",
            timestamp = DateTime.UtcNow.ToString("o")
        },
        priority = "high"
    };

    var json = JsonSerializer.Serialize(payload);
    var content = new StringContent(json, Encoding.UTF8, "application/json");

    var response = await httpClient.PostAsync(
        "https://fcm.googleapis.com/fcm/send", content);

    if (response.IsSuccessStatusCode)
    {
        Console.WriteLine("通知发送成功");
    }
    else
    {
        var error = await response.Content.ReadAsStringAsync();
        Console.WriteLine($"通知发送失败: {error}");
    }
}

Apple Push Notification Service:iOS推送通知配置

搞完Android,接下来看iOS这边。APNs(苹果推送通知服务)是iOS、iPadOS、macOS和watchOS上推送通知的基础设施。坦白说,配置APNs比FCM要麻烦不少,需要在Apple Developer Portal做好几步设置。

Apple Developer配置步骤

  1. 登录Apple Developer Portal(https://developer.apple.com)
  2. 创建App ID,启用Push Notifications能力
  3. 创建APNs证书(开发和生产环境各一个)或配置APNs Auth Key(推荐用Auth Key,省事)
  4. 创建Provisioning Profile,包含Push Notifications权限
  5. 下载并安装证书和配置文件

配置iOS项目

编辑Platforms/iOS/Entitlements.plist,添加推送通知权限:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>aps-environment</key>
    <string>development</string>
    <!-- 生产环境使用 "production" -->
</dict>
</plist>

编辑Platforms/iOS/Info.plist,加上后台模式:

<key>UIBackgroundModes</key>
<array>
    <string>remote-notification</string>
</array>

iOS推送通知初始化

Plugin.FirebasePushNotifications在iOS上也能用。Firebase会自动帮你跟APNs对接:

// 在Platforms/iOS/AppDelegate.cs中
using Foundation;
using UIKit;
using UserNotifications;

[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();

    public override bool FinishedLaunching(UIApplication app, NSDictionary options)
    {
        // 请求通知权限
        UNUserNotificationCenter.Current.RequestAuthorization(
            UNAuthorizationOptions.Alert |
            UNAuthorizationOptions.Badge |
            UNAuthorizationOptions.Sound,
            (granted, error) =>
            {
                if (granted)
                {
                    InvokeOnMainThread(() =>
                    {
                        UIApplication.SharedApplication.RegisterForRemoteNotifications();
                    });
                }
            });

        return base.FinishedLaunching(app, options);
    }

    public override void RegisteredForRemoteNotifications(
        UIApplication application, NSData deviceToken)
    {
        base.RegisteredForRemoteNotifications(application, deviceToken);
    }

    public override void FailedToRegisterForRemoteNotifications(
        UIApplication application, NSError error)
    {
        System.Diagnostics.Debug.WriteLine($"注册推送失败: {error.LocalizedDescription}");
        base.FailedToRegisterForRemoteNotifications(application, error);
    }
}

配置Firebase以支持iOS

在Firebase控制台的iOS应用设置中,还需要做这几步:

  1. 上传APNs认证密钥(.p8文件)或APNs证书(.p12文件)
  2. 输入密钥ID和团队ID(用Auth Key的话)
  3. 下载GoogleService-Info.plist文件
  4. GoogleService-Info.plist放到Platforms/iOS目录下
  5. 构建操作设置为BundleResource

Azure Notification Hubs:统一的跨平台推送后端

如果你的项目规模比较大,或者已经在用Azure生态,那Azure Notification Hubs可能是更好的选择。它是微软提供的企业级推送通知服务,支持所有主流平台。

Azure Notification Hubs的优势

为什么要考虑它?几个关键优势:

  • 统一管理:一个后端API搞定所有平台的推送
  • 标签和模板:灵活的用户分组和消息定制能力
  • 大规模推送:支持数百万设备同时推送(这个在To C应用里很重要)
  • 详细分析:推送成功率、设备注册等数据一目了然
  • 企业级功能:调度推送、A/B测试等高级玩法都有

安装Azure Notification Hubs SDK

dotnet add package Microsoft.Azure.NotificationHubs --version 4.2.0

客户端设备注册

在.NET MAUI应用中把设备注册到Azure Notification Hub:

using Microsoft.Azure.NotificationHubs;

public class NotificationService
{
    private const string NotificationHubName = "your-hub-name";
    private const string ListenConnectionString = "Endpoint=sb://...";

    private NotificationHubClient _hubClient;

    public NotificationService()
    {
        _hubClient = NotificationHubClient.CreateClientFromConnectionString(
            ListenConnectionString, NotificationHubName);
    }

    public async Task RegisterDeviceAsync(string platform, string deviceToken, string[] tags)
    {
        try
        {
            if (platform == "iOS")
            {
                await RegisterForApns(deviceToken, tags);
            }
            else if (platform == "Android")
            {
                await RegisterForFcm(deviceToken, tags);
            }
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine($"注册失败: {ex.Message}");
        }
    }

    private async Task RegisterForApns(string deviceToken, string[] tags)
    {
        var registration = new AppleRegistrationDescription(deviceToken, tags);
        await _hubClient.CreateOrUpdateRegistrationAsync(registration);
        System.Diagnostics.Debug.WriteLine("iOS设备注册成功");
    }

    private async Task RegisterForFcm(string deviceToken, string[] tags)
    {
        var registration = new FcmRegistrationDescription(deviceToken, tags);
        await _hubClient.CreateOrUpdateRegistrationAsync(registration);
        System.Diagnostics.Debug.WriteLine("Android设备注册成功");
    }
}

使用标签进行精准推送

标签系统是Azure Notification Hubs的核心亮点。你可以给设备打上各种标签,然后按标签组合来定向推送:

public async Task RegisterWithTags(string deviceToken)
{
    var tags = new[]
    {
        "userId:12345",           // 用户ID
        "language:zh-CN",          // 语言偏好
        "location:beijing",        // 地理位置
        "interests:sports",        // 兴趣标签
        "interests:technology",    // 可以有多个兴趣
        "vip:true"                 // VIP状态
    };

    var platform = DeviceInfo.Platform == DevicePlatform.iOS ? "iOS" : "Android";
    await RegisterDeviceAsync(platform, deviceToken, tags);
}

服务器端发送通知

从后端向特定标签的设备推送通知:

using Microsoft.Azure.NotificationHubs;

public class PushNotificationServer
{
    private NotificationHubClient _hubClient;
    private const string FullConnectionString = "Endpoint=sb://...";
    private const string HubName = "your-hub-name";

    public PushNotificationServer()
    {
        _hubClient = NotificationHubClient.CreateClientFromConnectionString(
            FullConnectionString, HubName);
    }

    // 发送到特定用户
    public async Task SendToUserAsync(string userId, string title, string message)
    {
        var tagExpression = $"userId:{userId}";
        await SendNotificationAsync(tagExpression, title, message);
    }

    // 复杂的标签表达式:发送给北京的体育爱好者
    public async Task SendToBeijingSportsLoversAsync(string title, string message)
    {
        var tagExpression = "location:beijing && interests:sports";
        await SendNotificationAsync(tagExpression, title, message);
    }

    private async Task SendNotificationAsync(string tagExpression, string title, string message)
    {
        try
        {
            // iOS通知载荷
            var apnsJson = $"{{\"aps\":{{\"alert\":{{\"title\":\"{title}\",\"body\":\"{message}\"}},\"badge\":1,\"sound\":\"default\"}}}}";

            // Android通知载荷
            var fcmJson = $"{{\"notification\":{{\"title\":\"{title}\",\"body\":\"{message}\",\"sound\":\"default\"}}}}";

            // 同时发送到iOS和Android
            await _hubClient.SendAppleNativeNotificationAsync(apnsJson, tagExpression);
            await _hubClient.SendFcmNativeNotificationAsync(fcmJson, tagExpression);

            System.Diagnostics.Debug.WriteLine("通知已发送");
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine($"发送通知失败: {ex.Message}");
        }
    }
}

通知权限管理:Android 13+和iOS的注意事项

这几年,移动操作系统对通知权限的管控越来越严了。Android 13(API 33)新增了运行时通知权限,iOS那边一直都有严格的权限请求规范。说白了,你不能一上来就弹权限框——那样用户八成会拒绝。

跨平台权限请求服务

public class NotificationPermissionService
{
    public async Task<bool> RequestNotificationPermissionAsync()
    {
        if (DeviceInfo.Platform == DevicePlatform.iOS)
        {
            return await RequestiOSPermission();
        }
        else if (DeviceInfo.Platform == DevicePlatform.Android)
        {
            return await RequestAndroidPermission();
        }

        return false;
    }

    private async Task<bool> RequestAndroidPermission()
    {
        var status = await Permissions.CheckStatusAsync<Permissions.PostNotifications>();

        if (status != PermissionStatus.Granted)
        {
            status = await Permissions.RequestAsync<Permissions.PostNotifications>();
        }

        return status == PermissionStatus.Granted;
    }

    private async Task<bool> RequestiOSPermission()
    {
#if IOS
        var settings = await UserNotifications.UNUserNotificationCenter.Current
            .GetNotificationSettingsAsync();

        if (settings.AuthorizationStatus ==
            UserNotifications.UNAuthorizationStatus.NotDetermined)
        {
            var (granted, error) = await UserNotifications.UNUserNotificationCenter.Current
                .RequestAuthorizationAsync(
                    UserNotifications.UNAuthorizationOptions.Alert |
                    UserNotifications.UNAuthorizationOptions.Badge |
                    UserNotifications.UNAuthorizationOptions.Sound);

            return granted;
        }

        return settings.AuthorizationStatus ==
            UserNotifications.UNAuthorizationStatus.Authorized;
#else
        return false;
#endif
    }
}

权限请求最佳实践

这里分享几条实战经验:

  • 选好时机:别在应用一启动就弹权限框,等用户做了某个需要通知的操作再请求
  • 说清楚为什么:请求前先告诉用户开启通知能得到什么好处
  • 优雅降级:用户拒绝了?应用照样能用,只是少了通知功能而已
  • 提供出口:如果用户后来改主意了,给个入口引导他去系统设置里开启
  • 发之前先查:每次发通知前,都检查一下权限状态

富媒体通知:图片、按钮和交互

纯文字的通知看多了容易被忽略。好在现在的移动系统都支持富媒体通知——可以放图片、视频,甚至加交互按钮。我在实际项目中发现,加了图片的通知点击率能提高不少。

Android大图样式通知

public async Task SendBigPictureNotification()
{
    var notification = new NotificationRequest
    {
        NotificationId = 5001,
        Title = "新品上架",
        Description = "全新iPhone 16系列现已开售,点击查看详情",
        Android = new AndroidOptions
        {
            ChannelId = "promotional_channel",
            Priority = AndroidPriority.High,
            StyleType = AndroidNotificationStyle.BigPicture,
            BigPicture = "https://example.com/images/promo.jpg",
            AutoCancel = true
        }
    };

    await LocalNotificationCenter.Current.Show(notification);
}

通知操作按钮

给通知加上操作按钮,用户不用打开App就能直接操作。比如好友请求这种场景,直接在通知上"接受"或"拒绝":

public async Task SendNotificationWithActions()
{
    var notification = new NotificationRequest
    {
        NotificationId = 5004,
        Title = "新好友请求",
        Description = "李明想要添加您为好友",
        CategoryType = NotificationCategoryType.Social,
        Android = new AndroidOptions
        {
            ChannelId = "social_channel",
            Priority = AndroidPriority.High,
            AutoCancel = true
        }
    };

    notification.Android.Actions = new List<AndroidAction>
    {
        new AndroidAction { ActionId = 1, Title = "接受", Icon = "accept_icon" },
        new AndroidAction { ActionId = 2, Title = "拒绝", Icon = "decline_icon" }
    };

    await LocalNotificationCenter.Current.Show(notification);
}

// 处理按钮点击事件
public void InitializeNotificationActions()
{
    LocalNotificationCenter.Current.OnNotificationActionTapped += (e) =>
    {
        if (e.ActionId == 1)
        {
            // 处理"接受"操作
        }
        else if (e.ActionId == 2)
        {
            // 处理"拒绝"操作
        }
    };
}

深度链接:从通知直达应用指定页面

用户点了通知,结果打开的是应用首页——这体验可就差了。深度链接就是解决这个问题的,它能把用户直接带到应用的特定页面。

在通知中使用深度链接

public async Task SendDeepLinkNotification()
{
    var notification = new NotificationRequest
    {
        NotificationId = 6001,
        Title = "您的订单已送达",
        Description = "请确认收货并评价商品",
        ReturningData = JsonSerializer.Serialize(new
        {
            action = "order_detail",
            orderId = "20240209001"
        }),
        Android = new AndroidOptions
        {
            ChannelId = "order_channel",
            Priority = AndroidPriority.High,
            AutoCancel = true
        }
    };

    await LocalNotificationCenter.Current.Show(notification);
}

处理深度链接导航

收到通知后根据数据做路由跳转:

public partial class App : Application
{
    public App()
    {
        InitializeComponent();
        SetupNotificationHandlers();
        MainPage = new AppShell();
    }

    private void SetupNotificationHandlers()
    {
        LocalNotificationCenter.Current.OnNotificationReceived += OnNotificationReceived;
    }

    private void OnNotificationReceived(NotificationEventArgs e)
    {
        if (!string.IsNullOrEmpty(e.Request.ReturningData))
        {
            var data = JsonSerializer.Deserialize<NotificationData>(
                e.Request.ReturningData);

            MainThread.BeginInvokeOnMainThread(async () =>
            {
                switch (data.Action)
                {
                    case "order_detail":
                        await Shell.Current.GoToAsync(
                            $"//orders/detail?id={data.OrderId}");
                        break;
                    case "chat_message":
                        await Shell.Current.GoToAsync(
                            $"//chat?userId={data.UserId}");
                        break;
                    default:
                        await Shell.Current.GoToAsync("//home");
                        break;
                }
            });
        }
    }
}

public class NotificationData
{
    public string Action { get; set; }
    public string OrderId { get; set; }
    public string UserId { get; set; }
}

通知设计最佳实践与用户体验优化

技术实现搞定了,但好的通知系统不只是"能发出去"就行——用户体验才是关键。下面这些实践,是我从踩坑中总结出来的。

通知频率控制

没人喜欢被通知轰炸。做个简单的节流机制很有必要:

public class NotificationThrottleService
{
    private readonly Dictionary<string, DateTime> _lastNotificationTimes = new();
    private readonly TimeSpan _minimumInterval = TimeSpan.FromMinutes(5);

    public bool CanSendNotification(string notificationType)
    {
        if (!_lastNotificationTimes.ContainsKey(notificationType))
            return true;

        var timeSinceLastNotification = DateTime.Now - _lastNotificationTimes[notificationType];
        return timeSinceLastNotification >= _minimumInterval;
    }

    public void RecordNotificationSent(string notificationType)
    {
        _lastNotificationTimes[notificationType] = DateTime.Now;
    }
}

用户偏好设置

给用户提供细粒度的通知偏好控制——包括免打扰时间段,这个真的很重要:

public class NotificationPreferences
{
    public bool EnablePushNotifications { get; set; } = true;
    public bool EnablePromotionalNotifications { get; set; } = true;
    public bool EnableOrderUpdates { get; set; } = true;
    public bool EnableSoundAndVibration { get; set; } = true;

    // 免打扰时间段
    public TimeSpan QuietHoursStart { get; set; } = new TimeSpan(22, 0, 0);
    public TimeSpan QuietHoursEnd { get; set; } = new TimeSpan(8, 0, 0);

    public bool IsInQuietHours()
    {
        var now = DateTime.Now.TimeOfDay;
        if (QuietHoursStart < QuietHoursEnd)
            return now >= QuietHoursStart && now <= QuietHoursEnd;
        else
            return now >= QuietHoursStart || now <= QuietHoursEnd;
    }

    public void SavePreferences()
    {
        var json = JsonSerializer.Serialize(this);
        Preferences.Default.Set("notification_preferences", json);
    }

    public static NotificationPreferences LoadPreferences()
    {
        var json = Preferences.Default.Get("notification_preferences", string.Empty);
        if (string.IsNullOrEmpty(json))
            return new NotificationPreferences();
        return JsonSerializer.Deserialize<NotificationPreferences>(json)
            ?? new NotificationPreferences();
    }
}

智能通知合并

当有多条类似的通知时,合并成一条显示,别让用户的通知栏变成灾难现场:

public class NotificationGroupingService
{
    public async Task SendGroupedNotifications(List<Message> messages)
    {
        if (messages.Count == 1)
        {
            var notification = new NotificationRequest
            {
                NotificationId = 7000,
                Title = messages[0].SenderName,
                Description = messages[0].Content,
                Group = "messages_group"
            };
            await LocalNotificationCenter.Current.Show(notification);
        }
        else
        {
            var notification = new NotificationRequest
            {
                NotificationId = 7000,
                Title = "新消息",
                Description = $"您有{messages.Count}条新消息",
                Group = "messages_group",
                IsGroupSummary = true,
                Android = new AndroidOptions
                {
                    ChannelId = "messages_channel",
                    StyleType = AndroidNotificationStyle.Inbox,
                    InboxList = messages.Select(
                        m => $"{m.SenderName}: {m.Content}").ToList()
                }
            };
            await LocalNotificationCenter.Current.Show(notification);
        }
    }
}

总结

到这里,我们基本上把.NET MAUI中通知系统的各个方面都过了一遍——从最基础的本地通知,到FCM推送、APNs配置,再到Azure Notification Hubs的企业级方案。内容确实不少,但每一块在实际开发中都会用到。

快速回顾一下核心要点:

  • 本地通知用Plugin.LocalNotification就够了,定时提醒、闹钟这些场景轻松搞定
  • Android通知渠道在8.0以上是必须配的,不配通知就出不来
  • FCM是Android和iOS推送通知的标准方案,大部分项目用它就行
  • Azure Notification Hubs更适合企业级大规模推送场景
  • 权限管理别急着弹框,选好时机才能提高授权率
  • 富媒体和深度链接能明显提升用户体验和点击转化
  • 频率控制和用户偏好是留住用户的关键,别把通知做成骚扰

最后说一句——通知是把双刃剑。用好了是提升留存的利器,用差了就是卸载的推手。希望这篇指南能帮到你,祝开发顺利!

关于作者 Editorial Team

Our team of expert writers and editors.