教程:通过后端服务使用 Azure 通知中心将推送通知发送到 Flutter 应用

下载示例 下载示例

本教程使用 Azure 通知中心 将通知推送到面向 android 和 iOSFlutter 应用程序。

ASP.NET 核心 Web API 后端用于使用最新、最佳 安装 方法处理客户端 设备注册。 该服务还将以跨平台的方式发送推送通知。

这些操作使用 通知中心 SDK 来处理后端操作从应用后端注册 文档中提供了有关总体方法的更多详细信息。

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

先决条件

若要继续操作,需要:

  • Azure 订阅,可在其中创建和管理资源。
  • Flutter 工具包(及其先决条件)。
  • 已安装 Flutter 和 Dart 插件的 Visual Studio Code。
  • 已安装用于管理库依赖项的 cocoaPods
  • 能够在 Android(物理设备或仿真器设备)或 iOS(仅限物理设备)上运行应用。

对于 Android,必须具备:

  • 开发人员解锁的物理设备或仿真器 (安装了 Google Play Services 的 API 26 及更高版本)

对于 iOS,必须具备:

注意

iOS 模拟器不支持远程通知,因此在 iOS 上浏览此示例时需要物理设备。 但是,无需在 android iOS 上运行应用才能完成本教程。

可以按照此第一原则示例中的步骤操作,无需事先体验。 但是,你将受益于熟悉以下方面。

提供的步骤特定于 macOS。 通过跳过 iOS 方面,可以遵循 Windows

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

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

创建 Firebase 项目并启用适用于 Android 的 Firebase Cloud Messaging

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

    注意

    将为你生成唯一名称。 默认情况下,它由你提供的名称的小写变体以及用短划线分隔的生成的数字组成。 如果想要提供它仍然全局唯一,则可以更改此项。

  2. 创建项目后,选择 将 Firebase 添加到 Android 应用

    将 Firebase 添加到 Android 应用

  3. 将 Firebase 添加到 Android 应用 页上,执行以下步骤。

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

      指定包名称

    2. 选择 注册应用

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

      下载 google-services.json

    4. 选择“下一”

    5. 选择 “继续”控制台

      注意

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

  4. 在 Firebase 控制台中,选择项目的齿轮。 然后选择 项目设置

    选择项目设置

    注意

    如果尚未下载 google-services.json 文件,可以在此页上下载该文件。

  5. 切换到顶部的“Cloud Messaging”选项卡。 复制并保存 服务器密钥 供以后使用。 使用此值配置通知中心。

    复制服务器密钥

注册 iOS 应用以获取推送通知

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

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

    iOS 预配门户应用 ID 页

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

    iOS 预配门户 注册新 ID 页

  3. 更新新应用的以下三个值,然后选择“继续”

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

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

      iOS 预配门户注册应用 ID 页

    • 推送通知:在 功能 部分中选中“推送通知”选项。

      表单以注册新的应用 ID

      此操作将生成应用 ID 和确认信息的请求。 选择 继续,然后选择 注册 以确认新的应用 ID。

      确认新的应用 ID

      选择 注册后,可在 证书、标识符 & 配置文件 页中看到新的应用 ID 作为行项。

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

为通知中心创建证书

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

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

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

较新的方法在 APNS的基于令牌的身份验证(HTTP/2)身份验证中记录了许多优势。 需要更少的步骤,但也要求针对特定方案执行这些步骤。 但是,已为这两种方法提供步骤,因为两种方法都适用于本教程。

选项 1:创建可直接上传到通知中心的 p12 推送证书
  1. 在 Mac 上,运行密钥链访问工具。 可以从 Utilities 文件夹或 Launchpad 上的 “其他”文件夹打开它。

  2. 选择 密钥链访问,展开 证书助手,然后选择 从证书颁发机构请求证书。

    使用密钥链访问请求新证书

    注意

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

  3. 选择 用户电子邮件地址,输入 公用名 值,确保指定保存到磁盘,然后选择 继续。 将 CA 电子邮件地址 保留为空,因为不需要。

    预期的证书信息

  4. 另存为中输入 证书签名请求(CSR) 文件的名称,选择位置,然后选择 保存

    为证书 选择文件名

    此操作将 CSR 文件 保存在所选位置。 默认位置为桌面。 请记住为文件选择的位置。

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

    “编辑应用 ID”页

  6. 此时会显示 Apple 推送通知服务 TLS/SSL 证书 窗口。 选择 开发 TLS/SSL 证书 部分下的“创建证书”按钮。

    “为应用 ID 创建证书”按钮

    将显示 “创建新证书”屏幕。

    注意

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

  7. 选择“选择文件,浏览到保存 CSR 文件的位置,然后双击证书名称将其加载。 然后选择 继续

  8. 门户创建证书后,选择“下载”按钮 。 保存证书,并记住其保存到的位置。

    生成的证书下载页

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

    “下载”文件夹中找到证书文件

    注意

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

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

    显示新证书 的 密钥链访问证书列表

    注意

    尽管证书中的名称可能不同,但名称将带有 Apple Development iOS 推送服务 前缀,并具有与之关联的相应捆绑标识符。

  10. 在密钥链访问中,控制 + 单击在 证书 类别中创建的新推送证书上的。 选择 导出,命名文件,选择 p12 格式,然后选择 保存

    将证书导出为 p12 格式

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

    注意

    p12 文件名和位置可能与本教程中所示的内容不同。

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

    • 应用 ID 前缀团队 ID
    • 捆绑 ID
  2. 返回 证书、标识符 & 配置文件,单击 密钥

    注意

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

  3. 单击 + 按钮(或 创建密钥 按钮)创建新密钥。

  4. 提供合适的 密钥名称 值,然后检查 Apple Push Notifications 服务(APNS) 选项,然后单击“继续”,然后在下一屏幕上 单击“注册”。

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

    注意

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

  6. 密钥上,单击创建的密钥(或者选择使用该密钥的现有密钥)。

  7. 记下 密钥 ID 值。

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

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

    注意

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

在这些步骤结束时,应该有以下信息供稍后在 使用 APNS 信息配置通知中心

  • 团队 ID(请参阅步骤 1)
  • 捆绑 ID(请参阅步骤 1)
  • 密钥 ID(请参阅步骤 7)
  • 令牌值(步骤 8 中获取的 p8 密钥值)

为应用创建预配配置文件

  1. 返回到 iOS 预配门户,选择 证书、标识符 & 配置文件,从左侧菜单中选择 配置文件,然后选择 + 创建新配置文件。 此时会显示“注册新的预配配置文件” 屏幕。

  2. 选择 开发 下的 iOS 应用开发 作为预配配置文件类型,然后选择 继续

    配置文件列表

  3. 接下来,选择从 应用 ID 下拉列表中创建的应用 ID,然后选择 继续

    选择应用 ID

  4. 在“选择证书 窗口中,选择用于代码签名的开发证书,然后选择 继续

    注意

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

  5. 返回到 证书,标识符 & 配置文件 页,从左侧菜单中选择 配置文件,然后选择 + 创建新配置文件。 此时会显示“注册新的预配配置文件” 屏幕。

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

  7. 接下来,选择要用于测试的设备,然后选择 继续

  8. 最后,在 预配配置文件名称中选择配置文件的名称,然后选择 生成

    选择预配配置文件名称

  9. 创建新的预配配置文件时,请选择“下载。 请记住保存它的位置。

  10. 浏览到预配配置文件的位置,然后双击它以在开发计算机上安装它。

创建通知中心

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

  1. 登录到 Azure

  2. 单击 创建资源,然后搜索并选择 通知中心,然后单击 创建

  3. 更新以下字段,然后单击“创建

    基本详细信息

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

    命名空间详细信息

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

    注意

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

    通知中心详细信息

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

    注意

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

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

  5. 导航到新的 通知中心

  6. 从列表中选择 访问策略MANAGE下)。

  7. 记下 策略名称 值及其相应的 连接字符串 值。

使用 APNS 信息配置通知中心

通知服务下,选择 Apple,然后根据之前在 创建通知中心证书 部分中选择的方法执行相应的步骤。

注意

仅当想要向从应用商店购买应用的用户发送推送通知时,才对 应用程序模式 使用 生产

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

  1. 选择 证书

  2. 选择文件图标。

  3. 选择之前导出的 .p12 文件,然后选择 打开

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

  5. 选择 沙盒 模式。

  6. 选择 保存

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

  1. 选择 令牌

  2. 输入之前获取的以下值:

    • 密钥 ID
    • 捆绑 ID
    • 团队 ID
    • 令牌
  3. 选择 沙盒

  4. 选择 保存

使用 FCM 信息配置通知中心

  1. 在左侧菜单中 设置 部分选择 Google (GCM/FCM)
  2. 输入从 Google Firebase 控制台中记录的 服务器密钥
  3. 选择工具栏上的 保存

创建 ASP.NET Core Web API 后端应用程序

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

创建 Web 项目

  1. Visual Studio中,选择 文件>新解决方案

  2. 选择 .NET Core>应用>ASP.NET Core>API>下一

  3. 配置新的 ASP.NET Core Web API 对话框中,选择 .NET Core 3.1 目标框架。

  4. 项目名称 输入 PushDemoApi,然后选择 创建

  5. 开始调试(命令 + Enter)以测试模板化应用。

    注意

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

    如果系统提示你出现 找不到开发证书 消息:

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

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

  6. 展开 控制器 文件夹,然后删除 WeatherForecastController.cs

  7. 删除 WeatherForecast.cs

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

    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 密钥不如令牌那么安全,但对于本教程而言,API 密钥就足够了。 可以通过 ASP.NET 中间件轻松配置 API 密钥。

  1. API 密钥 添加到本地配置值。

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

    注意

    应将占位符值替换为你自己的占位符值,并记下它。

  2. 控制在 pushDemoApi 项目中单击 ,从“添加”菜单中选择 “新建文件夹”,然后使用 身份验证 作为 文件夹名称单击 添加

  3. 控制 + 单击 身份验证 文件夹中的,然后从 “添加”菜单中选择 “新建文件...”

  4. 选择 常规>空类,输入 名称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; }
        }
    }
    
  5. 将另一个 空类 添加到名为 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 密钥方案。

  6. 将另一个 空类 添加到名为 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 中的中间件配置代码,使其更易于阅读且通常更易于遵循。

  7. Startup.cs中,更新 ConfigureServices 方法,以在调用 服务时配置 API 密钥身份验证。AddControllers 方法。

    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);
    }
    
  8. 仍在 Startup.cs中,更新 Configure 方法,以在应用的 IApplicationBuilder上调用 UseAuthenticationUseAuthorization 扩展方法。 确保在 UseRouting 之后和 应用之前调用这些方法。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 封装在服务中。 该服务通过适当的抽象进行注册和提供。

  1. 控制 + 单击 依赖项 文件夹上的,然后选择 “管理 NuGet 包...”

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

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

  4. Control + Click on the PushDemoApi project, choose New Folder from the Add menu, then click Add using Models as the Folder Name.

  5. 控制 + 单击 模型 文件夹中的然后从 “添加”菜单中选择“新建文件...”

  6. 选择 “常规空类,输入 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)\" }";
            }
        }
    }
    

    注意

    此类包含此方案所需的泛型通知和无提示通知的标记化通知有效负载。 有效负载在 安装 外部定义,以允许试验,而无需通过服务更新现有安装。 以这种方式处理对安装的更改不适用于本教程。 对于生产,请考虑 自定义模板

  7. 将另一个 空类 添加到名为 DeviceInstallation.csModels 文件夹中,然后添加以下实现。

    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>();
        }
    }
    
  8. 将另一个 空类 添加到名为 NotificationRequest.csModels 文件夹中,然后添加以下实现。

    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; }
        }
    }
    
  9. 将另一个 空类 添加到名为 NotificationHubOptions.csModels 文件夹中,然后添加以下实现。

    using System.ComponentModel.DataAnnotations;
    
    namespace PushDemoApi.Models
    {
        public class NotificationHubOptions
        {
            [Required]
            public string Name { get; set; }
    
            [Required]
            public string ConnectionString { get; set; }
        }
    }
    
  10. 将新文件夹添加到名为 ServicesPushDemoApi 项目。

  11. 空接口 添加到名为 INotificationService.csServices 文件夹中,然后添加以下实现。

    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);
        }
    }
    
  12. 空类 添加到名为 NotificationHubsService.csServices 文件夹中,然后添加以下代码来实现 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;
                else
                    return false;
    
                try
                {
                    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;
    
                try
                {
                    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);
    
                try
                {
                    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);
                    }
                    else
                    {
                        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,但在这种情况下,表达式仅包含 OU(||)。 如果请求中有 20 多个标记,则必须将其拆分为多个请求。 有关更多详细信息,请参阅 路由和标记表达式 文档。

  13. 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

  1. 控制 + 单击 控制器 文件夹中的,然后 从“添加”菜单中选择 “新建文件...”

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

    注意

    如果使用 Visual Studio 2019,请选择 模板 API 控制器以及读写操作。

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

    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;
    
  4. 更新模板化控制器,使其派生自 ControllerBase,并使用 ApiController 属性进行修饰。

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

    注意

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

  5. 如果选择使用 API 密钥 部分完成 身份验证客户端,则还应使用 Authorize 属性修饰 NotificationsController

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

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

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

    注意

    Visual Studio 可能不会在浏览器中自动启动应用。 你将使用 Postman 从此开始测试 API。

  9. 在新的 Postman 选项卡上,将请求设置为 GET。 输入下面的地址,将占位符 <applicationUrl> 替换为在 Properties>launchSettings.json中找到的 https applicationUrl

    <applicationUrl>/api/notifications
    

    注意

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

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

    钥匙 价值
    apikey <your_api_key>
  11. 单击“发送”按钮

    注意

    应收到一些 JSON 内容 200 正常 状态。

    如果收到 SSL 证书验证 警告,则可以在 设置中切换 postman 请求 SSL 证书 验证。

  12. NotificationsController.cs 中的模板化类方法替换为以下代码。

    [HttpPut]
    [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 应用

  1. 登录到 Azure 门户

  2. 单击 创建资源,然后搜索并选择 API 应用,然后单击 创建

  3. 更新以下字段,然后单击 创建

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

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

    资源组:
    在其中创建了通知中心,请选择相同的 资源组。

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

    注意

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

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

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

  5. 记下 概述顶部 Essentials 摘要中的 URL 属性。 此 URL 是 后端终结点,将在本教程后面部分使用。

    注意

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

  6. 从列表中选择 配置(在 设置下)。

  7. 对于以下每个设置,请单击 “新建应用程序”设置 以输入 名称,然后单击 “确定”

    名字 价值
    Authentication:ApiKey <api_key_value>
    NotificationHub:Name <hub_name_value>
    NotificationHub:ConnectionString <hub_connection_string_value>

    注意

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

  8. 添加所有应用程序设置后,单击 保存,然后 继续

发布后端服务

接下来,将应用部署到 API 应用,使其可从所有设备访问。

注意

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

  1. 如果尚未这样做,请将配置从 调试 更改为 发布

  2. 控制 + 单击 PushDemoApi 项目然后从 “发布”菜单中选择“发布到 Azure...”

  3. 如果系统提示这样做,请按照身份验证流进行操作。 使用在上一 中创建 API 应用 部分所用的帐户。

  4. 选择之前从列表中创建 Azure 应用服务 API 应用 作为发布目标,然后单击 发布

完成向导后,它会将应用发布到 Azure,然后打开该应用。 如果尚未这样做,请记下 URL。 此 URL 是本教程稍后使用的 后端终结点

验证已发布的 API

  1. Postman 打开一个新选项卡,将请求设置为 PUT,然后输入以下地址。 将占位符替换为在上一 发布后端服务 部分记下的基本地址。

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

    注意

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

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

    钥匙 价值
    apikey <your_api_key>
  3. 正文选择 原始 选项,然后从格式选项列表中选择 JSON,然后包括一些占位符 JSON 内容:

    {}
    
  4. 单击 发送

    注意

    应从服务收到 422 UnprocessableEntity 状态。

  5. 再次执行步骤 1-4,但这次指定请求终结点以验证收到 400 错误的请求 响应。

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

注意

目前无法使用有效的请求数据测试 API,因为这需要来自客户端移动应用的平台特定信息。

创建跨平台 Flutter 应用程序

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

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

指定操作且应用处于前台时,将显示警报。 否则,通知将显示在通知中心。

注意

通常在应用程序生命周期中的适当时间点(或作为首次运行体验的一部分)期间执行注册(和取消注册)操作,而无需显式用户注册/取消注册输入。 但是,此示例需要显式用户输入才能更轻松地浏览和测试此功能。

创建 Flutter 解决方案

  1. 打开 Visual Studio Code的新实例。

  2. 打开 命令面板Shift + 命令 + P)。

  3. 选择“Flutter:新建项目 命令,然后按 Enter

  4. 输入 push_demo项目名称,然后选择 项目位置

  5. 当系统提示执行此操作时,请选择“获取包”

  6. 控制单击 kotlin 文件夹中的(在 应用src下),然后选择“在 Finder中显示 显示”。 然后,将子文件夹(kotlin 文件夹下)分别重命名为 com<your_organization>pushdemo

    注意

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

    • kotlin
      • com
        • mobcat
          • pushdemo
  7. 返回 visual Studio Code ,将 android>应用中的 applicationId 值更新为>build.gradlecom.<your_organization>.pushdemo

    注意

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

  8. 更新 AndroidManifest.xml 文件中的 属性,在 src>调试下,src 分别>src>配置文件。 确保值与上一步中使用的 applicationId 匹配。

    <manifest
        xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.<your_organization>.pushdemo>">
        ...
    </manifest>
    
  9. 将 srcmain 下的 AndroidManifest.xml 文件中的 属性更新为 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>
    
  10. 打开 app-level build.gradle 文件(android>app>build.gradle),然后更新 compileSdkVersion(从 android 部分)使用 API 29。 然后,分别将 minSdkVersiontargetSdkVersion 值(从 defaultConfig 部分)更新为 2629

    注意

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

  11. 控制单击 ios 文件夹中的,然后选择“在 Xcode中打开

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

    注意

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

  13. 单击 Info.plist,然后将 捆绑包名称 值更新为 PushDemo

  14. 关闭 Xcode 并返回到 visual Studio Code

  15. 返回 visual Studio Code,打开 pubspec.yaml,添加 httpflutter_secure_storageDart 包 作为依赖项。 然后,保存该文件,并在系统提示时单击 获取包

    dependencies:
      flutter:
        sdk: flutter
    
      http: ^0.12.1
      flutter_secure_storage: ^3.3.3
    
  16. 终端中,将目录更改为 ios 文件夹(适用于 Flutter 项目)。 然后,执行 pod install 命令来安装新 Pod(flutter_secure_storage 包需要)。

  17. 控制 + 单击 文件夹中的,然后使用 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>[],
            )
          )
        );
      }
    }
    
  18. 在 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));
    
  19. 终端中,在每个目标平台上生成并运行应用,以测试模板化应用在设备上运行。 确保受支持的设备已连接。

    flutter run
    

实现跨平台组件

  1. 控制 + 单击 文件夹中的,然后使用 模型 作为 文件夹名称从菜单中选择 新建文件夹

  2. 控制 + 单击 模型 文件夹上的,然后使用 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,
        };
    }
    
  3. 将新文件添加到 模型 文件夹中,该文件夹称为 push_demo_action.dart 定义此示例中支持的操作的枚举。

    enum PushDemoAction {
      actionA,
      actionB,
    }
    
  4. 将一个新文件夹添加到名为“服务”的项目, 然后使用以下实现将名为 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 有助于与基础本机平台进行双向异步通信。 此通道的平台特定对应项将在后续步骤中创建。

  5. 使用以下实现将另一个文件添加到名为 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 准备好处理操作后,回顾性地检查在应用程序启动期间是否指定了操作。 例如,通过点击通知中心的通知启动应用时。

  6. 使用以下实现将新文件添加到名为 notification_registration_service.dart服务 文件夹中。

    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 参数。

  7. 使用以下实现将新文件添加到名为 config.dartlib 文件夹中。

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

    注意

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

    请务必将此内容添加到 gitignore 文件,以避免将这些机密提交到源代码管理。

实现跨平台 UI

  1. 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,
              ),
            ],
          ),
        ),
      );
    }
    
  2. 将必备导入添加到 main_page.dart 文件的顶部。

    import 'package:push_demo/services/notification_registration_service.dart';
    import 'config.dart';
    
  3. 将字段添加到 _MainPageState 类,以存储对 NotificationRegistrationService的引用。

    final notificationRegistrationService = NotificationRegistrationService(Config.backendServiceEndpoint, Config.apiKey);
    
  4. _MainPageState 类中,为 RegisterDeregister 按钮实现 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();
                  },
                ),
              ],
            );
          },
        );
      }
    
  5. 现在,在 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';
    
  6. 声明变量以存储对 NotificationActionService 实例的引用 并初始化它。

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

    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();
                },
              ),
            ],
          );
        },
      );
    }
    
  8. 更新 函数,观察 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 文件

  1. 控制 + 单击 android 文件夹中的,然后选择 在 Android Studio中打开。 然后,切换到 项目 视图(如果尚未)。

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

配置生成设置和权限

  1. 项目 视图切换到 Android

  2. 打开 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

  1. Android Studio中,打开项目级 build.gradle 文件(Gradle 脚本>build.gradle(Project: android))。 并确保 节点 依赖项中有“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 Project时,请确保根据 Firebase 控制台 中提供的说明引用最新版本。

  2. 在应用级 build.gradle 文件中(Gradle 脚本>build.gradle(模块:应用)),应用 Google Services Gradle 插件。 在 android 节点上方应用插件。

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

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

    注意

    请确保根据 Cloud Messaging Android 客户端文档引用最新版本。

  4. 保存更改,然后单击“立即 同步”按钮(从工具栏提示符)或 使用 Gradle 文件同步项目

处理适用于 Android 的推送通知

  1. Android Studio中,Control单击 com 上的your_organization.pushdemo 包文件夹(应用srckotlin),从 “新建”菜单中选择 。 输入 服务 作为名称,然后按 返回

  2. 控制 + 单击 服务 文件夹中的从“新建”菜单中选择 Kotlin 文件/类。 输入 DeviceInstallationService 作为名称,然后按 Return

  3. 使用以下代码实现 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)作为通知中心注册有效负载的一部分。

  4. 将另一个 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 通道实现特定于平台的对应项。 这是在 app 的 Flutter 部分中定义的,NotificationRegistrationService.dart。 在这种情况下,将从本机主机调用公共代码。 同样,请谨慎地将 <your_organization> 替换为自己的组织,无论使用何处。

  5. 将另一 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 通道实现特定于平台的对应项。 这是在 app 的 Flutter 部分中定义的,NotificationActionService.dart。 在这种情况下,可以双向调用。 请务必将 <your_organization> 替换为自己的组织,无论使用何处。

  6. 将新的 Kotlin 文件/类 添加到 com。your_organization.pushdemo 包,称为 pushNotificationsFirebaseMessagingService,然后使用以下代码实现。

    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 函数来重新生成 Firebase 令牌时,它还会在 NotificationRegistrationService 上调用 refreshRegistration

    同样,请谨慎地将 <your_organization> 替换为自己的组织,无论使用何处。

  7. AndroidManifest.xml应用>src>),使用 com.google.firebase.MESSAGING_EVENT 意向筛选器将 PushNotificationsFirebaseMessagingService 添加到 应用程序 元素的底部。

    <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>
    
  8. 返回 DeviceInstallationService,确保文件顶部存在以下导入。

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

    注意

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

  9. 更新占位符文本 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
    }
    
  10. 在 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> 替换为自己的组织价值。

  11. 添加变量以存储对 DeviceInstallationService的引用。

    private lateinit var deviceInstallationService: DeviceInstallationService
    
  12. 添加名为 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)
                }
            }
        }
    }
    
  13. 重写 onNewIntent 函数 以调用 processNotificationActions

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

    注意

    由于 MainActivityLaunchMode 设置为 SingleTop,因此将通过 on 意向 发送到现有 活动 实例NewIntent 函数,而不是 onCreate 函数,因此必须在 onCreateonNewIntent 函数中处理传入 意向

  14. 重写 onCreate 函数的 ,将 deviceInstallationService 设置为 DeviceInstallationService的新实例。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    
        flutterEngine?.let {
            deviceInstallationService = DeviceInstallationService(context, it)
        }
    }
    
  15. 在 pushNotificationFirebaseMessagingServices上设置 notificationActionServicenotificationRegistrationService 属性。

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

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

    processNotificationActions(this.intent, true)
    

注意

每次运行应用时,都必须重新注册该应用,并阻止它从调试会话中停止,以继续接收推送通知。

为推送通知配置本机 iOS 项目

配置运行程序目标和 Info.plist

  1. Visual Studio Code中,控件 + 单击 ios 文件夹中的,然后选择 “在 Xcode中打开”。

  2. Xcode中,单击 运行程序(顶部 xcodeproj,而不是文件夹),然后选择 运行程序 目标,然后 签名 & 功能。 选中“所有 生成配置”后,为 团队选择开发人员帐户。 确保选中“自动管理签名”选项,并自动选择签名证书和预配配置文件。

    注意

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

  3. 单击 + 功能,然后搜索 推送通知双击 推送通知 上的 以添加此功能。

  4. 打开 Info.plist 并将 最低系统版本 设置为 13.0

    注意

    出于本教程的目的,仅支持运行 iOS 13.0 及更高版本的设备,但你可以扩展它以支持运行旧版本的设备。

  5. 打开 Runner.entitlements 并确保将 APS 环境 设置设置为 开发

处理 iOS 的推送通知

  1. 控制 + 单击 运行程序 文件夹(在运行程序项目中)上的,然后选择 使用 服务 作为名称的新组

  2. 控制单击 服务 文件夹中的,然后选择 新建文件...。然后选择 Swift 文件,然后单击“下一步”。 为名称指定 DeviceInstallationService,然后单击“创建

  3. 使用以下代码实现 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) {
            do {
                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 值)作为通知中心注册有效负载的一部分。

  4. 将另一个 Swift 文件 添加到 名为 notificationRegistrationServiceServices 文件夹中,然后添加以下代码。

    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 通道实现特定于平台的对应项。 这是在 app 的 Flutter 部分中定义的,NotificationRegistrationService.dart。 在这种情况下,将从本机主机调用公共代码。 同样,请谨慎地将 <your_organization> 替换为自己的组织,无论使用何处。

  5. 将另一 Swift 文件 添加到名为 NotificationActionServiceServices 文件夹中,然后添加以下代码。

    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 通道实现特定于平台的对应项。 这是在 app 的 Flutter 部分中定义的,NotificationActionService.dart。 在这种情况下,可以双向调用。 请务必将 <your_organization> 替换为自己的组织,无论使用何处。

  6. AppDelegate.swift中,添加变量以存储对之前创建的服务的引用。

    var deviceInstallationService : DeviceInstallationService?
    var notificationRegistrationService : NotificationRegistrationService?
    var notificationActionService : NotificationActionService?
    
  7. 添加名为 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)
            }
        }
    }
    
  8. 重写 didRegisterForRemoteNotificationsWithDeviceToken 函数设置 DeviceInstallationService令牌 值。 然后,在 NotificationRegistrationService上调用 refreshRegistration

    override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
      deviceInstallationService?.token = deviceToken
      notificationRegistrationService?.refreshRegistration()
    }
    
  9. 重写 didReceiveRemoteNotification 函数,将 userInfo 参数传递给 processNotificationActions 函数。

    override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
        processNotificationActions(userInfo: userInfo)
    }
    
  10. 重写 didFailToRegisterForRemoteNotificationsWithError 函数以记录错误。

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

    注意

    这是一个非常占位符。 你需要为生产方案实现正确的日志记录和错误处理。

  11. didFinishLaunchingWithOptions中,实例化 deviceInstallationServicenotificationRegistrationServicenotificationActionService 变量。

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

    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()
              }
          }
      }
    }
    
  13. 如果 launchOptions 包含 remoteNotification 键,请在 didFinishLaunchingWithOptions 函数末尾 调用 processNotificationActions。 传入生成的 userInfo 对象,并为 launchAction 参数使用 truetrue 值表示在应用启动期间正在处理操作。

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

测试解决方案

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

发送测试通知

  1. Postman中打开一个新选项卡。

  2. 将请求设置为 POST,并输入以下地址:

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

    钥匙 价值
    apikey <your_api_key>
  4. 正文选择 原始 选项,然后从格式选项列表中选择 JSON,然后包括一些占位符 JSON 内容:

    {
        "text": "Message from Postman!",
        "action": "action_a"
    }
    
  5. 选择 代码 按钮,该按钮位于窗口右上角 “保存”按钮下。 当为 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"
    }
    
  6. 在一个或两个目标平台上运行 PushDemo 应用程序(AndroidiOS)。

    注意

    如果在 android 上进行测试 请确保未在调试运行,或者通过运行应用程序部署应用,然后强制关闭应用并从启动器重新启动它。

  7. PushDemo 应用中,点击“注册”按钮。

  8. 返回 Postman,关闭 生成代码片段 窗口(如果尚未这样做),然后单击 发送 按钮。

  9. 验证是否在 Postman 中收到 200 正常 响应,并且警报显示在显示收到ActionA 操作的应用中。

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

  11. 验证是否在 Postman 中收到 200 正常 响应。 使用正确的消息验证 PushDemo 应用的通知区域中是否显示通知。

  12. 点击通知以确认它打开应用并显示 ActionA 操作已收到 警报。

  13. 返回 Postman,修改上一个请求正文以发送一个无提示通知,指定 action_b 而不是 操作 值的 action_a

    {
        "action": "action_b",
        "silent": true
    }
    
  14. 应用仍然打开后,单击 Postman中的 发送 按钮。

  15. 验证是否在 Postman 中收到 200 正常 响应,并且警报显示在 收到的 ActionB 操作,而不是收到的 actionA 操作

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

  17. 验证是否在 Postman 中收到 200 正常 响应,并且无提示通知不会显示在通知区域中。

故障 排除

后端服务没有响应

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

如果针对 azure API 应用进行测试,请检查服务是否正在运行并已部署,并且已启动且未出错。

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

启动或停止调试会话后未在 Android 上收到通知

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

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

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

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

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

注意

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

如果选择不使用 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/

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

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

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

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

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

查看特定于平台的配置部分,确保未错过任何步骤。 检查是否正在为相应平台的 installation idtoken 变量解析合适的值。

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

查看特定于平台的配置部分,确保未错过任何步骤。

后续步骤

现在,应该有一个基本 Flutter 应用通过后端服务连接到通知中心,并且可以发送和接收通知。

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

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