共用方式為


教學課程:透過後端服務使用 Azure 通知中樞將推播通知傳送至 Flutter 應用程式

下載範例 下載範例

在本教學課程中,您會使用 Azure 通知中樞將通知推送至以 AndroidiOS 為目標的 Flutter 應用程式。

ASP.NET Core Web API 後端是用來使用最新且最佳的安裝方法來處理客戶端的裝置註冊。 此服務也會以跨平臺的方式傳送推播通知。

這些作業是使用 適用於後端作業的通知中樞 SDK 來處理。 如需整體方法的進一步詳細數據,請參閱 從您的應用程式後端註冊 檔。

本教學課程會引導您完成下列步驟:

Prerequisites

若要跟著做,您需要:

針對 Android,您必須具備:

  • 開發人員已解除鎖定實體裝置或模擬器 , (執行 API 26 和更新版本,且已安裝 Google Play Services)

針對 iOS,您必須具備:

注意

iOS 模擬器不支援遠端通知,因此在 iOS 上探索此範例時需要實體裝置。 不過,您不需要在 AndroidiOS 上執行應用程式,才能完成本教學課程。

您可以遵循此第一個原則範例中的步驟,且沒有先前的經驗。 不過,您將受益於熟悉下列層面。

提供的步驟是 macOS 專屬的步驟。 可以略過 iOS 層面,在 Windows 上跟著操作。

設定推播通知服務和 Azure 通知中樞

在本節中,您會設定 Firebase 雲端通訊 (FCM) Apple Push Notification Services (APNS) 。 接著,您可以建立並設定通知中樞來使用這些服務。

建立 Firebase 專案並啟用 Android 的 Firebase 雲端通訊

  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. 切換至頂端的 [ 雲端通訊] 索引 標籤。 複製並儲存 伺服器金鑰 以供稍後使用。 您可以使用此值來設定通知中樞。

    複製伺服器金鑰

註冊 iOS 應用程式以取得推播通知

若要將推播通知傳送至 iOS 應用程式,請向 Apple 註冊您的應用程式,以及註冊推播通知。

  1. 如果您尚未註冊您的應用程式,請流覽至 Apple 開發人員中心的 iOS 佈建入口網站 。 使用您的 Apple ID 登入入口網站,流覽至 [ 憑證]、[標識子 & 配置檔],然後選取 [ 標識符]。 按兩下 + 即可註冊新的應用程式。

    iOS 布建入口網站應用程式識別碼頁面

  2. 在 [ 註冊新的標識符 ] 畫面上,選取 [ 應用程式標識符] 單選按鈕。 然後選取 [繼續]。

    iOS 布建入口網站註冊新的標識碼頁面

  3. 更新新應用程式的下列三個值,然後選取 [ 繼續]:

    • 描述:輸入應用程式的描述性名稱。

    • 套件組合標識碼:輸入 com.organization_identifier<窗體的套件組合標識碼>。<如應用程式散發指南中所述product_name> 在下列螢幕快照中,此值 mobcat 會當做組織標識碼使用,並將 PushDemo 值當做產品名稱使用。

      iOS 布建入口網站註冊應用程式標識碼頁面

    • 推播通知:檢查 [功能] 區段中的 [推播通知] 選項。

      註冊新應用程式識別碼的表單

      此動作會產生您的應用程式識別碼,並要求您確認資訊。 選取 [繼續],然後選取 [ 註冊 ] 以確認新的應用程式標識符。

      確認新的應用程式識別碼

      選取 [ 註冊] 之後,您會在 [ 憑證]、[標識符] & [配置檔 ] 頁面中看到新的應用程式識別符作為明細專案。

  4. 在 [ 憑證]、[標識符 & 配置檔] 頁面的 [ 標識符] 下,找出您所建立的應用程式標識符明細專案。 然後,選取其數據列以顯示 [編輯您的應用程式識別符設定 ] 畫面。

建立通知中樞的憑證

必須有憑證,才能讓通知中樞使用 Apple Push Notification Services (APNS) ,而且可以使用下列兩種方式之一提供:

  1. 建立 p12 推播憑證,直接上傳至通知中樞 , (原始方法)

  2. 建立 p8 憑證,以用於令牌型驗證 , (較新的和建議方法)

較新的方法有許多優點,如 APNS的令牌型 (HTTP/2) 驗證中所述。 需要較少的步驟,但也適用於特定案例。 不過,已針對這兩種方法提供步驟,因為任一種方法都適用於本教學課程的目的。

選項 1:建立可直接上傳至通知中樞的 p12 推播憑證
  1. 在 Mac 上,執行 Keychain 存取工具。 您可以從 [公用程式 ] 資料夾或 Launchpad 上的 [其他 ] 資料夾開啟它。

  2. 選取 [金鑰鏈存取],展開 [ 憑證助理],然後 從證書頒發機構單位選取 [要求憑證]。

    使用 Keychain 存取來要求新的憑證

    注意

    根據預設,Keychain Access 會選取清單中的第一個專案。 如果您位於 [憑證 ] 類別中,而 Apple 全球開發人員關係證書頒發機構單位 不是清單中的第一個專案,這可能會是問題。 產生 CSR (憑證簽署要求) 之前,請確定您有非密鑰專案或 Apple 全球開發人員關係證書頒發機構單位 金鑰。

  3. 選取 [使用者 Email 位址],輸入您的 [一般名稱] 值,確定您指定 [已儲存到磁碟],然後選取 [繼續]。 將 CA Email 位址保留空白,因為不需要。

    預期的憑證資訊

  4. 在 [存新檔] 中輸入憑證簽署要求 (CSR) 檔案的名稱,選取 [位置] 中的位置,然後選取 [儲存]。

    選擇憑證的檔名

    此動作會將 CSR 檔案 儲存在選取的位置。 默認位置為 Desktop。 請記住為檔案選擇的位置。

  5. 回到iOS布建入口網站中的 [憑證]、[標識符 & 配置檔] 頁面,向下卷動至核取的 [推播通知] 選項,然後選取 [設定] 以建立憑證。

    編輯應用程式識別碼頁面

  6. [Apple 推播通知服務 TLS/SSL 憑證] 視窗隨即出現。 選取 [開發 TLS/SSL 憑證] 區段底下的 [建立憑證] 按鈕。

    建立應用程式識別碼按鈕的憑證

    [ 建立新的憑證 ] 畫面隨即顯示。

    注意

    本教學課程使用開發憑證。 註冊生產憑證時會使用相同的程式。 只要確定您在傳送通知時使用相同的憑證類型。

  7. 選取 [選擇檔案],流覽至您儲存 CSR 檔案的位置,然後按兩下憑證名稱以載入它。 然後選取 [繼續]。

  8. 入口網站建立憑證之後,選取 [ 下載] 按鈕。 儲存憑證,並記住其儲存位置。

    產生的憑證下載頁面

    憑證會下載並儲存到 [ 下載 ] 資料夾中的電腦。

    在 [下載] 資料夾中找出憑證檔案

    注意

    根據預設,下載的開發憑證會命名 為 aps_development.cer

  9. 按兩下下載的推播憑證 aps_development.cer。 此動作會在 Keychain 中安裝新的憑證,如下圖所示:

    顯示新憑證的 Keychain 存取憑證清單

    注意

    雖然憑證中的名稱可能不同,但名稱前面會加上 Apple Development iOS Push Services ,並具有與其相關聯的適當套件組合標識碼。

  10. 在 [金鑰鏈存取] 中, 控制 + 按下 您在 [ 憑證 ] 類別中建立的新推播憑證。 選取 [匯出],將檔案命名為 ,選取 p12 格式,然後選取 [ 儲存]。

    將憑證匯出為 p12 格式

    您可以選擇使用密碼保護憑證,但密碼是選擇性的。 如果您想要略過密碼建立,請按兩下 [ 確定 ]。 記下匯出 p12 憑證的檔名和位置。 它們用來啟用對 APN 的驗證。

    注意

    您的 p12 檔名和位置可能與本教學課程中所描述的內容不同。

選項 2:建立可用於令牌型驗證的 p8 憑證
  1. 記下下列詳細資料:

    • 應用程式標識碼前置 詞 (小組標識子)
    • 套件組合標識碼
  2. 回到 [憑證],[標識符] & 配置檔中,按兩下 [ 密鑰]。

    注意

    如果您已經針對 APNS設定金鑰,則可以在建立後立即重複使用您下載的 p8 憑證。 如果是,您可以忽略步驟 35

  3. +按鍵 (或 [建立金鑰] 按鈕) 來建立新的金鑰。

  4. 提供適當的 [金鑰名稱] 值,然後核取 [Apple Push Notifications 服務] ([APNS) ] 選項,然後按兩下 [ 繼續],然後在下一個畫面上按兩下 [ 註冊 ]。

  5. 按兩下 [下載],然後將前面加上 AuthKey_) p8 (檔案移至安全的本機目錄,然後按兩下 [完成]。

    注意

    請務必將 p8 檔案保留在安全的位置, (並儲存備份) 。 下載金鑰之後,無法重新下載金鑰,因為伺服器複本已移除。

  6. [金鑰] 上,如果您選擇改用該金鑰,請按下您建立的密鑰 (或現有的密鑰) 。

  7. 記下 [金鑰識別碼 ] 值。

  8. 在您選擇的適當應用程式中開啟 p8 憑證,例如 Visual Studio Code。 請記下 -----BEGIN 私鑰 ( -----和 -----END PRIVATE KEY 之間的金鑰值-----) 。

    -----BEGIN 私鑰-----
    <key_value>
    -----END 私鑰-----

    注意

    這是稍後用來設定通知中樞令牌值

在這些步驟結束時,您應該會有下列資訊,以供稍後 使用APNS資訊設定通知中樞

  • 小組標識子 (請參閱步驟 1)
  • 套件組合標識碼 (請參閱步驟 1)
  • 密鑰標識碼 (請參閱步驟 7)
  • 在步驟 8) 中取得的令牌值 (p8 金鑰值

建立應用程式的布建配置檔

  1. 返回 iOS 布建入口網站,選取 [ 憑證]、[標識符 & 配置檔]、從左側功能表中選取 [ 配置檔 ],然後選取 + 以建立新的配置檔。 [ 註冊新的布建配置檔 ] 畫面隨即出現。

  2. 選取 [開發] 下的 [iOS 應用程式開發] 作為布建配置檔類型,然後選取 [繼續]。

    布建配置檔清單

  3. 接下來,從 [ 應用程式 標識符] 下拉式清單中選取您建立的應用程式識別碼,然後選取 [ 繼續]。

    選取應用程式識別碼

  4. 在 [ 選取憑證 ] 視窗中,選取您用於程式代碼簽署的開發憑證,然後選取 [ 繼續]。

    注意

    此憑證不是您在 上一個步驟中建立的推播憑證。 這是您的開發憑證。 如果不存在,您必須建立它,因為這是本教學課程的必要 條件 。 開發人員憑證可以透過 XcodeVisual Studio,在 Apple Developer Portal 中建立。

  5. 返回 [ 憑證]、[標識符 & 配置檔] 頁面,從左側功能表中選取 [ 配置檔 ],然後選取 + 以建立新的配置檔。 [ 註冊新的布建配置檔 ] 畫面隨即出現。

  6. 在 [ 選取憑證] 視窗中,選取您建立的開發憑證。 然後選取 [繼續]。

  7. 接下來,選取要用於測試的裝置,然後選取 [ 繼續]。

  8. 最後,在 [ 布建配置檔名稱] 中選擇設定檔的名稱,然後選取 [ 產生]。

    選擇布建配置檔名稱

  9. 建立新的布建配置檔時,請選取 [ 下載]。 請記住儲存的位置。

  10. 流覽至布建配置檔的位置,然後按兩下它,將它安裝在您的開發電腦上。

建立通知中樞

在本節中,您會建立通知中樞,並使用 APNS設定驗證。 您可以使用 p12 推播憑證或令牌型驗證。 如果您想要使用已建立的通知中樞,您可以跳至步驟 5。

  1. 登入 Azure

  2. 按兩下 [建立資源],然後搜尋並選擇 [通知中樞],然後按兩下 [ 建立]。

  3. 更新下列欄位,然後按兩下列欄位[ 建立]:

    基本詳細數據

    訂閱: 從下拉式清單中選擇目標 用帳戶
    資源群組: 建立新的 資源群組 (或挑選現有的資源群組)

    命名空間詳細數據

    通知中樞命名空間: 輸入 通知中樞 命名空間的全域唯一名稱

    注意

    請確定已為此欄位選取 [ 建立新 ] 選項。

    通知中樞詳細數據

    通知中樞:輸入通知中樞的名稱
    位置: 從下拉式清單中選擇適當的位置
    定價層: 保留預設 的 [免費] 選項

    注意

    除非您已達到免費層上的中樞數目上限。

  4. 布建 通知中樞 之後,請流覽至該資源。

  5. 流覽至新的 通知中樞

  6. 從 [管理) ] 底下的清單中 (選取 [存取原則]。

  7. 記下原則 名稱 值及其對應的 連接字串 值。

使用APNS資訊設定通知中樞

[通知服務] 底下,選取 [Apple ],然後根據您先前在 [ 建立通知中樞憑證 ] 區段中選擇的方法,遵循適當的步驟。

注意

只有在您想要將推播通知傳送給從市集購買應用程式的使用者時,才使用 應用程式 生產 模式

選項 1:使用 .p12 推播憑證

  1. 選取 [ 憑證]。

  2. 選取檔案圖示。

  3. 選取您稍早導出的 .p12 檔案,然後選取 [ 開啟]。

  4. 如有必要,請指定正確的密碼。

  5. 選取 [沙盒 模式]。

  6. 選取 [ 儲存]。

選項 2:使用令牌型驗證

  1. 選取 [令牌]。

  2. 輸入您稍早取得的下列值:

    • 金鑰識別碼
    • 套件組合標識碼
    • 小組標識碼
    • 令牌
  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. 開始偵錯 (命令 + 輸入) 以測試樣板化應用程式。

    注意

    範本化應用程式已設定為使用 WeatherForecastController 作為 launchUrl。 這會在 [屬性>] launchSettings.json中設定。

    如果系統提示您 找到無效的開發憑證 訊息:

    1. 按兩下 [是 ] 同意執行 'dotnet dev-certs https' 工具來修正此問題。 'dotnet dev-certs https' 工具會提示您輸入憑證的密碼和密鑰鏈的密碼。

    2. 當系統提示您安裝並信任新憑證時,請按兩下 [],然後輸入密鑰鏈的密碼。

  6. 展開 Controllers 資料夾,然後刪除 WeatherForecastController.cs

  7. 刪除 WeatherForecast.cs

  8. 使用 秘密管理員工具設定本機組態值。 將秘密與解決方案分離可確保它們最終不會出現在原始檔控制中。 開啟 [終端機 ],然後移至項目檔的目錄,然後執行下列命令:

    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 App 服務 應用程式設定。

使用 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.csAuthentication 資料夾,然後新增下列實作。

    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的 Authentication 資料夾,然後新增下列實作。

    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 和 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 會封裝在服務內。 服務會註冊並透過適當的抽象概念提供。

  1. 控制 + 按兩下 [ 相依性 ] 資料夾,然後選擇 [管理 NuGet 套件...]。

  2. 搜尋 Microsoft.Azure.NotificationHubs 並確定已核取。

  3. 按兩下 [新增套件],然後在系統提示您接受授權條款時按兩下 [ 接受 ]。

  4. 控制 + 單擊PushDemoApi 專案,從 [新增] 選單中選擇 [新增資料夾],然後按兩下 [使用模型新增] 作為 [資料夾名稱]。

  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.cs 的Models 資料夾,然後新增下列實作。

    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.cs 的Models 資料夾,然後新增下列實作。

    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.cs 的Models 資料夾,然後新增下列實作。

    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.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);
        }
    }
    
  12. 空白類別新增至名為 NotificationHubsService.cs的 Services 資料夾,然後新增下列程式代碼以實作 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. 開始偵錯 (命令 + 輸入) 驗證應用程式正在使用新的 NotificationsController ,並傳回 401 未經授權的 狀態。

    注意

    Visual Studio 可能不會在瀏覽器中自動啟動應用程式。 您將使用 Postman 從這個點開始測試 API。

  9. 在新的 Postman 索引標籤上,將要求設定為 GET。 輸入下列位址,以屬性>launchSettings.json中找到的 HTTPs applicationUrl 取代佔位元 <applicationUrl>

    <applicationUrl>/api/notifications
    

    注意

    applicationUrl 應該是預設配置檔的 『https://localhost:5001』。 如果您在 Windows) 上使用 Visual Studio 2019 中的 IIS (預設值,您應該改用 iisSettings 專案中指定的 applicationUrl。 如果位址不正確,您會收到 404 回應。

  10. 如果您選擇完成 [使用 API 金鑰驗證用戶端 ] 區段,請務必設定要求標頭以包含 apikey 值。

    關鍵 價值
    apikey <your_api_key>
  11. 按兩下 [ 傳送] 按鈕。

    注意

    您應該會收到一些 JSON 內容的 200 OK 狀態。

    如果您收到 SSL 憑證驗證警告,您可以在 [設定] 中切換要求 SSL 憑證驗證 Postman 設定。

  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 App 服務 中建立 API 應用程式,以裝載後端服務。

  1. 登入 Azure 入口網站

  2. 按兩下 [建立資源],然後搜尋並選擇 [API 應用程式],然後按兩下 [ 建立]。

  3. 更新下列欄位,然後按兩 下列欄位,然後按兩下列欄位。。

    應用程式名稱:
    輸入 API 應用程式的全域唯一名稱

    訂閱:
    選擇您在中建立通知中樞的相同目標 用帳戶。

    資源群組:
    選擇您在中建立通知中樞的相同 資源群組

    App Service 方案/位置:
    建立新的 App Service方案

    注意

    從預設選項變更為包含 SSL 支援的方案。 否則,在使用行動應用程式時,您必須採取適當的步驟,以防止 HTTP 要求遭到封鎖。

    Application Insights:
    請保留建議的選項, (使用該名稱建立新的資源,) 或挑選現有的資源。

  4. 布建 API 應用程式 之後,請瀏覽至該資源。

  5. 記下 [概] 頂端 [基本資訊] 摘要中的 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 金鑰驗證用戶端一節時,才需要 Authentication:ApiKey 設定。 針對生產案例,您可以查看 Azure KeyVault 之類的選項。 為了簡單起見,這些設定已新增為應用程式設定。

  8. 新增所有應用程式設定之後,按兩下 [ 儲存],然後按兩下 [ 繼續]。

發佈後端服務

接下來,您會將應用程式部署至 API 應用程式,使其可從所有裝置存取。

注意

下列步驟專屬於 Visual Studio for Mac。 如果您在 Windows 上使用 Visual Studio 2019 進行追蹤,發佈流程將會不同。 請參閱在 Windows 上發行至 Azure App 服務

  1. 如果您尚未這麼做,請將組態從 [偵 錯] 變更為 [發行 ]。

  2. 控制 + 單擊PushDemoApi 項目,然後從 [發佈] 功能表中選擇 [發佈至 Azure...]。

  3. 如果系統提示您這麼做,請遵循驗證流程。 使用您在上一個 建立 API 應用程式 一節中使用的帳戶。

  4. 選取您先前從清單中建立的 Azure App 服務 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. 控制 + 按兩下app>src>main) 底下的 kotlin 資料夾 (,然後選擇 [在尋找工具中顯示]。 然後,將 kotlin 資料夾底下的子資料夾 () 分別重新命名為 com<your_organization>pushdemo

    注意

    使用 Visual Studio Code 範本時,這些資料夾預設為 com例如<,project_name>。 假設 mobcat 用於 組織,資料夾結構應該會顯示為:

    • kotlin
      • Com
        • mobcat
          • pushdemo
  7. 回到 Visual Studio Code,將 android>app>build.gradle 中的 applicationId 值更新為 com.<your_organization>.pushdemo

    注意

    您應該針對 <your_organization> 佔位元使用您自己的組織名稱。 例如,使用mobcat作為組織會導致 com.mobcat.pushdemo套件名稱值。

  8. 分別在 src>錯、srcmainsrc>>配置檔底下,更新 AndroidManifest.xml 檔案中的套件屬性。 請確定這些值符合您在上一個步驟中使用的 applicationId

    <manifest
        xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.<your_organization>.pushdemo>">
        ...
    </manifest>
    
  9. android:labelsrc>main 底下的 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. 開啟應用程式層級 build.gradle 檔案, (android>應用程式>build.gradle) ,然後從 android 區段更新 compileSdkVersion (,) 以使用 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_storageFlutter_secure_storage Flutter_secure_storage的套件新增為相依性。 然後,儲存盤案,並在出現提示時按兩下 [ 取得套件 ]。

    dependencies:
      flutter:
        sdk: flutter
    
      http: ^0.12.1
      flutter_secure_storage: ^3.3.3
    
  16. 終端機中,將目錄變更為 Flutter 專案的 ios 資料夾 () 。 然後,執行 pod install 命令,以安裝 flutter_secure_storage 套件) 所需的新 Pod (。

  17. 控制 + 單擊lib 資料夾,然後選擇選單中的 [ 新增檔案 ],並使用 main_page. 做為檔名。 然後,新增下列程序代碼。

    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.程式代碼中,以下列內容取代樣板化程序代碼。

    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. 控制 + 單擊[lib] 資料夾,然後從使用模型作為 [資料夾名稱] 的功能表中選擇 [新增資料夾]。

  2. 控制 + 單擊models 資料夾,然後選擇功能表中的 [ 新增檔案 ],使用 device_installation.device_installation. 做為檔名。 然後,新增下列程序代碼。

    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. 將新的檔案新增至 models 資料夾,名為 push_demo_action.push_demo_action.push_demo_action 定義在此範例中支持的動作列舉。

    enum PushDemoAction {
      actionA,
      actionB,
    }
    
  4. 將新的資料夾新增至名為 services 的項目,然後使用下列實作,將新的檔案新增至該資料夾 ,稱為 device_installation_service.device_installation_service .

    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作為組織會導致 com.mobcat.pushdemo/deviceinstallationMethodChannel 名稱。

    此類別會封裝使用基礎原生平臺來取得必要的裝置安裝詳細數據。 MethodChannel 有助於與基礎原生平台進行雙向異步通訊。 此通道的平臺特定對應專案將在後續步驟中建立。

  5. 使用下列實作,將另一個檔案新增至該資料夾 ,稱為 notification_action_service.notification_action_service .

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

    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 應用程式 URL 應該是 https://<api_app_name>.azurewebsites.net/。 只有在您選擇使用 API 金鑰驗證客戶端一節時,才需要 apiKey 成員。

    請務必將此新增至 gitignore 檔案,以避免將這些秘密認可至原始檔控制。

實作跨平臺UI

  1. main_page.main_page.main_page中,將 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.main_page. 檔案的頂端。

    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. 更新 main 函式以觀察 NotificationActionService宏指令Triggered 資料流,並檢查在應用程式啟動期間擷取的任何動作。

    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,然後在結尾標記之前,於應用程式元素後面新增因特網READ_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 Scripts>build.gradle (Project:android) ) 。 並確定您在相依性節點中有 buildscript> 'com.google.gms:google-services' classpath。

    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 控制台中提供的指示來參考最新版本。

  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. 在相同的檔案中,於相 依性 節點中,新增 雲端傳訊 Android 連結庫的相依性。

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

    注意

    請確定您根據 雲端通訊 Android 用戶端檔參考最新版本。

  4. 儲存變更,然後按兩下工具列提示字元中的 [ 立即同步 處理] 按鈕 (,) 或使用 Gradle 檔案同步專案

處理Android的推播通知

  1. Android Studio 中,單擊 + com.your_organization.pushdemo<> 套件資料夾, (應用程式>src>main>kotlin) ,從 [新增] 功能表選擇 [套件]。 輸入 服務 作為名稱,然後按 Return

  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.內應用程式的 Flutter 部分所定義。 在此情況下,會從一般程式代碼呼叫原生主機。 請務必使用您自己的組織來取代 <your_organization>

    這個類別提供唯一標識碼 (,使用 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 。 這是在 NotificationRegistrationService.內的 App Flutter 部分所定義。 在此情況下,會從原生主機呼叫通用程序代碼。 同樣地,請小心將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 。 這是在 NotificationActionService.dart 內應用程式之 Flutter 部分所定義。 在此情況下,可以雙向呼叫。 請務必使用您自己的組織來取代 <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 函式重新產生時,這也會在 NotificationRegistrationService 上呼叫 refreshRegistration

    同樣地,請小心將your_organization>取代<為使用所在的組織。

  7. AndroidManifest.xml (應用程式>src>main) 中,使用意圖篩選將 PushNotificationsFirebaseMessagingService 新增至 應用程式 元素 com.google.firebase.MESSAGING_EVENT 底部。

    <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 的 函式,以檢查 Intent 是否有額外的名稱 動作值。 有條件地觸發該動作,或在應用程式啟動期間處理動作時儲存動作以供稍後使用。

     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因此意圖會透過 onNewIntent 函式傳送至現有的 Activity 實例,而不是 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.app 的 Flutter 部分所定義。 在此情況下,會從一般程式代碼對原生主機進行呼叫。 請務必使用您自己的組織取代 <your_organization>

    這個類別會使用 UIDevice.identifierForVendor 值) 作為通知中樞註冊承載的一部分,提供唯一標識碼 (。

  4. 將另一個 Swift 檔案新增至名為 NotificationRegistrationService的服務資料夾,然後新增下列程序代碼。

    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.應用程式內的 Flutter 部分。 在此情況下,會從原生主機呼叫一般程序代碼。 同樣地,請小心將your_organization>取代<為您使用的位置。

  5. 將另一個 Swift 檔案新增至名為 NotificationActionService的服務資料夾,然後新增下列程式代碼。

    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.應用程式內的 Flutter 部分。 在此情況下,可以雙向呼叫。 請務必使用您自己的組織取代 <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. 選擇 Body的原始選項,然後從格式選項清單中選擇 JSON,然後包含一些佔位元 JSON 內容:

    {
        "text": "Message from Postman!",
        "action": "action_a"
    }
    
  5. 選取 [ 程序代碼] 按鈕,其位於視窗右上方的 [ 儲存 ] 按鈕底下。 根據您是否包含 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. (AndroidiOS) ,在一或兩個目標平台上執行 PushDemo 應用程式。

    注意

    如果您要在 Android 上測試,請確定您未在 [ 錯] 中執行,或執行應用程式來部署應用程式,然後強制關閉應用程式,然後從啟動器再次啟動它。

  7. PushDemo 應用程式中,點選 [ 註冊] 按鈕。

  8. 回到 Postman,如果您尚未) 按兩下 [傳送] 按鈕,請關閉 [產生代碼段] 視窗 (。

  9. 驗證您在 Postman 中收到 200 OK 回應,且警示會出現在應用程式中,其中顯示已收到的 ActionA 動作

  10. 關閉 PushDemo 應用程式,然後在 Postman 中再次按兩下 [傳送] 按鈕。

  11. 再次驗證您在Postman中收到200 OK回應。 使用正確的訊息,驗證 PushDemo 應用程式的通知出現在通知區域中。

  12. 點選通知以確認它已開啟應用程式並顯示 ActionA 動作已收到 警示。

  13. 回到Postman,修改先前的要求本文以傳送無訊息通知,以指定動作action_b而不是action_a

    {
        "action": "action_b",
        "silent": true
    }
    
  14. 在應用程式仍然開啟時,按兩下Postman中的 [傳送] 按鈕。

  15. 驗證您在 Postman 中收到 200 OK 回應,且警示出現在應用程式中,其中顯示已收到的 ActionB 動作,而不是收到 ActionA 動作

  16. 關閉 PushDemo 應用程式,然後在 Postman 中再次按兩下 [傳送] 按鈕。

  17. 驗證您在 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/

無法註冊並顯示通知中樞錯誤訊息

確認測試裝置具有網路連線能力。 然後,藉由設定斷點來檢查 HttpResponse 中的 StatusCode 屬性值,以判斷 Http 回應狀態代碼。

根據狀態代碼檢閱先前的疑難解答建議。

在傳回個別 API 之這些特定狀態代碼的行上設定斷點。 然後在本機偵錯時嘗試呼叫後端服務。

使用適當的承載,驗證後端服務是否如預期般運作。 針對有問題的平臺,使用用戶端程式代碼所建立的實際承載。

檢閱平臺特定的組態區段,以確保未遺漏任何步驟。 檢查是否要針對 installation id 適當的平臺解析適當的值和 token 變數。

無法解析裝置錯誤訊息的識別碼

檢閱平臺特定的組態區段,以確保未遺漏任何步驟。

後續步驟

您現在應該已透過後端服務連線到通知中樞的基本 Flutter 應用程式,而且可以傳送和接收通知。

您可能需要調整本教學課程中使用的範例,以符合您自己的案例。 也建議您實作更強固的錯誤處理、重試邏輯和記錄。

Visual Studio App Center 可以快速併入行動裝置應用程式中,提供分析和診斷,以協助進行疑難解答。