相关文章推荐
慷慨大方的番茄  ·  java ...·  1 周前    · 
欢快的领带  ·  Winscp使用密钥登录 - ...·  1 周前    · 
谈吐大方的火车  ·  Python selenium — ...·  1 年前    · 
博学的烤土司  ·  【新时代 新作为 ...·  1 年前    · 
奔放的萝卜  ·  javascript - ...·  1 年前    · 

本教程将使用 Azure 通知中心 将推送通知发送到适用于 Android 和 iOS 的 Flutter 应用程序 。

使用 ASP.NET Core Web API 后端,通过最新且最理想的 安装 方法来处理客户端的 设备注册 。 服务还将以跨平台的方式发送推送通知。

借助 用于后端操作的通知中心 SDK 处理这些操作。 从应用后端注册 文档中提供了整体方法的更多详细信息。

本教程将指导你完成以下步骤:

  • 设置推送通知服务和 Azure 通知中心。
  • 创建 ASP.NET Core Web API 后端应用程序。
  • 创建跨平台 Flutter 应用程序。
  • 针对推送通知配置本地 Android 项目。
  • 针对推送通知配置本地 iOS 项目。
  • 测试解决方案。
  • 若要继续操作,需要:

  • Azure 订阅 ,可以在其中创建和管理资源。
  • Flutter 工具包(及其必备组件)。
  • 已安装 Flutter 和 Dart 插件 Visual Studio Code
  • 已安装 CocoaPods ,用于管理库依赖项。
  • 在 Android(物理设备或仿真器设备)或 iOS(仅物理设备)上运行应用的功能。
  • 对于 Android 而言,必须具有:

  • 开发人员解锁的物理设备或仿真器(运行安装了 Google Play Services 的 API 26 及更高版本)。
  • 对于 iOS 而言,必须具有:

  • 有效的 Apple 开发人员帐户
  • 注册到开发人员帐户 的物理 iOS 设备 , (运行 iOS 13.0 及更高版本)
  • 密钥链 中安装的 .p12 开发证书 允许 在物理设备上运行应用
  • IOS 模拟器不支持远程通知,因此在 iOS 上浏览此示例时需要使用物理设备。 但是,无需在 Android 和 iOS 上都运行应用以完成本教程。

    可以遵循此首要原则示例中的步骤,而不需要事先体验。 不过,如能熟悉以下方面则会更有帮助。

  • Azure 开发人员门户
  • ASP.NET Core
  • Google Firebase 控制台
  • Microsoft Azure 以及 使用 Azure 通知中心向 iOS 应用发送推送通知
  • 用于跨平台开发的 Flutter Dart
  • 用于 Android 和 iOS 本机开发的 Kotlin Swift
  • 提供的步骤特定于 macOS 。 通过跳过 iOS 方面,可以在 Windows 上继续操作。

    设置推送通知服务和 Azure 通知中心

    在此部分,将设置 Firebase Cloud Messaging (FCM) Apple Push Notification Services (APNS) 然后,创建并配置通知中心来处理这些服务。

    创建 Firebase 项目并为 Android 启用 Firebase Cloud Messaging

  • 登录到 Firebase 控制台 。 创建一个新的 Firebase 项目,输入 PushDemo 作为项目名称 。

    将生成唯一名称。 默认情况下,此名称由提供的名称小写变体和用短划线分隔的生成数字组成。 如果需要可以对其进行更改,但前提是它仍是全局唯一名称。

  • 创建项目后,选择“向 Android 应用添加 Firebase”。

  • 在“将 Firebase 添加到 Android 应用”页上,执行以下步骤。

  • 对于“Android 包名称”,请输入包的名称。 例如: com.<organization_identifier>.<package_name>

  • 选择“注册应用”。

  • 选择“下载 google-services.json”。 然后将该文件保存到本地文件夹中供稍后使用,并选择“下一步”。

  • 选择“ 下一页 ”。

  • 选择“继续访问控制台”

    如果由于“验证安装”检查未启用“继续访问控制台”按钮,请选择“跳过此步骤”。

    若要将推送通知发送到 iOS 应用,请向 Apple 注册应用程序,还要注册推送通知。

  • 如果尚未注册应用,请浏览到 Apple 开发人员中心的 iOS 预配门户 。 使用 Apple ID 登录到门户,导航到 “证书”和“标识符 & 配置文件 ”,然后选择“ 标识符 ”。 单击“ + ”注册新应用。

  • 在“注册新的标识符” 屏幕上,选择“应用 ID” 单选按钮。 然后选择“继续”。

  • 更新新应用的以下三个值,然后选择“Continue”(继续):

  • 说明 :键入应用的描述性名称。

  • 捆绑 ID :输入 com.organization_identifier<>格式的捆绑 ID。<>product_name ,如应用分发指南 中所述。 在下面的屏幕截图中, mobcat 值用作组织标识符,PushDemo 值用作产品名称。

  • 推送通知 :在“Capabilities”(功能)部分选中“Push Notifications”(推送通知)选项 。

    此操作会生成应用 ID 并请求你确认信息。 选择“Continue”(继续),然后选择“Register”(注册) 以确认新的应用 ID。

    选择“注册”后,新的应用 ID 将作为行项出现在“证书、标识符和配置文件” & 页中。

  • “证书、标识符配置文件”页的& “标识符” 下,找到创建的“应用 ID”行项。 然后,选择其行以显示“编辑应用 ID 配置”屏幕。

    为通知中心创建证书

    若要使通知中心能够使用 Apple Push Notification 服务 (APNS),需要使用证书,可以通过以下两种方式之一提供证书:

  • 创建可以直接上传到通知中心的 p12 推送证书 (原始方法)

  • 创建可用于基于令牌的身份验证的 p8 证书 (推荐的较新方法)

    基于令牌 (HTTP/2) 的 APNS 身份验证 中所述,较新的方法有很多好处。 所需的步骤较少,但一些步骤对于特定的场景来说也是必需的。 不过,这两种方法的步骤均已提供,因为在本教程中,这两种方法均适用。

    选项 1:创建可以直接上传到通知中心的 p12 推送证书
  • 在 Mac 上,运行 Keychain Access 工具。 可以从启动台上的“Utilities”或“Other”文件夹中打开该工具。

  • 选择“Keychain Access”,展开“Certificate Assistant”(证书助理),然后选择“Request a Certificate from a Certificate Authority”(从证书颁发机构请求证书)。

    默认情况下,Keychain Access 选择列表中的第一项。 如果你位于“Certificates”(证书)类别中,并且“Apple Worldwide Developer Relations Certification Authority”(Apple 全球开发者关系证书颁发机构)不是列表中的第一项,这可能会是个问题。 在生成 CSR(证书签名请求)之前,请确保已有非密钥项,或者已选择“Apple Worldwide Developer Relations Certification Authority”(Apple 全球开发者关系证书颁发机构)密钥。

  • 选择“User Email Address”(用户电子邮件地址),输入“Common Name”(公用名)值,确保指定“Saved to disk”(保存到磁盘),然后选择“Continue”(继续)。 将“CA Email Address”(CA 电子邮件地址)留空,因为它不是必填字段 。

  • 在“另存为”中为证书签名请求 (CSR) 文件输入一个名称,在“位置”中选择一个位置,并选择“保存” 。

    此操作会将 CSR 文件保存到选定位置。 默认位置为“桌面”。 请记住为此文件选择的位置。

  • 返回到 iOS 预配门户中 的&“证书”“标识符配置文件 ”页,向下滚动到选中的 “推送通知 ”选项,然后选择“ 配置 ”以创建证书。

  • 此时将显示“Apple Push Notification 服务 TLS/SSL 证书”窗口。 选择“开发 TLS/SSL 证书”部分下的“创建证书”按钮 。

    此时将显示“Create a new Certificate”(创建新证书) 屏幕。

    本教程使用开发证书。 注册生产证书时使用相同的过程。 只需确保在发送通知时使用相同的证书类型。

  • 选择“选择文件”,浏览到保存 CSR 文件的位置,然后双击证书名以加载该证书 。 然后选择“继续”。

  • 当门户创建证书后,请选择“Download”(下载)按钮。 保存证书,并记住保存证书的位置。

    这将下载证书并将其保存到计算机的 Downloads 文件夹。

    默认情况下,下载的开发证书名为 aps_development.cer

  • 双击下载的推送证书 aps_development.cer 。 此操作将在密钥链中安装新证书,如下图所示:

    证书中的名称可能有所不同,但会以 Apple Development iOS Push Services 作为前缀,并且关联有相应捆绑标识符。

  • 在 Keychain Access 中,按住 Control 的同时单击在“证书”类别中创建的新推送证书 。 选择“导出”,为文件命名,选择“p12”格式,并选择“保存”。

    可以选择使用密码保护证书,但这是可选的。 如果要跳过密码创建,请单击“OK”(确定)。 记下导出的 p12 证书的文件名和位置。 它们用于启用 APNS 身份验证。

    你的 p12 文件名和位置可能不同于本教程中所示的名称和位置。

    选项 2:创建可用于基于令牌的身份验证的 p8 证书
  • 请记下以下详细信息:

  • 应用 ID 前缀(团队 ID )
  • 捆绑包 ID
  • 返回到“证书、标识符和配置文件”,单击“密钥”。

    如果已为 APNS 配置了密钥,则可以重复使用在创建后立即下载的 p8 证书。 如果是这样,则可以忽略步骤 3 到步骤 5 。

  • 单击 + 按钮(或“创建密钥”按钮)以创建新密钥。

  • 提供合适的“密钥名称”值,选中“Apple Push Notification 服务(APNS)”选项,然后单击“继续”,接下来在下一个屏幕上单击“注册”。

  • 单击“下载”,然后将 p8 文件(前缀为 AuthKey_)移动到安全的本地目录,然后单击“完成”。

    请确保将 p8 文件保存在安全的位置(并保存备份)。 密钥在下载后无法重新下载,因为服务器副本已删除。

  • 在“密钥”上,单击创建的密钥(如果已选择使用现有密钥,则改为单击现有密钥)。

  • 记下“密钥 ID”值。

  • 在所选的合适应用程序(如 Visual Studio Code )中打开 p8 证书。 记下密钥值(位于-----BEGIN PRIVATE KEY----- 和 -----END PRIVATE KEY----- 之间) 。

    -----BEGIN PRIVATE KEY-----
    <key_value>
    -----END PRIVATE KEY-----

    这是稍后将用于配置通知中心的令牌值 。

    完成这些步骤后,你应具有稍后要在 使用 APNS 信息配置通知中心 中使用的以下信息:

  • 团队 ID(请参阅步骤 1)
  • 捆绑包 ID (请参阅步骤 1)
  • 密钥 ID(请参阅步骤 7)
  • 令牌值(在步骤 8 中获取的 p8 密钥值)
  • 为应用程序创建配置文件

  • 返回到 iOS 预配门户 ,选择“Certificates, Identifiers & Profiles”(证书、标识符和配置文件),从左侧菜单中选择“Profiles”(配置文件),然后选择 + 创建新的配置文件。 此时将显示“Register a New Provisioning Profile”(注册新的预配配置文件) 屏幕。

  • 选择“Development”(开发)下的“iOS App Development”(iOS 应用程序开发)作为预配配置文件类型,然后选择“Continue”(继续)。

  • 接下来,从“App ID”(应用 ID)下拉列表中选择创建的应用 ID,然后选择“Continue”(继续)。

  • 在“Select certificates”(选择证书)窗口中,选择用于代码签名的开发证书,然后选择“Continue”(继续)。

    此证书不是在 上一步 中创建的推送证书。 这是开发证书。 如果没有开发证书,则必须创建一个,因为这是本教程的 先决条件 。 开发人员证书可以在 Apple 开发人员门户 中通过 Xcode 创建,或在 Visual Studio 中创建。

  • 返回到“Certificates, Identifiers & Profiles”(证书、标识符和配置文件)页,从左侧菜单中选择“Profiles”(配置文件),然后选择 + 创建新的配置文件。 此时将显示“Register a New Provisioning Profile”(注册新的预配配置文件) 屏幕。

  • 在“选择证书”窗口中,选择你创建的开发证书。 然后选择“继续”。

  • 接下来,选择用于测试的设备,然后选择“Continue”(继续)。

  • 最后,在“Provisioning Profile Name”(预配配置文件名称)中为概要文件选择一个名称,然后选择“Generate”(生成)。

  • 创建了新的预配配置文件后,选择“Download”(下载) 。 记住保存证书的位置。

  • 浏览到预配配置文件所在的位置,然后双击该配置文件以将其安装在开发计算机上。

    创建通知中心

    本部分将创建一个通知中心,并使用 APNS 配置身份验证。 可以使用 p12 推送证书或基于令牌的身份验证。 如果想要使用已创建的通知中心,可以跳到步骤 5。

  • 登录到 Azure

  • 单击“创建资源”,搜索并选择“通知中心”,然后单击“创建” 。

  • 更新以下字段,然后单击“创建”:

    基本详细信息

    订阅: 从下拉列表中选择目标订阅
    资源组: 新建资源组(或选择现有资源组)

    命名空间详细信息

    通知中心命名空间: 输入通知中心命名空间的全局唯一名称

    确保为此字段选择“新建”选项。

    通知中心详细信息

    通知中心: 输入通知中心的名称
    位置: 从下拉列表中选择合适的位置
    定价层: 保留默认“免费”选项

    除非已达到免费层的最大中心数。

  • 预配通知中心后,导航到该资源。

  • 导航到新的通知中心。

  • 从列表中选择“访问策略”(在“管理”下) 。

  • 请记下策略名称值及其对应的连接字符串值 。

    使用 APNS 信息配置通知中心

    在“Notification Services”下,选择“Apple”,然后根据以前在 为通知中心创建证书 部分中选择的方法,执行相应的步骤。

    仅当希望将推送通知发送给已从应用商店购买应用的用户时,才应当对“应用程序模式”使用“生产”。

    选项 1:使用 .p12 推送证书

  • 选择“证书”。

  • 选择文件图标。

  • 选择前面导出的 .p12 文件,然后选择“Open”(打开)。

  • 如有必要,请指定正确的密码。

  • 选择“沙盒”模式。

  • 选择“保存”。

    选项 2:使用基于令牌的身份验证

  • 选择“令牌”。

  • 输入前面获取的以下值:

  • 密钥 ID
  • 捆绑包 ID
  • 团队 ID
  • 选择“沙盒”。

  • 选择“保存”。

    使用 FCM 信息配置通知中心

  • 在左侧菜单的“设置”部分中选择“Google (GCM/FCM)” 。
  • 输入先前从 Google Firebase Console 记下的服务器密钥 。
  • 在工具栏上选择“保存”。
  • 创建 ASP.NET Core Web API 后端应用程序

    在本部分中,将创建 ASP.NET Core Web API 后端来进行 设备注册 并将通知发送到 Flutter 移动应用。

    创建 Web 项目

  • 在 Visual Studio 中,选择“文件”>“新建解决方案” 。

  • 依次选择“.NET Core”>“应用”>“ASP.NET Core”>“API”>“下一步” 。

  • 在“配置新的 ASP.NET Core Web API”对话框中,选择 .NET Core 3.1 作为“目标框架” 。

  • 输入“PushDemoApi”作为项目名称,然后选择“创建” 。

  • 启动调试 (Command + Enter) 来测试模板化应用 。

    模板化应用配置为使用 WeatherForecastController 作为 launchUrl。 这是在“属性”>“launchSettings.json”中设置的 。

    如果系统提示“找到的开发证书无效”消息:

  • 单击“是”同意运行“dotnet dev-certs https”工具来解决此问题。 然后“dotnet dev-certs https”工具会提示你输入证书的密码和密钥链的密码。

  • 当系统提示“安装并信任新证书”时,单击“是”,然后输入密钥链的密码 。

  • 删除 WeatherForecast.cs。

  • 使用 机密管理器工具 设置本地配置值。 将机密与解决方案分离可确保它们不会终止在源代码管理中。 打开“终端”,然后转到项目文件的目录,并运行以下命令:

    dotnet user-secrets init
    dotnet user-secrets set "NotificationHub:Name" <value>
    dotnet user-secrets set "NotificationHub:ConnectionString" <value>
    

    将占位符值替换为自己的通知中心名称和连接字符串值。 你已在创建通知中心部分中记下了这些值。 否则,可以在 Azure 中查找这些值。

    NotificationHub:Name
    请参阅“概述”顶部“基础”摘要中的“名称” 。

    NotificationHub:ConnectionString
    请参阅“访问策略”中的 DefaultFullSharedAccessSignature

    对于生产方案,可以查看 Azure KeyVault 等选项,以安全地存储连接字符串。 为简单起见,机密会添加到 Azure 应用服务应用程序设置。

    使用 API 密钥对客户端进行身份验证(可选)

    API 密钥的安全性虽然不如令牌,但它也可以满足本教程的需要。 可通过 ASP.NET 中间件轻松配置 API 密钥。

  • 将“API 密钥”添加到本地配置值。

    dotnet user-secrets set "Authentication:ApiKey" <value>
    

    应将占位符值替换为你自己的值,并对其进行记录。

  • 按住 Control 的同时单击 PushDemoApi 项目,从“添加”菜单中选择“新建文件夹”,然后单击“添加”,并使用“身份验证”作为文件夹名称 。

  • 按住 Control 的同时单击“身份验证”文件夹,然后从“添加”菜单中选择“新建文件...” 。

  • 选择“常规”>“空类”,输入“ApiKeyAuthOptions.cs”作为名称,然后单击“新建”添加以下实现 。

    using Microsoft.AspNetCore.Authentication;
    namespace PushDemoApi.Authentication
        public class ApiKeyAuthOptions : AuthenticationSchemeOptions
            public const string DefaultScheme = "ApiKey";
            public string Scheme => DefaultScheme;
            public string ApiKey { get; set; }
    
  • 将另一个“空类”添加到名为 ApiKeyAuthHandler.cs 的“身份验证”文件夹中,然后添加以下实现。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;
    using System.Text.Encodings.Web;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Authentication;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Options;
    namespace PushDemoApi.Authentication
        public class ApiKeyAuthHandler : AuthenticationHandler<ApiKeyAuthOptions>
            const string ApiKeyIdentifier = "apikey";
            public ApiKeyAuthHandler(
                IOptionsMonitor<ApiKeyAuthOptions> options,
                ILoggerFactory logger,
                UrlEncoder encoder,
                ISystemClock clock)
                : base(options, logger, encoder, clock) {}
            protected override Task<AuthenticateResult> HandleAuthenticateAsync()
                string key = string.Empty;
                if (Request.Headers[ApiKeyIdentifier].Any())
                    key = Request.Headers[ApiKeyIdentifier].FirstOrDefault();
                else if (Request.Query.ContainsKey(ApiKeyIdentifier))
                    if (Request.Query.TryGetValue(ApiKeyIdentifier, out var queryKey))
                        key = queryKey;
                if (string.IsNullOrWhiteSpace(key))
                    return Task.FromResult(AuthenticateResult.Fail("No api key provided"));
                if (!string.Equals(key, Options.ApiKey, StringComparison.Ordinal))
                    return Task.FromResult(AuthenticateResult.Fail("Invalid api key."));
                var identities = new List<ClaimsIdentity> {
                    new ClaimsIdentity("ApiKeyIdentity")
                var ticket = new AuthenticationTicket(
                    new ClaimsPrincipal(identities), Options.Scheme);
                return Task.FromResult(AuthenticateResult.Success(ticket));
    

    身份验证处理程序是实现方案行为的类型,在本例中为自定义 API 密钥方案。

  • 将另一个“空类”添加到名为 ApiKeyAuthenticationBuilderExtensions.cs 的“身份验证”文件夹中,然后添加以下实现。

    using System;
    using Microsoft.AspNetCore.Authentication;
    namespace PushDemoApi.Authentication
        public static class AuthenticationBuilderExtensions
            public static AuthenticationBuilder AddApiKeyAuth(
                this AuthenticationBuilder builder,
                Action<ApiKeyAuthOptions> configureOptions)
                return builder
                    .AddScheme<ApiKeyAuthOptions, ApiKeyAuthHandler>(
                        ApiKeyAuthOptions.DefaultScheme,
                        configureOptions);
    

    此扩展方法简化了 Startup.cs 中的中间件配置代码,使其更具有可读性并且更加通俗易懂。

  • 在 Startup.cs 中,更新 ConfigureServices 方法,以便在对 services.AddControllers 方法的调用下配置 API 密钥身份验证 。

    using PushDemoApi.Authentication;
    using PushDemoApi.Models;
    using PushDemoApi.Services;
    public void ConfigureServices(IServiceCollection services)
        services.AddControllers();
        services.AddAuthentication(options =>
            options.DefaultAuthenticateScheme = ApiKeyAuthOptions.DefaultScheme;
            options.DefaultChallengeScheme = ApiKeyAuthOptions.DefaultScheme;
        }).AddApiKeyAuth(Configuration.GetSection("Authentication").Bind);
    
  • 继续在 Startup.cs 中更新 Configure 方法,以便对应用的 IApplicationBuilder 调用 UseAuthentication 和 UseAuthorization扩展方法 。 请确保在 UseRouting 之后且 app.UseEndpoints 之前调用这些方法 。

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        if (env.IsDevelopment())
            app.UseDeveloperExceptionPage();
        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
            endpoints.MapControllers();
    

    调用 UseAuthentication 将注册中间件,该中间件使用以前注册的身份验证方案(来自 ConfigureServices) 。 必须在依赖于要进行身份验证的用户的任何中间件之前,调用此方法。

    添加依赖项并配置服务

    ASP.NET Core 支持依赖项注入 (DI) 软件设计模式,这是一种在类与其依赖项之间实现控制反转 (IoC) 的方法。

    使用的通知中心和用于后端操作的通知中心 SDK 封装在服务中。 该服务通过适当的抽象进行注册和提供。

  • 按住 Control 的同时单击“依赖项”文件夹,然后选择“管理 NuGet 包...” 。

  • 搜索 Microsoft.Azure.NotificationHubs 并确保选中它。

  • 单击“添加包”,然后在系统提示接受许可条款时单击“接受” 。

  • 按住 Control 的同时单击 PushDemoApi 项目,从“添加”菜单中选择“新建文件夹”,然后单击“添加”并使用“模型”作为文件夹名称 。

  • 按住 Control 的同时单击“模型”文件夹,然后从“添加”菜单中选择“新建文件...” 。

  • 选择“常规”>“空类”,输入 PushTemplates.cs 作为名称,然后单击“新建”添加以下实现 。

    namespace PushDemoApi.Models
        public class PushTemplates
            public class Generic
                public const string Android = "{ \"notification\": { \"title\" : \"PushDemo\", \"body\" : \"$(alertMessage)\"}, \"data\" : { \"action\" : \"$(alertAction)\" } }";
                public const string iOS = "{ \"aps\" : {\"alert\" : \"$(alertMessage)\"}, \"action\" : \"$(alertAction)\" }";
            public class Silent
                public const string Android = "{ \"data\" : {\"message\" : \"$(alertMessage)\", \"action\" : \"$(alertAction)\"} }";
                public const string iOS = "{ \"aps\" : {\"content-available\" : 1, \"apns-priority\": 5, \"sound\" : \"\", \"badge\" : 0}, \"message\" : \"$(alertMessage)\", \"action\" : \"$(alertAction)\" }";
    

    此类包含此方案所需的泛型和无提示通知的标记化通知有效负载。 有效负载在安装外定义,以便无需通过服务更新现有安装即可进行试验。 以这种方式处理对安装的更改超出了本教程的范围。 对于生产环境,请考虑使用自定义模板

  • 将另一个“空类”添加到名为 DeviceInstallation.cs 的“模型”文件夹中,然后添加以下实现。

    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    namespace PushDemoApi.Models
        public class DeviceInstallation
            [Required]
            public string InstallationId { get; set; }
            [Required]
            public string Platform { get; set; }
            [Required]
            public string PushChannel { get; set; }
            public IList<string> Tags { get; set; } = Array.Empty<string>();
    
  • 将另一个“空类”添加到名为 NotificationRequest.cs 的“模型”文件夹中,然后添加以下实现。

    using System;
    namespace PushDemoApi.Models
        public class NotificationRequest
            public string Text { get; set; }
            public string Action { get; set; }
            public string[] Tags { get; set; } = Array.Empty<string>();
            public bool Silent { get; set; }
    
  • 将另一个“空类”添加到名为 NotificationHubOptions.cs 的“模型”文件夹中,然后添加以下实现 。

    using System.ComponentModel.DataAnnotations;
    namespace PushDemoApi.Models
        public class NotificationHubOptions
            [Required]
            public string Name { get; set; }
            [Required]
            public string ConnectionString { get; set; }
    
  • 将一个新文件夹添加到名为“服务”的 PushDemoApi 项目。

  • 将“空接口”添加到名为 INotificationService.cs 的“服务”文件夹,然后添加以下实现 。

    using System.Threading;
    using System.Threading.Tasks;
    using PushDemoApi.Models;
    namespace PushDemoApi.Services
        public interface INotificationService
            Task<bool> CreateOrUpdateInstallationAsync(DeviceInstallation deviceInstallation, CancellationToken token);
            Task<bool> DeleteInstallationByIdAsync(string installationId, CancellationToken token);
            Task<bool> RequestNotificationAsync(NotificationRequest notificationRequest, CancellationToken token);
    
  • 将“空类”添加到名为 NotificationHubsService.cs 的“服务”文件夹中,然后添加以下代码以实现 INotificationService 接口 :

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.Azure.NotificationHubs;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Options;
    using PushDemoApi.Models;
    namespace PushDemoApi.Services
        public class NotificationHubService : INotificationService
            readonly NotificationHubClient _hub;
            readonly Dictionary<string, NotificationPlatform> _installationPlatform;
            readonly ILogger<NotificationHubService> _logger;
            public NotificationHubService(IOptions<NotificationHubOptions> options, ILogger<NotificationHubService> logger)
                _logger = logger;
                _hub = NotificationHubClient.CreateClientFromConnectionString(
                    options.Value.ConnectionString,
                    options.Value.Name);
                _installationPlatform = new Dictionary<string, NotificationPlatform>
                    { nameof(NotificationPlatform.Apns).ToLower(), NotificationPlatform.Apns },
                    { nameof(NotificationPlatform.Fcm).ToLower(), NotificationPlatform.Fcm }
            public async Task<bool> CreateOrUpdateInstallationAsync(DeviceInstallation deviceInstallation, CancellationToken token)
                if (string.IsNullOrWhiteSpace(deviceInstallation?.InstallationId) ||
                    string.IsNullOrWhiteSpace(deviceInstallation?.Platform) ||
                    string.IsNullOrWhiteSpace(deviceInstallation?.PushChannel))
                    return false;
                var installation = new Installation()
                    InstallationId = deviceInstallation.InstallationId,
                    PushChannel = deviceInstallation.PushChannel,
                    Tags = deviceInstallation.Tags
                if (_installationPlatform.TryGetValue(deviceInstallation.Platform, out var platform))
                    installation.Platform = platform;
                    return false;
                    await _hub.CreateOrUpdateInstallationAsync(installation, token);
                catch
                    return false;
                return true;
            public async Task<bool> DeleteInstallationByIdAsync(string installationId, CancellationToken token)
                if (string.IsNullOrWhiteSpace(installationId))
                    return false;
                    await _hub.DeleteInstallationAsync(installationId, token);
                catch
                    return false;
                return true;
            public async Task<bool> RequestNotificationAsync(NotificationRequest notificationRequest, CancellationToken token)
                if ((notificationRequest.Silent &&
                    string.IsNullOrWhiteSpace(notificationRequest?.Action)) ||
                    (!notificationRequest.Silent &&
                    (string.IsNullOrWhiteSpace(notificationRequest?.Text)) ||
                    string.IsNullOrWhiteSpace(notificationRequest?.Action)))
                    return false;
                var androidPushTemplate = notificationRequest.Silent ?
                    PushTemplates.Silent.Android :
                    PushTemplates.Generic.Android;
                var iOSPushTemplate = notificationRequest.Silent ?
                    PushTemplates.Silent.iOS :
                    PushTemplates.Generic.iOS;
                var androidPayload = PrepareNotificationPayload(
                    androidPushTemplate,
                    notificationRequest.Text,
                    notificationRequest.Action);
                var iOSPayload = PrepareNotificationPayload(
                    iOSPushTemplate,
                    notificationRequest.Text,
                    notificationRequest.Action);
                    if (notificationRequest.Tags.Length == 0)
                        // This will broadcast to all users registered in the notification hub
                        await SendPlatformNotificationsAsync(androidPayload, iOSPayload, token);
                    else if (notificationRequest.Tags.Length <= 20)
                        await SendPlatformNotificationsAsync(androidPayload, iOSPayload, notificationRequest.Tags, token);
                        var notificationTasks = notificationRequest.Tags
                            .Select((value, index) => (value, index))
                            .GroupBy(g => g.index / 20, i => i.value)
                            .Select(tags => SendPlatformNotificationsAsync(androidPayload, iOSPayload, tags, token));
                        await Task.WhenAll(notificationTasks);
                    return true;
                catch (Exception e)
                    _logger.LogError(e, "Unexpected error sending notification");
                    return false;
            string PrepareNotificationPayload(string template, string text, string action) => template
                .Replace("$(alertMessage)", text, StringComparison.InvariantCulture)
                .Replace("$(alertAction)", action, StringComparison.InvariantCulture);
            Task SendPlatformNotificationsAsync(string androidPayload, string iOSPayload, CancellationToken token)
                var sendTasks = new Task[]
                    _hub.SendFcmNativeNotificationAsync(androidPayload, token),
                    _hub.SendAppleNativeNotificationAsync(iOSPayload, token)
                return Task.WhenAll(sendTasks);
            Task SendPlatformNotificationsAsync(string androidPayload, string iOSPayload, IEnumerable<string> tags, CancellationToken token)
                var sendTasks = new Task[]
                    _hub.SendFcmNativeNotificationAsync(androidPayload, tags, token),
                    _hub.SendAppleNativeNotificationAsync(iOSPayload, tags, token)
                return Task.WhenAll(sendTasks);
    

    提供给 SendTemplateNotificationAsync 的标记表达式限制为包含 20 个标记。 大多数运算符的限制数量为 6 个,但在本例中,表达式仅包含 OR (||)。 如果请求中的标记超过 20 个,则必须将它们拆分为多个请求。 有关更多详细信息,请参阅路由和标记表达式文档。

  • 在 Startup.cs 中,更新 ConfigureServices 方法,以将 NotificationHubsService 添加为 INotificationService 的单一实现 。

    using PushDemoApi.Models; using PushDemoApi.Services; public void ConfigureServices(IServiceCollection services) services.AddSingleton<INotificationService, NotificationHubService>(); services.AddOptions<NotificationHubOptions>() .Configure(Configuration.GetSection("NotificationHub").Bind) .ValidateDataAnnotations();

    创建通知 API

  • 按住 Control 的同时单击“控制器”文件夹,然后从“添加”菜单中选择“新建文件...” 。

  • 选择“ASP.NET Core”>“Web API 控制器类”,输入 NotificationsController 作为名称,然后单击“新建” 。

    如果使用的是 Visual Studio 2019,请选择“包含读/写操作的 API 控制器”模板。

  • 将以下命名空间添加到文件顶部。

    using System.ComponentModel.DataAnnotations;
    using System.Net;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Mvc;
    using PushDemoApi.Models;
    using PushDemoApi.Services;
    
  • 更新模板化控制器,使其从 ControllerBase 派生,并通过 ApiController 属性进行修饰 。

    [ApiController]
    [Route("api/[controller]")]
    public class NotificationsController : ControllerBase
        // Templated methods here
    

    Controller 基类提供对视图的支持,但在这种情况下不需要这样做,因此可以改用 ControllerBase。 如果使用的是 Visual Studio 2019,可跳过此步骤。

  • 如果选择完成使用 API 密钥对客户端进行身份验证部分,则还应该使用 Authorize 特性来修饰 NotificationsController 。

    [Authorize]
    
  • 更新构造函数以接受 INotificationServic 的注册实例作为参数,并将其分配给只读成员。

    readonly INotificationService _notificationService;
    public NotificationsController(INotificationService notificationService)
        _notificationService = notificationService;
    
  • 在“属性”文件夹) 的 launchSettings.json (中,将 launchUrlweatherforecast 更改为 api/notifications,以匹配 RegistrationsControllerRoute 属性中指定的 URL。

  • 启动调试 (Command + Enter),验证应用是否正在使用新的 NotificationsController 并返回“401 未授权”状态 。

    Visual Studio 可能不会在浏览器中自动启动该应用。 此时将使用 Postman 来测试 API。

  • 在新的 Postman 选项卡上,将请求设置为“GET” 。 输入以下地址,将占位符 <applicationUrl> 替换为属性>launchSettings.json 中的 https applicationUrl

    <applicationUrl>/api/notifications
    

    对于默认配置文件,applicationUrl 应为“https://localhost:5001”。 如果使用的是 IIS(Windows 上的 Visual Studio 2019 中的默认设置),则应改为使用 iisSettings 项中指定的 applicationUrl 。 如果地址不正确,你会收到 404 响应。

  • 如果选择完成使用 API 密钥对客户端进行身份验证部分,请确保将请求标头配置为包含 apikey 值。

    [Route("installations")] [ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType((int)HttpStatusCode.UnprocessableEntity)] public async Task<IActionResult> UpdateInstallation( [Required]DeviceInstallation deviceInstallation) var success = await _notificationService .CreateOrUpdateInstallationAsync(deviceInstallation, HttpContext.RequestAborted); if (!success) return new UnprocessableEntityResult(); return new OkResult(); [HttpDelete()] [Route("installations/{installationId}")] [ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType((int)HttpStatusCode.UnprocessableEntity)] public async Task<ActionResult> DeleteInstallation( [Required][FromRoute]string installationId) var success = await _notificationService .DeleteInstallationByIdAsync(installationId, CancellationToken.None); if (!success) return new UnprocessableEntityResult(); return new OkResult(); [HttpPost] [Route("requests")] [ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType((int)HttpStatusCode.UnprocessableEntity)] public async Task<IActionResult> RequestPush( [Required]NotificationRequest notificationRequest) if ((notificationRequest.Silent && string.IsNullOrWhiteSpace(notificationRequest?.Action)) || (!notificationRequest.Silent && string.IsNullOrWhiteSpace(notificationRequest?.Text))) return new BadRequestResult(); var success = await _notificationService .RequestNotificationAsync(notificationRequest, HttpContext.RequestAborted); if (!success) return new UnprocessableEntityResult(); return new OkResult();

    创建 API 应用

    现在,在 Azure 应用服务中创建 API 应用,以便托管后端服务。

  • 登录 Azure 门户

  • 单击“创建资源”,搜索并选择“API 应用”,然后单击“创建” 。

  • 更新以下字段,然后单击“创建”。

    应用名称:
    输入 API 应用的全局唯一名称

    选择在其中创建了通知中心的同一目标订阅。

    选中在其中创建了通知中心的同一资源组。

    应用服务计划/位置:
    创建新的应用服务计划

    将默认选项更改为包含 SSL 支持的计划。 否则,在使用移动应用时,需要执行适当的步骤,以防止 http 请求被阻止。

    Application Insights:
    保留建议的选项(将使用该名称创建新资源)或选择现有资源。

  • 预配 API 应用后,导航到该资源。

  • 记下“概述”顶部“基础信息”摘要中的 URL 属性 。 此 URL 是“后端终结点”,本教程后面的部分中会用到它。

    URL 使用前面指定的 API 应用名称,其格式为 https://<app_name>.azurewebsites.net

  • 从列表选择“配置”(在“设置”下) 。

  • 对于下面的每个设置,单击“新建应用程序设置”,输入名称和值,然后单击“确定” 。

    这些设置与你之前在用户设置中定义的设置相同。 应该可以复制将这些设置复制过来。 仅当你选择完成使用 API 密钥对客户端进行身份验证部分时,才需要 Authentication:ApiKey 设置。 对于生产方案,可以查看 Azure KeyVault 等选项。 在本例中,为简单起见,这些设置已作为应用程序设置添加。

  • 添加所有应用程序设置后,依次单击“保存”和“继续” 。

    发布后端服务

    接下来,需要将应用部署到 API 应用,以便可以从任意设备访问它。

    以下步骤特定于 Visual Studio for Mac。 如果你使用 Windows 上的 Visual Studio 2019,则发布流将有所不同。 请参阅发布到 Windows 上的 Azure 应用服务

  • 将配置从“调试”更改为“发布”(如果尚未这样做) 。

  • 按住 Control 的同时单击 PushDemoApi 项目,然后从“发布”菜单中选择“发布到 Azure...” 。

  • 如果系统提示进行身份验证,请遵循身份验证流。 使用之前在创建 API 应用部分中使用的帐户。

  • 从列表中选择之前创建的“Azure 应用服务 API 应用”作为发布目标,然后单击“发布” 。

    完成向导后,它会将应用发布到 Azure,然后打开该应用。 记下 URL(如果尚未这样做)。 此 URL 是“后端终结点”,本教程后面的部分中会用到它。

    验证已发布的 API

  • Postman 中打开新选项卡,将请求设置为“PUT”并输入下面的地址 。 将占位符替换为在之前的发布后端服务部分中记下的基址。

    https://<app_name>.azurewebsites.net/api/notifications/installations
    

    基址的格式应为 https://<app_name>.azurewebsites.net/

  • 如果选择完成使用 API 密钥对客户端进行身份验证部分,请确保将请求标头配置为包含 apikey 值。

    在本部分中,将构建一个 Flutter 移动应用程序,实施以跨平台的方式推送通知。

    它允许你通过创建的后端服务从通知中心注册和取消注册。

    当指定了一个操作,并且应用位于前台时,将显示一个警报。 否则,通知中心将显示通知。

    通常,会在应用程序生命周期中的相应时间点(或在首次运行体验过程中)执行注册(和取消注册)操作,无需进行显式用户注册/取消注册输入。 但是,此示例将需要显式用户输入,以便能够更轻松地探索和测试此功能。

    创建 Flutter 解决方案

  • 打开“Visual Studio Code”的新实例。

  • 打开“命令面板”(“Shift” + “Command” + “P”) 。

  • 选择“Flutter: 新建项目”命令,然后按“输入”。

  • 为“项目名称”输入“push_demo”,然后选择“项目位置”。

  • 系统提示时,选择“获取包”。

  • 同时按住“Control” + “单击”“kotlin”文件夹(位于“app”>“scr”>“main”下),然后选择“在查找器中显示” 。 然后将子文件(在“kotlin”文件夹下)分别重命名为 com<your_organization>pushdemo

    使用 Visual Studio Code 模板时,这些文件夹默认为 com<例如,project_name> 假设“mobcat”用于“组织”,则文件夹结构应显示为 :

  • Kotlin
    • mobcat
      • pushdemo
      • 返回“Visual Studio Code”,将“android”>“app”>“build.gradle”中的 applicationId 值更新为 com.<your_organization>.pushdemo

        应将自己的组织名称用于 <your_organization> 占位符。 例如,使用“mobcat”作为组织将导致“包名称”的值为“com.mobcat.pushdemo” 。

      • 更新“AndroidManifest.xml”文件中的“包”的属性,分别位于“scr”>“debug”、“scr”>“main”和“scr”>“profile”下 。 确保值与你在上一步中使用的“applicationId”相匹配。

        <manifest
            xmlns:android="http://schemas.android.com/apk/res/android"
            package="com.<your_organization>.pushdemo>">
        </manifest>
        
      • 将“AndroidManifest.xml”文件中“scr”>“main”下的 android:label 属性更新为“PushDemo” 。 然后,直接在 android:label 下添加 android:allowBackup 属性,将其值设置为“false”。

        <application
            android:name="io.flutter.app.FlutterApplication"
            android:label="PushDemo"
            android:allowBackup="false"
            android:icon="@mipmap/ic_launcher">
        </application>
        
      • 打开应用级别的“build.gradle”文件(“android”>“app”>“build.gradle”),然后更新“compileSdkVersion”(从“android”部分)以使用 API“29” 。 然后,将“minSdkVersion”和“targetSdkVersion”的值(从“defaultConfig”部分)分别更新为“26”和“29” 。

        在本教程中,仅支持运行 API 级别 26 及更高级别的设备,但可以将其扩展为支持运行较早版本的设备。

      • 按住“Control” + 单击“ios”文件夹,然后选择“在 Xcode 中打开” 。

      • 在“Xcode”中,单击“运行器”(是顶部的“xcodeproj”,而不是文件夹) 。 然后,选择“运行器”目标,然后选择“常规”选项卡 。选择“全部”生成配置后,将“捆绑包标识符”更新为 com.<your_organization>.PushDemo

        应将自己的组织名称用于 <your_organization> 占位符。 例如,使用“mobcat”作为组织将导致“捆绑包标识符”的值为“com.mobcat.PushDemo” 。

      • 单击“info.plist”然后将“捆绑包名称”值更新为“PushDemo”

      • 关闭“Xcode”并返回到“Visual Studio Code” 。

      • 返回Visual Studio Code,打开 pubspec.yaml,将 httpflutter_secure_storageDart 包添加为依赖项。 然后,保存文件并在出现提示时单击“获取包”。

        dependencies:
          flutter:
            sdk: flutter
          http: ^0.12.1
          flutter_secure_storage: ^3.3.3
        
      • 在“终端”中,将目录更改为“ios”文件夹(对于 Flutter 项目) 。 然后,执行 “pod install”命令安装新的 pod(flutter_secure_storage 包所要求)。

      • 按住“Control” + 单击“lib”文件夹,然后从菜单中选择“新建文件夹”,使用“main_page.dart”作为文件名 。 然后,添加以下代码。

        import 'package:flutter/material.dart';
        class MainPage extends StatefulWidget {
          @override
          _MainPageState createState() => _MainPageState();
        class _MainPageState extends State<MainPage> {
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              body: SafeArea(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: <Widget>[],
        
      • 在“main.dart”中,用以下代码替换模板代码。

        import 'package:flutter/material.dart';
        import 'package:push_demo/main_page.dart';
        final navigatorKey = GlobalKey<NavigatorState>();
        void main() => runApp(MaterialApp(home: MainPage(), navigatorKey: navigatorKey));
        
      • 在“终端”中,在每个目标平台上生成并运行该应用,以测试模板化的应用在设备上的运行情况。 确保支持的设备已连接。

        flutter run
        

        实现跨平台组件

      • 按住“Control” + 单击“lib”文件夹,然后从菜单中选择“新建文件夹”,使用“models”作为“文件夹名称” 。

      • 按住“Control” + 单击“models”文件夹,然后从菜单中选择“新建文件夹”,使用“device_installation.dart”作为文件名 。 然后,添加以下代码。

        class DeviceInstallation {
            final String deviceId;
            final String platform;
            final String token;
            final List<String> tags;
            DeviceInstallation(this.deviceId, this.platform, this.token, this.tags);
            DeviceInstallation.fromJson(Map<String, dynamic> json)
              : deviceId = json['installationId'],
                platform = json['platform'],
                token = json['pushChannel'],
                tags = json['tags'];
            Map<String, dynamic> toJson() =>
              'installationId': deviceId,
              'platform': platform,
              'pushChannel': token,
              'tags': tags,
        
      • 将一个新文件添加到名为“push_demo_action.dart”的“models”文件夹中,以定义此示例中支持的操作的枚举。

        enum PushDemoAction {
          actionA,
          actionB,
        
      • 通过以下实现,将新文件夹添加到名为“服务”的项目,然后将新文件添加到名为“device_installation_service.dart”的文件夹。

        import 'package:flutter/services.dart';
        class DeviceInstallationService {
          static const deviceInstallation = const MethodChannel('com.<your_organization>.pushdemo/deviceinstallation');
          static const String getDeviceIdChannelMethod = "getDeviceId";
          static const String getDeviceTokenChannelMethod = "getDeviceToken";
          static const String getDevicePlatformChannelMethod = "getDevicePlatform";
          Future<String> getDeviceId() {
            return deviceInstallation.invokeMethod(getDeviceIdChannelMethod);
          Future<String> getDeviceToken() {
            return deviceInstallation.invokeMethod(getDeviceTokenChannelMethod);
          Future<String> getDevicePlatform() {
            return deviceInstallation.invokeMethod(getDevicePlatformChannelMethod);
        

        应将自己的组织名称用于 <your_organization> 占位符。 例如,使用“mobcat”作为组织将导致 MethodChannel 名称为“com.mobcat.pushdemo / deviceinstallation”。

        此类封装了与底层本机平台一起使用的功能,以获取必需的设备安装详细信息。 MethodChannel 有助于与底层本机平台进行双向异步通信。 在后面的步骤中,将创建该通道的特定于平台的对应项。

      • 通过以下实现,将另一个文件添加到名为“notification_action_service.dart”的文件夹中。

        import 'package:flutter/services.dart';
        import 'dart:async';
        import 'package:push_demo/models/push_demo_action.dart';
        class NotificationActionService {
          static const notificationAction =
              const MethodChannel('com.<your_organization>.pushdemo/notificationaction');
          static const String triggerActionChannelMethod = "triggerAction";
          static const String getLaunchActionChannelMethod = "getLaunchAction";
          final actionMappings = {
            'action_a' : PushDemoAction.actionA,
            'action_b' : PushDemoAction.actionB
          final actionTriggeredController = StreamController.broadcast();
          NotificationActionService() {
            notificationAction
                .setMethodCallHandler(handleNotificationActionCall);
          Stream get actionTriggered => actionTriggeredController.stream;
          Future<void> triggerAction({action: String}) async {
            if (!actionMappings.containsKey(action)) {
              return;
            actionTriggeredController.add(actionMappings[action]);
          Future<void> checkLaunchAction() async {
            final launchAction = await notificationAction.invokeMethod(getLaunchActionChannelMethod) as String;
            if (launchAction != null) {
              triggerAction(action: launchAction);
          Future<void> handleNotificationActionCall(MethodCall call) async {
            switch (call.method) {
              case triggerActionChannelMethod:
                return triggerAction(action: call.arguments as String);
              default:
                throw MissingPluginException();
                break;
        

        这是一种用于集中处理通知操作的简单机制,以便可以使用强类型枚举以跨平台的方式进行处理。 当通知有效负载中指定了一项操作时,该服务可使底层本机平台触发该操作。 一旦 Flutter 准备好处理它,它还使通用代码能够以可追溯的方式检查在应用程序启动期间是否指定了操作。 例如,通过点击通知中心的通知启动应用时。

      • 通过以下实现,将新文件添加到名为“notification_registration_service.dart”的“services”文件夹中。

        import 'dart:convert';
        import 'package:flutter/services.dart';
        import 'package:http/http.dart' as http;
        import 'package:push_demo/services/device_installation_service.dart';
        import 'package:push_demo/models/device_installation.dart';
        import 'package:flutter_secure_storage/flutter_secure_storage.dart';
        class NotificationRegistrationService {
          static const notificationRegistration =
              const MethodChannel('com.<your_organization>.pushdemo/notificationregistration');
          static const String refreshRegistrationChannelMethod = "refreshRegistration";
          static const String installationsEndpoint = "api/notifications/installations";
          static const String cachedDeviceTokenKey = "cached_device_token";
          static const String cachedTagsKey = "cached_tags";
          final deviceInstallationService = DeviceInstallationService();
          final secureStorage = FlutterSecureStorage();
          String baseApiUrl;
          String apikey;
          NotificationRegistrationService(this.baseApiUrl, this.apikey) {
            notificationRegistration
                .setMethodCallHandler(handleNotificationRegistrationCall);
          String get installationsUrl => "$baseApiUrl$installationsEndpoint";
          Future<void> deregisterDevice() async {
            final cachedToken = await secureStorage.read(key: cachedDeviceTokenKey);
            final serializedTags = await secureStorage.read(key: cachedTagsKey);
            if (cachedToken == null || serializedTags == null) {
              return;
            var deviceId = await deviceInstallationService.getDeviceId();
            if (deviceId.isEmpty) {
              throw "Unable to resolve an ID for the device.";
            var response = await http
                .delete("$installationsUrl/$deviceId", headers: {"apikey": apikey});
            if (response.statusCode != 200) {
              throw "Deregister request failed: ${response.reasonPhrase}";
            await secureStorage.delete(key: cachedDeviceTokenKey);
            await secureStorage.delete(key: cachedTagsKey);
          Future<void> registerDevice(List<String> tags) async {
            try {
              final deviceId = await deviceInstallationService.getDeviceId();
              final platform = await deviceInstallationService.getDevicePlatform();
              final token = await deviceInstallationService.getDeviceToken();
              final deviceInstallation =
                  DeviceInstallation(deviceId, platform, token, tags);
              final response = await http.put(installationsUrl,
                  body: jsonEncode(deviceInstallation),
                  headers: {"apikey": apikey, "Content-Type": "application/json"});
              if (response.statusCode != 200) {
                throw "Register request failed: ${response.reasonPhrase}";
              final serializedTags = jsonEncode(tags);
              await secureStorage.write(key: cachedDeviceTokenKey, value: token);
              await secureStorage.write(key: cachedTagsKey, value: serializedTags);
            } on PlatformException catch (e) {
              throw e.message;
            } catch (e) {
              throw "Unable to register device: $e";
          Future<void> refreshRegistration() async {
            final currentToken = await deviceInstallationService.getDeviceToken();
            final cachedToken = await secureStorage.read(key: cachedDeviceTokenKey);
            final serializedTags = await secureStorage.read(key: cachedTagsKey);
            if (currentToken == null ||
                cachedToken == null ||
                serializedTags == null ||
                currentToken == cachedToken) {
              return;
            final tags = jsonDecode(serializedTags);
            return registerDevice(tags);
          Future<void> handleNotificationRegistrationCall(MethodCall call) async {
            switch (call.method) {
              case refreshRegistrationChannelMethod:
                return refreshRegistration();
              default:
                throw MissingPluginException();
                break;
        

        此类封装了“DeviceInstallationService”的使用和对后端服务的请求,以执行必需的注册、注销和刷新注册操作。 仅当你选择完成使用 API 密钥对客户端进行身份验证部分时,才需要 apiKey 参数。

      • 通过以下实现,将新文件添加到名为“config.dart”的“lib”文件夹中。

        class Config {
          static String apiKey = "API_KEY";
          static String backendServiceEndpoint = "BACKEND_SERVICE_ENDPOINT";
        

        这是定义应用机密的简单方法。 将占位符值替换为你自己的值。 应在构建后端服务时记下这些值。 “API 应用 URL”应为 https://<api_app_name>.azurewebsites.net/。 仅当你选择完成使用 API 密钥对客户端进行身份验证部分时,才需要 apiKey 成员。

        请确保将其添加到 gitignore 文件,以避免将这些机密提交给源代码管理。

        实现跨平台 UI

      • 在“main_page.dart”中,将“build”函数替换为以下内容 。

        @override
        Widget build(BuildContext context) {
        return Scaffold(
            body: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 40.0),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.end,
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: <Widget>[
                  FlatButton(
                    child: Text("Register"),
                    onPressed: registerButtonClicked,
                  FlatButton(
                    child: Text("Deregister"),
                    onPressed: deregisterButtonClicked,
        
      • 将必需的导入添加到“main_page dart”文件的顶部。

        import 'package:push_demo/services/notification_registration_service.dart';
        import 'config.dart';
        
      • 向“_MainPageState”类添加一个字段,以存储对“NotificationRegistrationService”的引用 。

        final notificationRegistrationService = NotificationRegistrationService(Config.backendServiceEndpoint, Config.apiKey);
        
      • 在“_MainPageState”类中,实现“Register”和“Deregister”按钮“onPressed”事件的事件处理程序 。 调用相应的“Register”/“Deregister”方法,然后显示警报以指示结果 。

        void registerButtonClicked() async {
            try {
              await notificationRegistrationService.registerDevice(List<String>());
              await showAlert(message: "Device registered");
            catch (e) {
              await showAlert(message: e);
          void deregisterButtonClicked() async {
            try {
              await notificationRegistrationService.deregisterDevice();
              await showAlert(message: "Device deregistered");
            catch (e) {
              await showAlert(message: e);
          Future<void> showAlert({ message: String }) async {
            return showDialog<void>(
              context: context,
              barrierDismissible: false,
              builder: (BuildContext context) {
                return AlertDialog(
                  title: Text('PushDemo'),
                  content: SingleChildScrollView(
                    child: ListBody(
                      children: <Widget>[
                        Text(message),
                  actions: <Widget>[
                    FlatButton(
                      child: Text('OK'),
                      onPressed: () {
                        Navigator.of(context).pop();
        
      • 在“main.dart”中,确保以下导入位于文件顶部。

        import 'package:flutter/material.dart';
        import 'package:push_demo/models/push_demo_action.dart';
        import 'package:push_demo/services/notification_action_service.dart';
        import 'package:push_demo/main_page.dart';
        
      • 声明一个变量以存储对 “NotificationActionService” 实例的引用并对其进行初始化。

        final notificationActionService = NotificationActionService();
        
      • 添加函数以处理触发操作时警报的显示。

        void notificationActionTriggered(PushDemoAction action) {
          showActionAlert(message: "${action.toString().split(".")[1]} action received");
        Future<void> showActionAlert({ message: String }) async {
          return showDialog<void>(
            context: navigatorKey.currentState.overlay.context,
            barrierDismissible: false,
            builder: (BuildContext context) {
              return AlertDialog(
                title: Text('PushDemo'),
                content: SingleChildScrollView(
                  child: ListBody(
                    children: <Widget>[
                      Text(message),
                actions: <Widget>[
                  FlatButton(
                    child: Text('OK'),
                    onPressed: () {
                      Navigator.of(context).pop();
        
      • 更新 main 函数以观察 NotificationActionServiceactionTriggered 流,并检查应用启动期间捕获的任何操作。

        void main() async {
          runApp(MaterialApp(home: MainPage(), navigatorKey: navigatorKey,));
          notificationActionService.actionTriggered.listen((event) { notificationActionTriggered(event as PushDemoAction); });
          await notificationActionService.checkLaunchAction();
        

        这只是为了演示推送通知操作的接收和传播。 通常情况下,将以无提示方式处理这些操作(例如导航到特定视图或刷新某些数据),而不是在这种情况下显示警报。

        针对推送通知配置本地 Android 项目

        添加 Google Services JSON 文件

      • 按住“Control”的同时 + 单击“android”文件夹,然后选择“在 Android Studio 中打开” 。 然后,切换到“项目”视图(如果还没有切换到此视图)。

      • 找到之前在 Firebase 控制台中设置 PushDemo 项目时下载的 google-services.json 文件。 然后,将其拖动到“应用”模块根目录(“android”>“android”>“应用”)中 。

        配置生成设置和权限

      • 将“项目”视图切换成“Android” 。

      • 打开AndroidManifest.xml,然后在结束标记之前,在应用程序元素后面添加 INTERNETREAD_PHONE_STATE权限。

        <manifest>
            <application>...</application>
            <uses-permission android:name="android.permission.INTERNET" />
            <uses-permission android:name="android.permission.READ_PHONE_STATE" />
        </manifest>
        

        添加 Firebase SDK

      • 在 Android Studio 中,打开项目级别 build.gradle 文件(Gradle 脚本>“build.gradle (Project: android)”) 。 并确保在buildscript>“依赖项”节点中具有“com.google.gms:google-services”类路径。

        buildscript {
          repositories {
            // Check that you have the following line (if not, add it):
            google()  // Google's Maven repository
          dependencies {
            // ...
            // Add the following line:
            classpath 'com.google.gms:google-services:4.3.3'  // Google Services plugin
        allprojects {
          // ...
          repositories {
            // Check that you have the following line (if not, add it):
            google()  // Google's Maven repository
            // ...
        

        确保在创建“Android 项目”时,按照 Firebase 控制台中提供的说明引用最新版本。

      • 在应用级 build.gradle 文件(Gradle 脚本>“build.gradle (Module: app)”)中,应用 Google Services Gradle 插件 。 在“android”节点的正上方应用插件。

        // ...
        // Add the following line:
        apply plugin: 'com.google.gms.google-services'  // Google Services plugin
        android {
          // ...
        
      • 在同一个文件的“依赖项”节点中,添加“Cloud Messaging”Android 库的依赖项 。

        dependencies {
            // ...
            implementation 'com.google.firebase:firebase-messaging:20.2.0'
        

        确保按照 Cloud Messaging Android 客户端文档引用了最新版本。

      • 保存更改,然后(从工具栏提示)单击“立即同步”按钮或“将项目与 Gradle 文件同步” 。

        处理 Android 的推送通知

      • Android Studio 中,控件 + 单击com.your_organization.pushdemo<> 包文件夹 (应用>src>main>kotlin) ,从“新建”菜单中选择“”。 输入“services”作为名称,然后按“返回” 。

      • 按住“Control”的同时 + 单击“services”文件夹,选择“新建”菜单中的“Kotlin 文件/类” 。 输入“DeviceInstallationService”作为名称,然后按“返回” 。

      • 使用以下代码实现“DeviceInstallationService”。

        package com.<your_organization>.pushdemo.services
        import android.annotation.SuppressLint
        import android.content.Context
        import android.provider.Settings.Secure
        import com.google.android.gms.common.ConnectionResult
        import com.google.android.gms.common.GoogleApiAvailability
        import io.flutter.embedding.engine.FlutterEngine
        import io.flutter.plugin.common.MethodCall
        import io.flutter.plugin.common.MethodChannel
        @SuppressLint("HardwareIds")
        class DeviceInstallationService {
            companion object {
                const val DEVICE_INSTALLATION_CHANNEL = "com.<your_organization>.pushdemo/deviceinstallation"
                const val GET_DEVICE_ID = "getDeviceId"
                const val GET_DEVICE_TOKEN = "getDeviceToken"
                const val GET_DEVICE_PLATFORM = "getDevicePlatform"
            private var context: Context
            private var deviceInstallationChannel : MethodChannel
            val playServicesAvailable
                get() = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS
            constructor(context: Context, flutterEngine: FlutterEngine) {
                this.context = context
                deviceInstallationChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, DEVICE_INSTALLATION_CHANNEL)
                deviceInstallationChannel.setMethodCallHandler { call, result -> handleDeviceInstallationCall(call, result) }
            fun getDeviceId() : String
                = Secure.getString(context.applicationContext.contentResolver, Secure.ANDROID_ID)
            fun getDeviceToken() : String {
                if(!playServicesAvailable) {
                    throw Exception(getPlayServicesError())
                // TODO: Revisit once we have created the PushNotificationsFirebaseMessagingService
                val token = "Placeholder_Get_Value_From_FirebaseMessagingService_Implementation"
                if (token.isNullOrBlank()) {
                    throw Exception("Unable to resolve token for FCM.")
                return token
            fun getDevicePlatform() : String = "fcm"
            private fun handleDeviceInstallationCall(call: MethodCall, result: MethodChannel.Result) {
                when (call.method) {
                    GET_DEVICE_ID -> {
                        result.success(getDeviceId())
                    GET_DEVICE_TOKEN -> {
                        getDeviceToken(result)
                    GET_DEVICE_PLATFORM -> {
                        result.success(getDevicePlatform())
                    else -> {
                        result.notImplemented()
            private fun getDeviceToken(result: MethodChannel.Result) {
                try {
                    val token = getDeviceToken()
                    result.success(token)
                catch (e: Exception) {
                    result.error("ERROR", e.message, e)
            private fun getPlayServicesError(): String {
                val resultCode = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
                if (resultCode != ConnectionResult.SUCCESS) {
                    return if (GoogleApiAvailability.getInstance().isUserResolvableError(resultCode)){
                        GoogleApiAvailability.getInstance().getErrorString(resultCode)
                    } else {
                        "This device is not supported"
                return "An error occurred preventing the use of push notifications"
        

        此类为 com.<your_organization>.pushdemo/deviceinstallation 通道实现特定于平台的对应项。 这是在“DeviceInstallationService.dart”中的应用的 Flutter 部分中定义的。 在这种情况下,调用是从公共代码到本机主机进行的。 请确保将 your_organization> 替换为<自己的组织,无论在何处使用此功能。

        此类提供一个唯一 ID(使用 Secure.AndroidId)作为通知中心注册有效负载的一部分。

      • 将另一个“Kotlin 文件/类”添加到名为 NotificationRegistrationService 的“服务”文件夹中,然后添加以下代码 。

        package com.<your_organization>.pushdemo.services
        import io.flutter.embedding.engine.FlutterEngine
        import io.flutter.plugin.common.MethodChannel
        class NotificationRegistrationService {
            companion object {
                const val NOTIFICATION_REGISTRATION_CHANNEL = "com.<your_organization>.pushdemo/notificationregistration"
                const val REFRESH_REGISTRATION = "refreshRegistration"
            private var notificationRegistrationChannel : MethodChannel
            constructor(flutterEngine: FlutterEngine) {
                notificationRegistrationChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, NotificationRegistrationService.NOTIFICATION_REGISTRATION_CHANNEL)
            fun refreshRegistration() {
                notificationRegistrationChannel.invokeMethod(REFRESH_REGISTRATION, null)
        

        此类为 com.<your_organization>.pushdemo/notificationregistration 通道实现特定于平台的对应项。 这是在“NotificationRegistrationService.dart”中的应用的 Flutter 部分中定义的。 在这种情况下,调用是从本机主机到公共代码进行的。 同样,无论在何处使用your_organization,请务必将your_organization>替换为<自己的组织。

      • 将另一个“Kotlin 文件/类”添加到名为 NotificationActionService 的“服务”文件夹中,然后添加以下代码 。

        package com.<your_organization>.pushdemo.services
        import io.flutter.embedding.engine.FlutterEngine
        import io.flutter.plugin.common.MethodCall
        import io.flutter.plugin.common.MethodChannel
        class NotificationActionService {
            companion object {
                const val NOTIFICATION_ACTION_CHANNEL = "com.<your_organization>.pushdemo/notificationaction"
                const val TRIGGER_ACTION = "triggerAction"
                const val GET_LAUNCH_ACTION = "getLaunchAction"
            private var notificationActionChannel : MethodChannel
            var launchAction : String? = null
            constructor(flutterEngine: FlutterEngine) {
                notificationActionChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, NotificationActionService.NOTIFICATION_ACTION_CHANNEL)
                notificationActionChannel.setMethodCallHandler { call, result -> handleNotificationActionCall(call, result) }
            fun triggerAction(action: String) {
                notificationActionChannel.invokeMethod(NotificationActionService.TRIGGER_ACTION, action)
            private fun handleNotificationActionCall(call: MethodCall, result: MethodChannel.Result) {
                when (call.method) {
                    NotificationActionService.GET_LAUNCH_ACTION -> {
                        result.success(launchAction)
                    else -> {
                        result.notImplemented()
        

        此类为 com.<your_organization>.pushdemo/notificationaction 通道实现特定于平台的对应项。 这是在“NotificationActionService.dart”中的应用的 Flutter 部分中定义的。 在这种情况下,可以进行双向调用。 请确保将 your_organization> 替换为<自己的组织,无论在何处使用此功能。

      • 将新的 Kotlin 文件/类添加到名为 PushNotificationsFirebaseMessagingServicecom.your_organization.pushdemo<> 包,然后使用以下代码实现。

        package com.<your_organization>.pushdemo
        import android.os.Handler
        import android.os.Looper
        import com.google.firebase.messaging.FirebaseMessagingService
        import com.google.firebase.messaging.RemoteMessage
        import com.<your_organization>.pushdemo.services.NotificationActionService
        import com.<your_organization>.pushdemo.services.NotificationRegistrationService
        class PushNotificationsFirebaseMessagingService : FirebaseMessagingService() {
            companion object {
                var token : String? = null
                var notificationRegistrationService : NotificationRegistrationService? = null
                var notificationActionService : NotificationActionService? = null
            override fun onNewToken(token: String) {
                PushNotificationsFirebaseMessagingService.token = token
                notificationRegistrationService?.refreshRegistration()
            override fun onMessageReceived(message: RemoteMessage) {
                message.data.let {
                    Handler(Looper.getMainLooper()).post {
                        notificationActionService?.triggerAction(it.getOrDefault("action", null))
        

        此类负责处理应用在前台运行时的通知。 如果在“onMessageReceived”中接收到的通知有效负载中包含一个操作,则它将有条件地调用“NotificationActionService”上的“triggerAction” 。 当“Firebase”令牌通过替代“onNewToken”函数重新生成时,还将在“NotificationRegistrationService”上调用“refreshRegistration” 。

        再次,无论在何处使用,都小心将your_organization>替换为<自己的组织。

      • 在 AndroidManifest.xml(“app”>“src”>“main”)中,使用 com.google.firebase.MESSAGING_EVENT 意向筛选器将“PushNotificationsFirebaseMessagingService”添加到“application”元素的底部 。

        <manifest>
            <application>
                <!-- EXISTING MANIFEST CONTENT -->
                 <service
                    android:name="com.<your_organization>.pushdemo.PushNotificationsFirebaseMessagingService"
                    android:exported="false">
                    <intent-filter>
                        <action android:name="com.google.firebase.MESSAGING_EVENT" />
                    </intent-filter>
                </service>
            </application>
        </manifest>
        
      • 回到“DeviceInstallationService”中,确保以下导入位于文件顶部。

        package com.<your_organization>.pushdemo
        import com.<your_organization>.pushdemo.services.PushNotificationsFirebaseMessagingService
        

        your_organization> 替换为<自己的组织值。

      • 更新占位符文本 Placeholder_Get_Value_From_FirebaseMessagingService_Implementation 以从“PushNotificationFirebaseMessagingService”获取令牌值。

        fun getDeviceToken() : String {
            if(!playServicesAvailable) {
                throw Exception(getPlayServicesError())
            // Get token from the PushNotificationsFirebaseMessagingService.token field.
            val token = PushNotificationsFirebaseMessagingService.token
            if (token.isNullOrBlank()) {
                throw Exception("Unable to resolve token for FCM.")
            return token
        
      • 在“MainActivity”中,确保以下导入位于文件顶部。

        package com.<your_organization>.pushdemo
        import android.content.Intent
        import android.os.Bundle
        import com.google.android.gms.tasks.OnCompleteListener
        import com.google.firebase.iid.FirebaseInstanceId
        import com.<your_organization>.pushdemo.services.DeviceInstallationService
        import com.<your_organization>.pushdemo.services.NotificationActionService
        import com.<your_organization>.pushdemo.services.NotificationRegistrationService
        import io.flutter.embedding.android.FlutterActivity
        

        your_organization> 替换为<自己的组织值。

      • 添加一个变量以存储对“DeviceInstallationService”的引用。

        private lateinit var deviceInstallationService: DeviceInstallationService
        
      • 添加一个名为 processNotificationActions 的函数,以检查“意向”是否具有一个名为“操作”的额外值 。 如果在应用启动期间正在处理该操作,则有条件地触发该操作或将其存储以供以后使用。

         private fun processNotificationActions(intent: Intent, launchAction: Boolean = false) {
            if (intent.hasExtra("action")) {
                var action = intent.getStringExtra("action");
                if (action.isNotEmpty()) {
                    if (launchAction) {
                        PushNotificationsFirebaseMessagingService.notificationActionService?.launchAction = action
                    else {
                        PushNotificationsFirebaseMessagingService.notificationActionService?.triggerAction(action)
        
      • 替代 onNewIntent 函数以调用 processNotificationActions 。

        override fun onNewIntent(intent: Intent) {
            super.onNewIntent(intent)
            processNotificationActions(intent)
        

        由于“MainActivity”的 LaunchMode 设置为 SingleTop,因此会通过 onNewIntent 函数(而不是 onCreate 函数)向现有“活动”实例发送一个“意向”,因此必须同时在 onCreate 和 onNewIntent 函数中处理传入意向 。

      • 替代“onCreate”函数,将“deviceInstallationService”设置为“DeviceInstallationService”的新实例 。

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            flutterEngine?.let {
                deviceInstallationService = DeviceInstallationService(context, it)
        
      • 在“PushNotificationFirebaseMessagingServices”上设置“notificationActionService”和“notificationRegistrationService”属性 。

        flutterEngine?.let {
          deviceInstallationService = DeviceInstallationService(context, it)
          PushNotificationsFirebaseMessagingService.notificationActionService = NotificationActionService(it)
          PushNotificationsFirebaseMessagingService.notificationRegistrationService = NotificationRegistrationService(it)
        
      • 在同一函数中,有条件地调用 FirebaseInstanceId.getInstance().instanceId。 在调用“refreshRegistration”之前,实现“OnCompleteListener”以在“PushNotificationFirebaseMessagingService”上设置令牌值 。

        if(deviceInstallationService?.playServicesAvailable) {
            FirebaseInstanceId.getInstance().instanceId
                .addOnCompleteListener(OnCompleteListener { task ->
                    if (!task.isSuccessful)
                        return@OnCompleteListener
                    PushNotificationsFirebaseMessagingService.token = task.result?.token
                    PushNotificationsFirebaseMessagingService.notificationRegistrationService?.refreshRegistration()
        
      • 仍在“onCreate”中,在函数末尾调用“processNotificationActions” 。 将 true 用于 launchAction 参数,以指示在应用启动期间正在处理此操作 。

        processNotificationActions(this.intent, true)
        
      • 在“Visual Studio Code”中,按住 Ctrl + 单击“ios”文件夹,然后选择“在 Xcode 中打开” 。

      • Xcode 中,单击 “运行器 ” (顶部的 xcodeproj ,而不是) 文件夹,然后选择 “运行器 ”目标,然后选择 “签名 & 功能”。 选择了“全部”生成配置后,选择“团队”的开发人员帐户 。 请确保选中“自动管理签名”选项,这样就会自动选中签名证书和预配配置文件。

        如果未显示新的“预配配置文件”值,请尝试通过依次选择“Xcode”>“首选项”>“帐户”来刷新“签名标识”的配置文件,然后选择“下载手动配置文件”按钮来下载配置文件 。

      • 单击“+ 功能”,然后搜索“推送通知” 。 双击“推送通知”以添加此功能 。

      • 打开“信息列表”并将“最低系统版本”设置为“13.0” 。

        在本教程中,仅支持运行 iOS 13.0 及更高版本的设备,但你可以将其扩展为支持运行较早版本的设备。

      • 打开“Runner.entitlements”并确保“APS 环境”设置设置为“开发” 。

        处理 iOS 的推送通知

      • 按住 Ctrl + 单击“Runner”文件夹(位于 Runner 项目内),然后选择“新建组”并使用“Services”作为名称 。

      • 按住 Ctrl + 单击“Services”文件夹,然后选择“新建文件...” 。然后,选择“Swift 文件”,再单击“下一步” 。 指定“DeviceInstallationService”作为名称,然后单击“创建” 。

      • 使用以下代码实现“DeviceInstallationService.swift”。

        import Foundation
        class DeviceInstallationService {
            enum DeviceRegistrationError: Error {
                case notificationSupport(message: String)
            var token : Data? = nil
            let DEVICE_INSTALLATION_CHANNEL = "com.<your_organization>.pushdemo/deviceinstallation"
            let GET_DEVICE_ID = "getDeviceId"
            let GET_DEVICE_TOKEN = "getDeviceToken"
            let GET_DEVICE_PLATFORM = "getDevicePlatform"
            private let deviceInstallationChannel : FlutterMethodChannel
            var notificationsSupported : Bool {
                get {
                    if #available(iOS 13.0, *) {
                        return true
                    else {
                        return false
            init(withBinaryMessenger binaryMessenger : FlutterBinaryMessenger) {
                deviceInstallationChannel = FlutterMethodChannel(name: DEVICE_INSTALLATION_CHANNEL, binaryMessenger: binaryMessenger)
                deviceInstallationChannel.setMethodCallHandler(handleDeviceInstallationCall)
            func getDeviceId() -> String {
                return UIDevice.current.identifierForVendor!.description
            func getDeviceToken() throws -> String {
                if(!notificationsSupported) {
                    let notificationSupportError = getNotificationsSupportError()
                    throw DeviceRegistrationError.notificationSupport(message: notificationSupportError)
                if (token == nil) {
                    throw DeviceRegistrationError.notificationSupport(message: "Unable to resolve token for APNS.")
                return token!.reduce("", {$0 + String(format: "%02X", $1)})
            func getDevicePlatform() -> String {
                return "apns"
            private func handleDeviceInstallationCall(call: FlutterMethodCall, result: @escaping FlutterResult) {
                switch call.method {
                case GET_DEVICE_ID:
                    result(getDeviceId())
                case GET_DEVICE_TOKEN:
                    getDeviceToken(result: result)
                case GET_DEVICE_PLATFORM:
                    result(getDevicePlatform())
                default:
                    result(FlutterMethodNotImplemented)
            private func getDeviceToken(result: @escaping FlutterResult) {
                    let token = try getDeviceToken()
                    result(token)
                catch let error {
                    result(FlutterError(code: "UNAVAILABLE", message: error.localizedDescription, details: nil))
            private func getNotificationsSupportError() -> String {
                if (!notificationsSupported) {
                    return "This app only supports notifications on iOS 13.0 and above. You are running \(UIDevice.current.systemVersion)"
                return "An error occurred preventing the use of push notifications."
        

        此类为 com.<your_organization>.pushdemo/deviceinstallation 通道实现特定于平台的对应项。 这是在“DeviceInstallationService.dart”中的应用的 Flutter 部分中定义的。 在这种情况下,调用是从公共代码到本机主机进行的。 请确保将 your_organization> 替换为<自己的组织,无论在何处使用此功能。

        此类提供一个唯一 ID(使用 UIDevice.identifierForVendor 值)作为通知中心注册有效负载的一部分。

      • 将另一个“Swift 文件”添加到名为 NotificationRegistrationService 的“Services”文件夹中,然后添加以下代码 。

        import Foundation
        class NotificationRegistrationService {
            let NOTIFICATION_REGISTRATION_CHANNEL = "com.<your_organization>.pushdemo/notificationregistration"
            let REFRESH_REGISTRATION = "refreshRegistration"
            private let notificationRegistrationChannel : FlutterMethodChannel
            init(withBinaryMessenger binaryMessenger : FlutterBinaryMessenger) {
               notificationRegistrationChannel = FlutterMethodChannel(name: NOTIFICATION_REGISTRATION_CHANNEL, binaryMessenger: binaryMessenger)
            func refreshRegistration() {
                notificationRegistrationChannel.invokeMethod(REFRESH_REGISTRATION, arguments: nil)
        

        此类为 com.<your_organization>.pushdemo/notificationregistration 通道实现特定于平台的对应项。 这是在“NotificationRegistrationService.dart”中的应用的 Flutter 部分中定义的。 在这种情况下,调用是从本机主机到公共代码进行的。 同样,无论在何处使用your_organization,请务必将your_organization>替换为<自己的组织。

      • 将另一个“Swift 文件”添加到名为 NotificationActionService 的“Services”文件夹中,然后添加以下代码 。

        import Foundation
        class NotificationActionService {
            let NOTIFICATION_ACTION_CHANNEL = "com.<your_organization>.pushdemo/notificationaction"
            let TRIGGER_ACTION = "triggerAction"
            let GET_LAUNCH_ACTION = "getLaunchAction"
            private let notificationActionChannel: FlutterMethodChannel
            var launchAction: String? = nil
            init(withBinaryMessenger binaryMessenger: FlutterBinaryMessenger) {
                notificationActionChannel = FlutterMethodChannel(name: NOTIFICATION_ACTION_CHANNEL, binaryMessenger: binaryMessenger)
                notificationActionChannel.setMethodCallHandler(handleNotificationActionCall)
            func triggerAction(action: String) {
               notificationActionChannel.invokeMethod(TRIGGER_ACTION, arguments: action)
            private func handleNotificationActionCall(call: FlutterMethodCall, result: @escaping FlutterResult) {
                switch call.method {
                case GET_LAUNCH_ACTION:
                    result(launchAction)
                default:
                    result(FlutterMethodNotImplemented)
        

        此类为 com.<your_organization>.pushdemo/notificationaction 通道实现特定于平台的对应项。 这是在“NotificationActionService.dart”中的应用的 Flutter 部分中定义的。 在这种情况下,可以进行双向调用。 请确保将 your_organization> 替换为<自己的组织,无论在何处使用此功能。

      • 在“AppDelegate.swift”中,添加变量以存储对先前创建的服务的引用。

        var deviceInstallationService : DeviceInstallationService?
        var notificationRegistrationService : NotificationRegistrationService?
        var notificationActionService : NotificationActionService?
        
      • 添加一个名为"processNotificationActions"的函数来处理通知数据。 如果在应用启动期间正在处理该操作,则有条件地触发该操作或将其存储以供以后使用。

        func processNotificationActions(userInfo: [AnyHashable : Any], launchAction: Bool = false) {
            if let action = userInfo["action"] as? String {
                if (launchAction) {
                    notificationActionService?.launchAction = action
                else {
                    notificationActionService?.triggerAction(action: action)
        
      • 替代用于为“DeviceInstallationService”设置令牌值的“didRegisterForRemoteNotificationsWithDeviceToken”函数 。 然后,在“NotificationRegistrationService”上调用“refreshRegistration” 。

        override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
          deviceInstallationService?.token = deviceToken
          notificationRegistrationService?.refreshRegistration()
        
      • 替代 didReceiveRemoteNotification 函数,该函数将 userInfo 参数传递到 processNotificationActions 函数 。

        override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
            processNotificationActions(userInfo: userInfo)
        
      • 替代“didFailToRegisterForRemoteNotificationsWithError”函数来记录错误。

        override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
            print(error);
        

        这就是一个占位符。 对于生产场景,需要实现正确的日志记录和错误处理。

      • 在“didFinishLaunchingWithOptions”中,实例化“deviceInstallationService”、“notificationRegistrationService”和“notificationActionService”变量 。

        let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
        deviceInstallationService = DeviceInstallationService(withBinaryMessenger: controller.binaryMessenger)
        notificationRegistrationService = NotificationRegistrationService(withBinaryMessenger: controller.binaryMessenger)
        notificationActionService = NotificationActionService(withBinaryMessenger: controller.binaryMessenger)
        
      • 在同一函数中,有条件地请求授权,并注册远程通知。

        if #available(iOS 13.0, *) {
          UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {
              (granted, error) in
              if (granted)
                  DispatchQueue.main.async {
                      let pushSettings = UIUserNotificationSettings(types: [.alert, .sound, .badge], categories: nil)
                      application.registerUserNotificationSettings(pushSettings)
                      application.registerForRemoteNotifications()
        
      • 如果“launchOptions”包含“remoteNotification”项,请在“didFinishLaunchingWithOptions”函数末尾调用“processNotificationActions” 。 传入生成的 userInfo 对象,并使用 true 作为 launchAction 参数 。 true 值表示在应用启动期间正在处理该操作。

        if let userInfo = launchOptions?[.remoteNotification] as? [AnyHashable : Any] {
            processNotificationActions(userInfo: userInfo, launchAction: true)
        

        测试解决方案

        现在可以测试通过后端服务发送通知。

        发送测试通知

      • Postman 中打开一个新选项卡。

      • 将请求设置为“POST”,然后输入以下地址:

        https://<app_name>.azurewebsites.net/api/notifications/requests
        
      • 如果选择完成使用 API 密钥对客户端进行身份验证部分,请确保将请求标头配置为包含 apikey 值。

      • 选择“代码”按钮,该按钮位于窗口右上角的“保存”按钮下方 。 显示 HTML 请求时,请求应类似于以下示例(具体取决于是否包含 apikey 标头) :

        POST /api/notifications/requests HTTP/1.1
        Host: https://<app_name>.azurewebsites.net
        apikey: <your_api_key>
        Content-Type: application/json
            "text": "Message from backend service",
            "action": "action_a"
        
      • 在一个或两个目标平台(Android 和 iOS)上运行 PushDemo 应用程序 。

        如果你正在 Android 上进行测试,请确保未在“调试”模式中运行,或者如果通过运行应用程序部署了应用,请强制关闭该应用,然后从启动器重新启动 。

      • 在 PushDemo 应用中,点击“注册”按钮 。

      • 返回 Postman,关闭“生成代码片段”窗口(如果尚未关闭),然后单击“发送”按钮 。

      • 验证是否在 Postman 中收到“200 OK”响应,并验证该警报是否显示在应用中,表明“已收到 ActionA 操作” 。

      • 关闭 PushDemo 应用,然后再次单击 Postman 中的“发送”按钮 。

      • 验证是否在 Postman 中再次收到“200 OK”响应 。 验证 PushDemo 应用的通知区域中是否显示带有正确消息的通知。

      • 点击通知,确认它会打开应用并显示“已收到 ActionA 操作”警报。

      • 返回 Postman,修改之前的请求正文以发送无提示通知,指定“action”值为 action_b 而不是 action_a 。

        "action": "action_b", "silent": true
      • 应用仍处于打开状态的情况下,单击 Postman 中的“发送”按钮 。

      • 验证是否在 Postman 中收到“200 OK”响应,并验证是否在应用中显示该警报,表明“已收到 ActionB 操作”,而不是“已收到 ActionA 操作” 。

      • 关闭 PushDemo 应用,然后再次单击 Postman 中的“发送”按钮 。

      • 验证是否在 Postman 中收到“200 OK”响应,并验证无提示通知是否未显示在通知区域中 。

        后端服务没有响应

        在本地进行测试时,请确保后端服务正在运行,并且正在使用正确的端口。

        如果要对 Azure API 应用进行测试,请检查服务是否正在运行、是否已部署并且已正确启动。

        通过客户端进行测试时,请确保在 Postman 或移动应用配置中正确地指定了基址。 在本地进行测试时,基址应显示为 https://<api_name>.azurewebsites.net/https://localhost:5001/

        开始或停止调试会话后,Android 上没有收到通知

        请确保在启动或停止调试会话后重新注册。 调试器将导致生成新的 Firebase 令牌。 还必须更新通知中心安装。

        从后端服务收到 401 状态代码

        验证是否正在设置 apikey 请求标头,以及此值是否与你为后端服务配置的值相匹配。

        如果在本地进行测试时收到此错误,请确保在客户端配置中定义的密钥值与 API 使用的 Authentication:ApiKey 用户设置值相匹配。

        如果要对 API 应用进行测试,请确保客户端配置文件中的密钥值与你在 API 应用中使用的 Authentication:ApiKey 应用程序设置相匹配 。

        如果在部署后端服务后创建或更改了此设置,则必须重启该服务才能使其生效。

        如果选择不完成使用 API 密钥对客户端进行身份验证部分,请确保未将 Authorize 特性应用到 NotificationsController 类 。

        从后端服务收到 404 状态代码

        验证终结点和 HTTP 请求方法是否正确。 例如,终结点应显示为:

      • [PUT]https://<api_name>.azurewebsites.net/api/notifications/installations
      • [DELETE]https://<api_name>.azurewebsites.net/api/notifications/installations/<installation_id>
      • [POST]https://<api_name>.azurewebsites.net/api/notifications/requests
      • 如果在本地进行测试,应为:

      • [PUT]https://localhost:5001/api/notifications/installations
      • [DELETE]https://localhost:5001/api/notifications/installations/<installation_id>
      • [POST]https://localhost:5001/api/notifications/requests
      • 在客户端应用中指定基址时,请确保它以 / 结尾。 在本地进行测试时,基址应显示为 https://<api_name>.azurewebsites.net/https://localhost:5001/

        无法注册并显示通知中心错误消息

        验证测试设备是否已连接到网络。 然后,通过设置断点来确定 Http 响应状态代码,以检查 HttpResponse 中的 StatusCode 属性值 。

        根据状态代码,查看以前的故障排除建议(如果适用)。

        在为相应的 API 返回这些特定状态代码的行上设置断点。 然后,在本地进行调试时,尝试调用后端服务。

        使用适当的有效负载,通过 Postman 验证后端服务是否按预期方式工作。 使用客户端代码为相关平台创建的实际有效负载。

        查看特定于平台的配置部分,确保没有遗漏任何步骤。 检查是否为相应平台的 installation idtoken 变量解析了合适的值。

        无法解析显示的设备错误消息的 ID

        查看特定于平台的配置部分,确保没有遗漏任何步骤。

      • Azure 通知中心概述
      • 在 macOS 上安装 Flutter
      • 在 Windows 上安装 Flutter
      • 用于后端操作的通知中心 SDK
      • GitHub 上的通知中心 SDK
      • 使用应用程序后端注册
      • 使用自定义模板
      • 现在应具有一个通过后端服务连接到通知中心的基本 Flutter 应用,可以发送和接收通知。

        可能需要调整本教程中使用的示例,使其适合你自己的方案。 此外,还建议实现更可靠的错误处理、重试逻辑和日志记录。

        Visual Studio App Center 可以快速合并到提供分析诊断的移动应用中,从而帮助你进行故障排除。

  •