Руководство. Отправка push-уведомлений в приложения Flutter с помощью Центров уведомлений Azure через серверную службу

скачать пример скачать пример

В этом руководстве вы используете Центры уведомлений Azure для отправки push-уведомлений в приложение Flutter, предназначенные для Android и iOS.

Серверная часть веб-API ASP.NET Core используется для обработки регистрации устройств для клиента с помощью последнего и лучшего подхода установки. Служба также отправляет push-уведомления кроссплатформенным образом.

Эти операции обрабатываются с помощью пакета SDK для центров уведомлений для внутренних операций. Дополнительные сведения о общем подходе приведены в документации по регистрации из серверной части приложения документации.

В этом руководстве описаны следующие действия.

Необходимые условия

Для выполнения дальнейших инструкций вам потребуется:

  • подписке Azure, где можно создавать ресурсы и управлять ими.
  • Набор средств Flutter (а также необходимые компоненты).
  • Visual Studio Code с установленными подключаемы ми модулями Flutter и Dart.
  • CocoaPods установлен для управления зависимостями библиотеки.
  • Возможность запуска приложения на Android (физические или эмуляторные устройства) или iOS (только физические устройства).

Для Android необходимо иметь следующее:

  • Разработчик разблокировал физическое устройство или эмулятор (с установленным API 26 и более поздних версий с установленными службами Google Play).

Для iOS необходимо:

Заметка

Симулятор iOS не поддерживает удаленные уведомления, поэтому при изучении этого примера в iOS требуется физическое устройство. Однако для выполнения этого руководства не требуется запускать приложение как на Android, так и iOS.

Вы можете выполнить действия, описанные в этом примере с использованием первых принципов без предыдущего опыта. Тем не менее, вы получите преимущество от знакомства со следующими аспектами.

Описанные действия относятся к macOS. Вы можете следовать Windows, пропуская аспекты iOS.

Настройка служб push-уведомлений и Центра уведомлений Azure

В этом разделе описано, как настроить Firebase Cloud Messaging (FCM) и службы push-уведомлений Apple (APNS). Затем вы создаете и настраиваете концентратор уведомлений для работы с этими службами.

Создание проекта Firebase и включение Firebase Cloud Messaging для Android

  1. Войдите вконсоли Firebase . Создайте проект Firebase, введющий PushDemo в качестве имени проекта.

    Заметка

    Для вас будет создано уникальное имя. По умолчанию это состоит из нижнего регистра имени, указанного плюс созданного числа, разделенного дефисом. Это можно изменить, если вы хотите предоставить его по-прежнему глобально уникальным.

  2. После создания проекта выберите Добавить Firebase в приложение Android.

    добавить Firebase в приложение Android

  3. На странице приложения Android добавить Firebase выполните следующие действия.

    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 для push-уведомлений

Чтобы отправить push-уведомления в приложение iOS, зарегистрируйте приложение в Apple, а также зарегистрируйтесь для push-уведомлений.

  1. Если вы еще не зарегистрировали приложение, перейдите на портал подготовки iOS в Центре разработчиков Apple. Войдите на портал с помощью идентификатора Apple ID, перейдите к сертификатам, идентификаторам & профилям, а затем выберите идентификаторы. Щелкните +, чтобы зарегистрировать новое приложение.

    страница идентификаторов приложений портала подготовки iOS

  2. На экране Регистрация нового идентификатора выберите идентификаторы приложений переключателя. Затем выберите Продолжить.

    портал подготовки iOS зарегистрируйте новую страницу идентификатора

  3. Обновите следующие три значения для нового приложения, а затем выберите Продолжить:

    • описание. Введите описательное имя приложения.

    • идентификатор пакета: введите идентификатор пакета формы com.organization_identifier.product_name, как упоминалось вруководстве по распространению приложений . На следующем снимке экрана значение mobcat используется в качестве идентификатора организации, а значение PushDemo используется в качестве имени продукта.

      портал подготовки iOS страницы идентификатора приложения

    • push-уведомлений. Проверьте параметр push-уведомлений в разделе Возможности.

      форма для регистрации нового идентификатора приложения

      Это действие создает идентификатор приложения и запросы, которые вы подтверждаете информацию. Выберите продолжить, а затем выберите Зарегистрировать, чтобы подтвердить новый идентификатор приложения.

      Подтверждение нового идентификатора приложения

      После выбора регистрациивы увидите новый идентификатор приложения в виде элемента строки на странице Сертификатов, идентификаторов & профилей.

  4. На странице "Сертификаты идентификаторы" & профили в разделе "Идентификаторы " найдите созданный элемент строки идентификатора приложения. Затем выберите строку, чтобы отобразить экран Изменить конфигурацию идентификатора приложения.

Создание сертификата для центров уведомлений

Сертификат необходим, чтобы центр уведомлений работал с службами push-уведомлений Apple (APNS) и может быть предоставлен одним из двух способов:

  1. создание push-сертификата p12, который можно отправить непосредственно в центр уведомлений (исходного подхода)

  2. создание сертификата p8, который можно использовать для проверки подлинности на основе маркеров (более новый и рекомендуемый подход)

Более новый подход имеет ряд преимуществ, как описано в проверке подлинности на основе токенов (HTTP/2) дляAPNS. Для конкретных сценариев требуется меньше шагов, но также требуется. Однако для обоих подходов были предоставлены шаги, так как они будут работать в целях этого руководства.

ВАРИАНТ 1. Создание push-сертификата p12, который можно отправить непосредственно в Центр уведомлений
  1. На компьютере Mac запустите средство доступа к цепочке ключей. Его можно открыть из папки Служебные программы или папку Other на панели запуска.

  2. Выберите доступ к цепочке ключей, развернитепомощника по сертификату , а затем выберите Запросить сертификат из центра сертификации.

    Использовать доступ к цепочке ключей для запроса нового сертификата

    Заметка

    По умолчанию доступ к цепочке ключей выбирает первый элемент в списке. Это может быть проблема, если вы находитесь в категории сертификатов и Центр сертификации Apple По всему миру разработчиков не является первым элементом в списке. Убедитесь, что у вас есть элемент, отличный от ключа, или Центр сертификации Apple По всему миру разработчиков ключ выбран, прежде чем создавать CSR (запрос на подписи сертификатов).

  3. Выберитеадрес электронной почты пользователя, введите значение общего имени, убедитесь, что сохранено на диске, а затем нажмите кнопку Продолжить. Оставьте адрес электронной почты ЦС пустым, так как он не требуется.

    ожидаемые сведения о сертификате

  4. Введите имя файла запроса подписи сертификата (CSR) в сохранить как, выберите расположение в где, а затем нажмите кнопку Сохранить.

    Выберите имя файла для сертификата

    Это действие сохраняет CSR-файл в выбранном расположении. Расположение по умолчанию — desktop. Помните расположение, выбранное для файла.

  5. Вернитесь на страницу сертификатов, идентификаторов & профилей на портале подготовки iOS, прокрутите страницу вниз до флажка push-уведомлений, а затем выберите Настроить для создания сертификата.

    страница

  6. Появится появится окно TLS/SSL-сертификат ов службы push-уведомлений Apple. Нажмите кнопку создать сертификат в разделе Сертификат TLS/SSL.

    кнопка

    Отображается экран создание нового сертификата.

    Заметка

    В этом руководстве используется сертификат разработки. При регистрации рабочего сертификата используется тот же процесс. Просто убедитесь, что при отправке уведомлений используется тот же тип сертификата.

  7. Выберите Выбрать файл, перейдите к расположению, где вы сохранили CSR-файл, а затем дважды щелкните имя сертификата, чтобы загрузить его. Затем выберите Продолжить.

  8. После создания сертификата на портале нажмите кнопку "Скачать". Сохраните сертификат и запомните расположение, в котором он сохранен.

    страница скачивания сертификата

    Сертификат скачан и сохранен на компьютер в папке загрузки.

    Найти файл сертификата в папке

    Заметка

    По умолчанию скачанный сертификат разработки называется aps_development.cer.

  9. Дважды щелкните скачанный push-сертификат aps_development.cer. Это действие устанавливает новый сертификат в цепочке ключей, как показано на следующем рисунке:

    списке сертификатов доступа к цепочке ключей с новым сертификатом

    Заметка

    Хотя имя в сертификате может отличаться, имя будет префиксировано с помощью push-служб Apple Development iOS и имеет соответствующий идентификатор пакета.

  10. В access Control + Click на новом push-сертификате, созданном в категории сертификатов. Выберите экспорт, назовите файл, выберите формат p12 и нажмите кнопку Сохранить.

    Экспорт сертификата в формате p12

    Вы можете защитить сертификат паролем, но пароль необязателен. Нажмите кнопку ОК, если вы хотите обойти создание пароля. Запишите имя файла и расположение экспортированного сертификата p12. Они используются для включения проверки подлинности с помощью APN.

    Заметка

    Имя и расположение файла p12 могут отличаться от того, что изображено в этом руководстве.

ВАРИАНТ 2. Создание сертификата p8, который можно использовать для проверки подлинности на основе токенов
  1. Запишите следующие сведения:

    • префикс идентификатора приложения ( идентификатор команды)
    • идентификатор пакета
  2. Вернитесь в сертификаты, идентификаторы & профили, щелкните ключи.

    Заметка

    Если у вас уже есть ключ, настроенный для APNS, можно повторно использовать сертификат p8, скачанный сразу после его создания. В этом случае можно игнорировать шаги 3 через 5.

  3. Нажмите кнопку + (или кнопку Создать ключ), чтобы создать новый ключ.

  4. Укажите подходящее значение имени ключа, а затем проверьте параметр службы push-уведомлений Apple (APNS), а затем нажмите кнопку Продолжить, а затем Зарегистрировать на следующем экране.

  5. Щелкните Скачать, а затем переместите файл p8 (префикс с AuthKey_) в безопасный локальный каталог, а затем щелкните Готово.

    Заметка

    Не забудьте сохранить p8-файл в безопасном месте (и сохранить резервную копию). После скачивания ключа его невозможно повторно скачать по мере удаления копии сервера.

  6. На клавишищелкните созданный ключ (или существующий ключ, если вы решили использовать это).

  7. Запишите значение идентификатора ключа .

  8. Откройте сертификат p8 в подходящем приложении, например Visual Studio Code. Запишите значение ключа (между закрытым ключом -----BEGIN----- и -----END PRIVATE KEY-----).

    ЗАКРЫТЫЙ КЛЮЧ -----BEGIN-----
    <key_value>
    -----END PRIVATE KEY-----

    Заметка

    Это значение маркера , которое будет использоваться позже для настройки концентратора уведомлений.

В конце этих действий вам должны быть приведены следующие сведения для последующего использования в Настройка концентратора уведомлений с помощью сведений APNS:

  • идентификатор команды (см. шаг 1)
  • идентификатор пакета (см. шаг 1)
  • идентификатор ключа (см. шаг 7)
  • значение маркера (значение ключа p8, полученное на шаге 8)

Создание профиля подготовки для приложения

  1. Вернитесь кпортала подготовки iOS , выберите сертификаты, идентификаторы & профили, выберите профили в меню слева, а затем выберите , чтобы создать новый профиль. Появится экран Регистрация нового профиля подготовки.

  2. Выберите разработки приложений iOS в разделе "Разработка " в качестве типа профиля подготовки, а затем выберите "Продолжить".

    список профилей подготовки

  3. Затем выберите идентификатор приложения, созданный в раскрывающемся списке идентификатор приложения, и выберите Продолжить.

    Выберите идентификатор приложения

  4. В окне Выбор сертификатов выберите сертификат разработки, используемый для подписывания кода, и выберите Продолжить.

    Заметка

    Этот сертификат не является push-сертификатом, созданным на предыдущем шаге . Это ваш сертификат разработки. Если он не существует, необходимо создать его, так как это предварительный для этого руководства. Сертификаты разработчика можно создавать на портале разработчиков Apple,с помощью Xcode или Visual Studio.

  5. Вернитесь на страницу профилей , идентификаторы & профилей, выберите профили в меню слева, а затем выберите , чтобы создать новый профиль. Появится экран Регистрация нового профиля подготовки.

  6. В окне Выбор сертификатов выберите созданный сертификат разработки. Затем выберите Продолжить.

  7. Затем выберите устройства, которые будут использоваться для тестирования, и выберите Продолжить.

  8. Наконец, выберите имя профиля в имя профиля подготовкии выберите Создать.

    Выбор имени профиля подготовки

  9. При создании нового профиля подготовки выберите Скачать. Помните расположение, в котором он сохранен.

  10. Перейдите к расположению профиля подготовки и дважды щелкните его, чтобы установить его на компьютере разработки.

Создание центра уведомлений

В этом разделе описано, как создать концентратор уведомлений и настроить проверку подлинности с помощью APNS. Вы можете использовать push-сертификат p12 или проверку подлинности на основе маркеров. Если вы хотите использовать созданный центр уведомлений, можно перейти к шагу 5.

  1. Войдите в Azure.

  2. Щелкните Создать ресурс, а затем найдите и выберите Центр уведомлений, а затем щелкните Создать.

  3. Обновите следующие поля, а затем щелкните Создать:

    БАЗОВЫЕ СВЕДЕНИЯ

    подписка : выберите целевую подписку из раскрывающегося списка
    группа ресурсов : создать новую группу ресурсов (или выбрать существующую)

    СВЕДЕНИЯ О ПРОСТРАНСТВЕ ИМЕН

    пространство имен центра уведомлений : Введите глобально уникальное имя для пространства имен центра уведомлений

    Заметка

    Убедитесь, что для этого поля выбран параметр "Создать новую".

    СВЕДЕНИЯ ЦЕНТРА УВЕДОМЛЕНИЙ

    Центр уведомлений : введите имя центра уведомлений
    расположение: Выбрать подходящее расположение из раскрывающегося списка
    ценовая категория : Сохранить параметр бесплатного по умолчанию

    Заметка

    Если вы не достигли максимального количества центров на уровне "Бесплатный".

  4. После подготовки Центра уведомлений перейдите к ресурсу.

  5. Перейдите к новому центру уведомлений .

  6. Выберите политики доступа из списка (в разделе MANAGE).

  7. Запишите значения имени политики вместе со значениями строки подключения .

Настройка центра уведомлений с помощью сведений APNS

В разделеслужб уведомлений выберите Apple выполните соответствующие действия на основе выбранного ранее подхода в разделе Создание сертификата для центров уведомлений.

Заметка

Используйте рабочую для режима приложений только в том случае, если вы хотите отправлять push-уведомления пользователям, которые приобрели приложение из магазина.

ВАРИАНТ 1. Использование push-сертификата P12

  1. Выберитесертификата .

  2. Щелкните значок файла.

  3. Выберите P12-файл, экспортируемый ранее, и выберите Открыть.

  4. При необходимости укажите правильный пароль.

  5. Выберите режим песочницы.

  6. Выберите Сохранить.

ВАРИАНТ 2. Использование проверки подлинности на основе токенов

  1. Выберите токена.

  2. Введите следующие значения, полученные ранее:

    • идентификатор ключа
    • идентификатор пакета
    • идентификатор команды
    • маркера
  3. Выберите песочницу.

  4. Выберите Сохранить.

Настройка концентратора уведомлений с помощью данных FCM

  1. Выберите Google (GCM/FCM) в разделе "Параметры " меню слева.
  2. Введите ключ сервера , который вы указали из консоли Google Firebase.
  3. Выберите Сохранить на панели инструментов.

Создание серверного приложения веб-API ASP.NET Core

В этом разделе описано, как создать ASP.NET веб-API core серверной части для обработки регистрации устройств и отправки уведомлений в мобильное приложение Flutter.

Создание веб-проекта

  1. В Visual Studioвыберите файл>новый.

  2. Выберите .NET Core>App>ASP.NET Core>API>Далее.

  3. В диалоговом окне Настройка нового веб- API ASP.NET Core Web API выберите Target Framework.NET Core 3.1.

  4. Введите PushDemoApi для имени проекта и выберите Создать.

  5. Начните отладку (команда + ВВОД) для тестирования шаблонного приложения.

    Заметка

    Шаблонное приложение настроено для использования 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:
    См. в сводке Essentials в верхней частиобзора .

    NotificationHub:ConnectionString:
    См. defaultFullSharedAccessSignature в политиках доступа

    Заметка

    В рабочих сценариях можно просмотреть такие варианты, как Azure KeyVault для безопасного хранения строки подключения. Для простоты секреты будут добавлены в параметры приложения службы приложений Azure .

Проверка подлинности клиентов с помощью ключа API (необязательно)

Ключи API не так безопасны, как маркеры, но достаточно для целей этого руководства. Ключ API можно легко настроить с помощью ASP.NET ПО промежуточного слоя.

  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, чтобы вызвать метод UseAuthentication и Методы расширения UseAuthorization в IApplicationBuilder приложения. Убедитесь, что эти методы вызываются после 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. Элемент управлениящелкните в проекте 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, а затем добавьте следующую реализацию.

    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. Добавьте еще один пустой класс в папку Models с именем NotificationRequest.cs, а затем добавьте следующую реализацию.

    using System;
    
    namespace PushDemoApi.Models
    {
        public class NotificationRequest
        {
            public string Text { get; set; }
            public string Action { get; set; }
            public string[] Tags { get; set; } = Array.Empty<string>();
            public bool Silent { get; set; }
        }
    }
    
  9. Добавьте еще один пустой класс в папк у моделей с именем NotificationHubOptions.cs, а затем добавьте следующую реализацию.

    using System.ComponentModel.DataAnnotations;
    
    namespace PushDemoApi.Models
    {
        public class NotificationHubOptions
        {
            [Required]
            public string Name { get; set; }
    
            [Required]
            public string ConnectionString { get; set; }
        }
    }
    
  10. Добавьте новую папку в проект PushDemoApi с именем Services.

  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, а затем добавьте следующий код для реализации интерфейса 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 для большинства операторов, но выражение содержит только OR (||) в этом случае. Если в запросе существует более 20 тегов, они должны быть разделены на несколько запросов. Дополнительные сведения см. в документации по выражения маршрутизации и тегов.

  13. В Startup.csобновите метод ConfigureServices, чтобы добавить NotificationHubsService 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. Выберитекласс контроллера веб-API ASP.NET Core, введите 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
    }
    

    Заметка

    Базовый класс контроллера обеспечивает поддержку представлений, но в этом случае это не требуется, поэтому вместо этого можно использовать ControllerBase ControllerBase. Если вы используете Visual Studio 2019, этот шаг можно пропустить.

  5. Если вы решили завершить проверку подлинности клиентов с помощью раздела ключа API, необходимо также украсить notificationsController с помощью атрибута авторизовать .

    [Authorize]
    
  6. Обновите конструктор, чтобы принять зарегистрированный экземпляр INotificationService в качестве аргумента и назначить его элементу чтения.

    readonly INotificationService _notificationService;
    
    public NotificationsController(INotificationService notificationService)
    {
        _notificationService = notificationService;
    }
    
  7. В launchSettings.json (в папке свойств ) измените launchUrl с на api/notifications, чтобы он соответствовал URL-адресу, указанному в атрибуте RegistrationsControllerRoute.

  8. Запустите отладку (команда + ВВОД) для проверки работы приложения с новым NotificationsController и возвращает состояние 401 Unauthorized.

    Заметка

    Visual Studio может не запускать приложение в браузере автоматически. Вы будете использовать Postman для тестирования API с этой точки.

  9. На новой вкладке Postman задайте для запроса GET. Введите приведенный ниже адрес, заменив заполнитель applicationUrl https applicationUrl, найденный вlaunchSettings.jsonсвойств .

    <applicationUrl>/api/notifications
    

    Заметка

    applicationUrl должен быть "https://localhost:5001" для профиля по умолчанию. Если вы используете IIS (по умолчанию в Visual Studio 2019 в Windows), следует использовать applicationUrl, указанные в элементе iisSettings. Если адрес неверный, вы получите ответ 404.

  10. Если вы решили выполнить аутентификацию клиентов с помощью раздела ключа API, обязательно настройте заголовки запроса, чтобы включить значение apikey.

    Ключ Ценность
    apikey <your_api_key>
  11. Нажмите кнопку "Отправить ".

    Заметка

    Вы должны получить состояние 200 OK с некоторыми содержимого JSON.

    Если вы получаете предупреждение проверки 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

Теперь вы создадите приложения API в службе приложений Azure для размещения серверной службы.

  1. Войдите на портал Azure .

  2. Щелкните Создать ресурс, а затем найдите и выберите приложение API, а затем щелкните Создать.

  3. Обновите следующие поля, а затем щелкните Создать.

    имя приложения :
    Введите глобально уникальное имя приложения API

    подписка :
    Выберите тот же целевой подписки, в которую вы создали концентратор уведомлений.

    Группа ресурсов :
    Выберите ту же группу ресурсов, в которую вы создали концентратор уведомлений.

    план или расположение службы приложений :
    Создание плана службы приложений

    Заметка

    Переход от параметра по умолчанию к плану, который включает поддержку SSL-. В противном случае при работе с мобильным приложением необходимо выполнить соответствующие действия, чтобы предотвратить блокировку запросов http.

    Application Insights:
    Сохраните предлагаемый параметр (новый ресурс будет создан с помощью этого имени) или выберите существующий ресурс.

  4. После подготовки приложения API перейдите к ресурсу.

  5. Запишите свойство URL-адреса в сводке Essentials в верхней частиобзора . Этот 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>

    Заметка

    Это те же параметры, которые вы определили ранее в параметрах пользователя. Их можно скопировать. Параметр проверки подлинности :ApiKey требуется только в том случае, если вы решили выполнить проверку подлинности клиентов с помощью раздела ключа API. В рабочих сценариях можно просмотреть такие варианты, как Azure KeyVault. Они добавлены в качестве параметров приложения для простоты в этом случае.

  8. После добавления всех параметров приложения нажмите кнопку Сохранить, а затем продолжить.

Публикация серверной службы

Затем вы развернете приложение в приложении API, чтобы сделать его доступным на всех устройствах.

Заметка

Следующие действия относятся к Visual Studio для Mac. Если вы используете Visual Studio 2019 в Windows, поток публикации будет отличаться. См. статью Публикации в Службе приложений Azure в Windows.

  1. Измените конфигурацию с отладки на выпуск, если это еще не сделано.

  2. Элемент управления + щелкните проект PushDemoApi, а затем выберите Опубликовать в Azure... в меню публикации.

  3. Следуйте потоку проверки подлинности, если появится запрос на это. Используйте учетную запись, используемую в предыдущем создайте приложение API.

  4. Выберите приложение API службы приложений Azure, созданное ранее в списке в качестве целевого объекта публикации, а затем щелкните Опубликовать.

После завершения работы мастера он публикует приложение в 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 мобильное приложение, реализующее push-уведомления кроссплатформенным способом.

Он позволяет зарегистрировать и отменить регистрацию из центра уведомлений с помощью созданной серверной службы.

Оповещение отображается при указании действия и приложение находится на переднем плане. В противном случае уведомления отображаются в центре уведомлений.

Заметка

Обычно вы выполняете действия регистрации (и дерегистрации) во время соответствующего момента жизненного цикла приложения (или как часть первого запуска) без явного регистрации или отмены регистрации входных данных. Однако в этом примере потребуются явные входные данные пользователя, которые позволяют просматривать и тестировать эту функцию более легко.

Создание решения Flutter

  1. Откройте новый экземпляр Visual Studio Code.

  2. Откройте палитру команд (shiftCommandP).

  3. Выберите команду Flutter: Создать проект и нажмите клавишу ВВОД.

  4. Введите push_demo для имени проекта, а затем выберите расположение проекта.

  5. При появлении запроса выберите получить пакеты.

  6. Элемент управления + щелкните в папке kotlin (в разделе приложения>src>main), а затем выберите Показать в finder. Затем переименуйте дочерние папки (в папке kotlin) в com, <your_organization>и pushdemo соответственно.

    Заметка

    При использовании шаблона Visual Studio Code эти папки по умолчанию используются для com, примераproject_name. Если mobcat используется для организации, структура папок должна отображаться примерно следующим образом:

    • kotlin
      • com
        • mobcat
          • pushdemo
  7. Еще в Visual Studio Codeобновите значение applicationId в приложении android>>build.gradle до com.<your_organization>.pushdemo.

    Заметка

    Для заполнителя <your_organization> следует использовать собственное имя организации. Например, использование mobcat, так как организация приведет к имени пакета значению com.mobcat.pushdemo.

  8. Обновите атрибут пакета в файлах AndroidManifest.xml, в разделе srcотладки, srcmainи src профилей соответственно. Убедитесь, что значения соответствуют applicationId, которые вы использовали на предыдущем шаге.

    <manifest
        xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.<your_organization>.pushdemo>">
        ...
    </manifest>
    
  9. Обновите атрибут android:label в файле AndroidManifest.xml в разделе src>main, чтобы PushDemo. Затем добавьте атрибут android:allowBackup непосредственно в android:label, задав его значение false.

    <application
        android:name="io.flutter.app.FlutterApplication"
        android:label="PushDemo"
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher">
        ...
    </application>
    
  10. Откройте файл уровня приложения build.gradle (androidappbuild.gradle), а затем обновите компиляцииSdkVersion (из раздела android) для использования API 29. Затем обновите значения minSdkVersion и targetSdkVersion (из раздела defaultConfig), чтобы 26 и 29 соответственно.

    Заметка

    Для целей этого руководства поддерживаются только те устройства, на которых запущены уровня API 26 и более поздних версий.

  11. Элемент управления + щелкните в папке ios, а затем выберите Открыть в Xcode.

  12. В Xcodeщелкните Runner (xcodeproj вверху, а не папку). Затем выберите целевой объект Runner и перейдите на вкладку "Общие". Выбрав конфигурацию сборки all, обновите идентификатор пакета до .

    Заметка

    Для заполнителя <your_organization> следует использовать собственное имя организации. Например, использование mobcat, так как организация приведет к идентификатору пакета значению com.mobcat.PushDemo.

  13. Щелкните Info.plist затем обновите имя пакета до PushDemo

  14. Закройте Xcode и вернитесь к Visual Studio Code.

  15. Еще в Visual Studio Codeоткройте pubspec.yaml, добавьте http и flutter_secure_storageпакеты Dart в качестве зависимостей. Затем сохраните файл и щелкните получить пакеты при появлении запроса.

    dependencies:
      flutter:
        sdk: flutter
    
      http: ^0.12.1
      flutter_secure_storage: ^3.3.3
    
  16. В терминалеизмените каталог на папку ios (для проекта Flutter). Затем выполните команду pod install, чтобы установить новые модули pod (требуется пакетом flutter_secure_storage).

  17. Элемент управлениящелкните в папке lib, а затем выберите Создать файл в меню, используя main_page.dart в качестве имени файла. Затем добавьте следующий код.

    import 'package:flutter/material.dart';
    
    class MainPage extends StatefulWidget {
      @override
      _MainPageState createState() => _MainPageState();
    }
    
    class _MainPageState extends State<MainPage> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: SafeArea(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[],
            )
          )
        );
      }
    }
    
  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. Элемент управлениящелкните в папке lib, а затем выберите создать папку в меню с помощью моделей в качестве именипапки.

  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 и запросов к серверной службе для выполнения необходимых действий регистрации, отмены регистрации и обновления. Аргумент apiKey требуется только в том случае, если вы решили выполнить проверку подлинности клиентов с помощью раздела ключа API.

  7. Добавьте новый файл в папку lib с именем config.dart со следующей реализацией.

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

    Заметка

    Это используется в качестве простого способа определения секретов приложения. Замените значения заполнителей собственными. При создании серверной службы необходимо заметить их. URL-адрес приложения API должен быть . Элемент apiKey требуется только в том случае, если вы решили выполнить проверку подлинности клиентов с помощью раздела ключа API.

    Не забудьте добавить это в файл Gitignore, чтобы избежать фиксации этих секретов в системе управления версиями.

Реализация кроссплатформенного пользовательского интерфейса

  1. В main_page.dartзамените функцию сборки следующим образом.

    @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 реализуйте обработчики событий для register и Deregister button onPressed events. Вызовите соответствующие методы 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();
    }
    

    Заметка

    Это просто для демонстрации получения и распространения действий push-уведомлений. Как правило, они будут обрабатываться автоматически, например переход к определенному представлению или обновлению некоторых данных, а не отображение оповещения в этом случае.

Настройка собственного проекта Android для push-уведомлений

Добавление JSON-файла служб Google

  1. Элемент управлениящелкните в папке android, а затем выберите Открыть в Android Studio. Затем перейдите в представление Project (если это еще не так).

  2. Найдите скачанный ранее файл google-services.json при настройке проекта PushDemo вконсоли Firebase. Затем перетащите его в корневой каталог модуля (androidandroidприложения).

Настройка параметров и разрешений сборки

  1. Переключите представление проекта на Android.

  2. Откройте AndroidManifest.xml, а затем добавьте разрешения INTERNET и READ_PHONE_STATE после элемента приложения перед закрывающим тегом .

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

Добавление пакетов SDK Firebase

  1. В Android Studioоткройте файл build.gradle build.gradle (скрипты Gradlebuild.grad le (Project: android)). и убедитесь, что в узле buildscript>зависимостей есть класс com.google.gms:google-services.

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

    Заметка

    Убедитесь, что вы ссылаетесь на последнюю версию согласно инструкциям, приведенным в консоли Firebase при создании проекта Android.

  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.

Обработка push-уведомлений для Android

  1. В Android Studioэлемент управлениящелкните на com.your_organizationпапку пакета pushdemo (srcmainkotlin), выберите пакет в меню New. Введите службы в качестве имени, а затем нажмите клавишу Return.

  2. Элемент управлениящелкните в папке служб , выберите Kotlin File/Class в меню "Создать". Введите 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. Это было определено в части приложения Flutter в DeviceInstallationService.dart. В этом случае вызовы выполняются из общего кода к собственному узлу. Обязательно замените <your_organization> собственной организацией, где бы это ни было.

    Этот класс предоставляет уникальный идентификатор (используя Secure.AndroidId) в рамках полезных данных регистрации концентратора уведомлений.

  4. Добавьте еще один Kotlin File/Class в папку служб с именем 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. Это было определено в части приложения Flutter в NotificationRegistrationService.dart. В этом случае вызовы выполняются из собственного узла в общий код. Опять же, необходимо заменить <your_organization> собственной организацией, где бы это ни было.

  5. Добавьте еще один Kotlin File/Class в папку служб с именем 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. Это было определено в части приложения Flutter в NotificationActionService.dart. В этом случае вызовы можно выполнять в обоих направлениях. Обязательно замените <your_organization> собственной организацией, где бы это ни было.

  6. Добавьте новый Kotlin File/Class в 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))
                }
            }
        }
    }
    

    Заметка

    Этот класс отвечает за обработку уведомлений при запуске приложения на переднем плане. При условном вызове триггера triggerAction на NotificationActionService, если действие включается в полезные данные уведомления, полученные в onMessageReceived. Это также вызовет refreshRegistration в NotificationRegistrationService при повторном создании маркера Firebase путем переопределения функции onNewToken.

    Еще раз обратите внимание, чтобы заменить <your_organization> вашей собственной организацией, где бы она ни использовалась.

  7. В AndroidManifest.xml (app>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, чтобы проверить, имеет ли намерение дополнительное значение с именем действия. Условно активируйте это действие или сохраните его для последующего использования, если действие обрабатывается во время запуска приложения.

     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)
    }
    

    Заметка

    Так как launchMode для MainActivity задано значение SingleTop, намерение будет отправлено в существующий экземпляр действия через onNe Функция, а не функция onCreate, поэтому необходимо обрабатывать входящие Намерения как в onCreate, так и в функцияхNewIntent.

  14. Переопределите функцию onCreate, задайте deviceInstallationService на новый экземпляр DeviceInstallationService.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    
        flutterEngine?.let {
            deviceInstallationService = DeviceInstallationService(context, it)
        }
    }
    
  15. Задайте свойства notificationActionService и notificationRegistrationService для PushNotificationFirebaseMessagingServices.

    flutterEngine?.let {
      deviceInstallationService = DeviceInstallationService(context, it)
      PushNotificationsFirebaseMessagingService.notificationActionService = NotificationActionService(it)
      PushNotificationsFirebaseMessagingService.notificationRegistrationService = NotificationRegistrationService(it)
    }
    
  16. В той же функции условно вызовите FirebaseInstanceId.getInstance().instanceId. Реализуйте OnCompleteListener, чтобы задать полученное значение маркера PushNotificationFirebaseMessagingService перед вызовом refreshRegistration.

    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 в конце функции. Используйте true для аргумента launchAction , чтобы указать, что это действие обрабатывается во время запуска приложения.

    processNotificationActions(this.intent, true)
    

Заметка

Необходимо повторно зарегистрировать приложение при каждом запуске и остановить его от сеанса отладки, чтобы продолжить получение push-уведомлений.

Настройка собственного проекта iOS для push-уведомлений

Настройка целевого объекта runner и Info.plist

  1. В Visual Studio Codeэлемент управления щелкните в папке ios, а затем выберите Открыть в Xcode.

  2. В Xcodeщелкните Runner (xcodeproj вверху, а не папку), а затем выберите целевой объект Runner, а затем подписывание возможностей&. Выбрав конфигурацию сборки "Все", выберите учетную запись разработчика для команды . Убедитесь, что флажок "Автоматическое управление подписыванием" установлен, а профиль подписывания и подготовки автоматически выбраны.

    Заметка

    Если вы не видите новое значение профиля подготовки, попробуйте обновить профили для удостоверения подписи, выбрав XcodeНастройкиучетной записи, а затем нажмите кнопку Скачать профили вручную, чтобы скачать профили.

  3. Щелкните + Возможности, а затем найдитеpush-уведомлений . дважды щелкните на push-уведомлений, чтобы добавить эту возможность.

  4. Откройте Info.plist и задайте минимальную версию системы 13.0.

    Заметка

    Для целей этого руководства поддерживаются только те устройства, которые работают iOS 13.0 и более поздних версий.

  5. Откройте Runner.entitlements и убедитесь, что параметр среды APS установлен разработки.

Обработка push-уведомлений для iOS

  1. Элемент управленияЩелкните в папке Runner (в проекте Runner), а затем выберите Создать группу с помощью служб .

  2. Элемент управленияЩелкните в папке служб , а затем выберите Создать файл.... Затем выберите Swift File и нажмите кнопку Далее. Укажите 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. Это было определено в части приложения Flutter в DeviceInstallationService.dart. В этом случае вызовы выполняются из общего кода к собственному узлу. Обязательно замените <your_organization> собственной организацией, где бы это ни было.

    Этот класс предоставляет уникальный идентификатор (используя значение UIDevice.identifierForVendor) в рамках полезных данных регистрации концентратора уведомлений.

  4. Добавьте еще один Swift File в папку служб с именем 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. Это было определено в части приложения Flutter в NotificationRegistrationService.dart. В этом случае вызовы выполняются из собственного узла в общий код. Опять же, необходимо заменить <your_organization> собственной организацией, где бы это ни было.

  5. Добавьте еще один Swift File в папку служб с именем 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. Это было определено в части приложения 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. Затем вызовите refreshRegistration наnotificationRegistrationService .

    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, создайте экземпляр deviceInstallationService, notificationRegistrationServiceи переменные notificationActionService.

    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    
    deviceInstallationService = DeviceInstallationService(withBinaryMessenger: controller.binaryMessenger)
    notificationRegistrationService = NotificationRegistrationService(withBinaryMessenger: controller.binaryMessenger)
    notificationActionService = NotificationActionService(withBinaryMessenger: controller.binaryMessenger)
    
  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, вызовите processNotificationActions в конце функции didFinishLaunchingWithOptions. Передайте полученный объект userInfo и используйте true для аргумента launchAction . Значение true указывает, что действие обрабатывается во время запуска приложения.

    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 на одной или обеих целевых платформах (Android и iOS).

    Заметка

    Если вы тестируете Android убедитесь, что вы не работаете в отладкеили если приложение было развернуто, запустив приложение, принудительно закройте приложение и запустите его снова с средства запуска.

  7. В приложении PushDemo нажмите кнопку Зарегистрировать.

  8. Вернитесь в Postman, закройте окно создания фрагментов кода (если это еще не сделано), а затем нажмите кнопку Отправить.

  9. Убедитесь, что вы получите ответ 200 OK в Postman, а оповещение отображается в приложении с действие ActionA, полученное.

  10. Закройте приложение pushDemo , а затем нажмите кнопку Отправить еще раз в Postman.

  11. Убедитесь, что вы получите ответ 200 OK в Postman еще раз. Убедитесь, что уведомление отображается в области уведомлений для приложения PushDemo с правильным сообщением.

  12. Коснитесь уведомления, чтобы убедиться, что оно открывает приложение и отображает действие ActionA, полученное оповещение.

  13. В Postmanизмените текст предыдущего запроса, чтобы отправить автоматическое уведомление, указывающее action_b вместо action_a для значения действия.

    {
        "action": "action_b",
        "silent": true
    }
    
  14. При открытии приложения нажмите кнопку Отправить в Postman.

  15. Убедитесь, что в Postman появит ся ответ ОК 200 ОК, а не действие ActionA, полученное.

  16. Закройте приложение pushDemo , а затем нажмите кнопку Отправить еще раз в Postman.

  17. Убедитесь, что вы получите ответ 200 ОК в Postman и что автоматическое уведомление не отображается в области уведомлений.

Устранение неполадок

Нет ответа от серверной службы

При локальном тестировании убедитесь, что серверная служба запущена и использует правильный порт.

Если тестирование на приложения API Azure, проверьте, запущена ли служба и была развернута и запущена без ошибок.

Убедитесь, что вы правильно указали базовый адрес в Postman или в конфигурации мобильного приложения при тестировании через клиент. Базовый адрес должен быть https://<api_name>.azurewebsites.net/ или https://localhost:5001/ при локальном тестировании.

Не получая уведомления в Android после запуска или остановки сеанса отладки

Убедитесь, что вы снова зарегистрируетесь после запуска или остановки сеанса отладки. Отладчик приведет к созданию нового маркера Firebase. Также необходимо обновить установку центра уведомлений.

Получение кода состояния 401 из серверной службы

Убедитесь, что вы задаете заголовок запроса apikey , и это значение соответствует тому, который вы настроили для серверной службы.

Если при локальном тестировании эта ошибка возникает, убедитесь, что значение ключа, определенное в конфигурации клиента, соответствует значению Authentication:ApiKey параметров пользователя, используемомуAPI .

Если вы тестируетеприложения API , убедитесь, что значение ключа в файле конфигурации клиента соответствует параметру приложения Authentication:ApiKey, который вы используете в приложении API.

Заметка

Если вы создали или изменили этот параметр после развертывания серверной службы, необходимо перезапустить службу, чтобы она вступила в силу.

Если вы решили не выполнять проверку подлинности клиентов с помощью раздела ключа API, убедитесь, что атрибут авторизации не применяется к классу NotificationsController NotificationsController.

Получение кода состояния 404 из серверной службы

Убедитесь, что конечная точка и метод HTTP-запроса верны. Например, конечные точки должны быть примерно следующими:

  • [PUT]https://<api_name>.azurewebsites.net/api/notifications/installations
  • [DELETE]https://<api_name>.azurewebsites.net/api/notifications/installations/<installation_id>
  • [POST]https://<api_name>.azurewebsites.net/api/notifications/requests

Или при локальном тестировании:

  • [PUT]https://localhost:5001/api/notifications/installations
  • [DELETE]https://localhost:5001/api/notifications/installations/<installation_id>
  • [POST]https://localhost:5001/api/notifications/requests

При указании базового адреса в клиентском приложении убедитесь, что он заканчивается /. Базовый адрес должен быть https://<api_name>.azurewebsites.net/ или https://localhost:5001/ при локальном тестировании.

Не удается зарегистрировать и отображается сообщение об ошибке центра уведомлений

Убедитесь, что тестовое устройство имеет сетевое подключение. Затем определите код состояния ответа Http, задав точку останова, чтобы проверить значение свойства StatusCode в HttpResponse.

Ознакомьтесь с предыдущими предложениями по устранению неполадок, применимыми в зависимости от кода состояния.

Задайте точку останова в строках, возвращающих эти определенные коды состояния для соответствующего API. Затем попробуйте вызвать серверную службу при локальной отладке.

Убедитесь, что серверная служба работает должным образом с помощью Postman с помощью соответствующей полезных данных. Используйте фактические полезные данные, созданные клиентским кодом для платформы.

Просмотрите разделы конфигурации для конкретной платформы, чтобы убедиться, что никаких шагов не было пропущено. Убедитесь, что подходящие значения разрешаются для installation id и token переменных для соответствующей платформы.

Не удается устранить идентификатор сообщения об ошибке устройства

Просмотрите разделы конфигурации для конкретной платформы, чтобы убедиться, что никаких шагов не было пропущено.

Дальнейшие действия

Теперь у вас должно быть базовое приложение Flutter, подключенное к центру уведомлений через серверную службу, и может отправлять и получать уведомления.

Скорее всего, вам потребуется адаптировать пример, используемый в этом руководстве, чтобы соответствовать собственному сценарию. Кроме того, рекомендуется реализовать более надежную обработку ошибок, логику повторных попыток и ведение журнала.

Центра приложений Visual Studio можно быстро включить в мобильные приложения, предоставляющие аналитику и диагностику для устранения неполадок.