Flutter — Estratégia de navegação através de push notifications

Muitas vezes na implementação de push notification queremos que o usuário seja direcionado para uma tela ou ação específica quando pressionar sobre a notificação.

Antônio Alexandre
Flutter Brasil
6 min readApr 27, 2024

--

Neste artigo, mergulharemos fundo na implementação de uma estratégia robusta de navegação através de push notifications no Flutter.

Quando um usuário recebe uma notificação em seu aplicativo flutter, espera-se que a ação de tocar na notificação o leve diretamente para o conteúdo relevante. Imagine receber uma mensagem informando sobre uma promoção em um aplicativo de compras, mas ao clicar na notificação, você é levado para a tela inicial do aplicativo, sem nenhuma indicação da oferta mencionada. Isso pode ser frustrante e resultar em uma experiência desagradável para o usuário. Portanto, é crucial que as notificações sejam vinculadas as telas desejadas de navegação.

O que veremos neste artigo:

Implementação da Estratégia de Navegação:

  • Explicação da configuração inicial de push notifications com o firebase cloud messaging.

Estratégias de Navegação:

  • Implementação das diferentes estratégias de navegação, como OpenOrderStrategy e OpenLinkStrategy, usando o padrão de design Strategy.
  • Descrição do propósito de cada estratégia e como ela é acionada.

Configuração Inicial:

  • Você deve fazer uma configuração do firebase cloud messaging no flutter de acordo com a documentação.

Dependências utilizadas:

Para a sequência dos próximos passos e concretização da ação de navegação ao tocar na notificação é necessário que você tenha o ambiente de push notification configurado nas plataformas desejadas, para isso a Google tem uma documentação da configuração necessária da plataforma.

…Então vamos lá!

Primeiro precisamos criar uma classe chamada PushNotifications que tem a responsabilidade de inicializar as configurações de notificação em background por exemplo.

class PushNotifications {
Future<void> configure() async {
FirebaseMessaging.instance.requestPermission();
FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
alert: true,
badge: true,
sound: true,
);

// O método a seguir irá obter o token responsável por identificar
// unicamente o dispositivo a qual irá receber a notificação.
// é importante que isto já tenha sido configurado de acordo com a doc.
//_registerFcmToken();

FirebaseMessaging.onBackgroundMessage(_backgroundMessageHandler);

FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
final notificationData = PushNotificationDataModel.fromJson(message.data);
PushNotificationFactory.create(notificationData).execute();
});
}
}

Future<void> _backgroundMessageHandler(RemoteMessage message) async {
await Firebase.initializeApp();
}

E você deve chamar na main.dart este método responsável por inicializar e configurar as notificações de push notification:

Future<void> main(_) async {
await Firebase.initializeApp();
PushNotifications().configure();

runApp(
ModularApp(
module: AppModule(),
child: const AppWidget(),
),
);
}

Como podem perceber, o ponto chave da classe PushNotifications é dado no momento em que a notificação é disparada via onMessageOpenedApp, isso significa que neste exemplo iremos apenas fazer a navegação para as telas desejadas quando a notificação vir em background, mas o app não estiver encerrado, para que seja possível navegar através da notificação com o aplicativo encerrado veremos mais na frente como é implementado.

Esta classe acima tem as seguintes dependências: PushNotificationDataModel e PushNotificationFactory, vamos criar-las agora:

import 'package:equatable/equatable.dart';

import '../enums/notification_action_type_enum.dart';

// Você pode implementar o equality manualmente, neste caso estou
// utilizando o package Equatable para diminuir o boilerplate
class PushNotificationDataEntity extends Equatable {
final NotificationActionTypeEnum action;
final String identifier;

const PushNotificationDataEntity({
required this.action,
required this.identifier,
});

@override
List<Object?> get props => [
action,
identifier,
];
}
import '../../domain/entities/push_notification_data_entity.dart';
import '../../domain/enums/notification_action_type_enum.dart';

class PushNotificationDataModel extends PushNotificationDataEntity {
const PushNotificationDataModel({
required super.action,
required super.identifier,
});

factory PushNotificationDataModel.fromJson(Map<String, dynamic> json) =>
PushNotificationDataModel(
action: NotificationActionTypeEnum.fromString(json['action'] ?? ''),
identifier: json['identifier'] ?? '',
);

Map<String, dynamic> toJson() => {
'action': action.value,
'identifier': identifier,
};
}
enum NotificationActionTypeEnum {
openPage('open_page'), // Abrir uma página
openOrder('open_order'), // Abrir um pedido
openLink('open_link'); // Abrir um link

static NotificationActionTypeEnum fromString(String value) => switch (value) {
'open_page' => NotificationActionTypeEnum.openPage,
'open_order' => NotificationActionTypeEnum.openOrder,
'open_link' => NotificationActionTypeEnum.openLink,
_ => throw Exception('Value not mapped')
};

const NotificationActionTypeEnum(this.value);
final String value;
}

Com estas classes já podemos seguir para o ponto principal da navegação onde iremos criar as estratégias responsáveis para cada tipo de ação, citarei alguns exemplos da requisição POST de teste via serviço do firebase cloud messaging: https://fcm.googleapis.com/fcm/send:

{
"to": "{{token do dispositivo gerado via _registerFcmToken }}",
"notification": {
"title": "Novo status do seu pedido",
"body": "Seu pedido foi entregue"
},
"data": {
"action": "open_order",
"identifier": "e0c4bcee-f032-4d03-a341-9d8c890cf766"
}
}

Este campo "action" poderia vir com o valor um open_page e o identifier com o mesmo mapeamento URL das rotas nomeadas (você pode nomear via Modular/GoRouter/AutoRoute, etc) ou também com o valor open_link induzindo ao usuário a abrir um link específico ao tocar na notificação.

Agora vamos criar as nossas estratégias de acordo com essa action recomendo a leitura do design pattern comportamental strategy para um conhecimento mais aprofundado, mas resumindo: através de um contexto (push notification), cria-se classes de estratégias para cada caminho desta action. Os principais ganhos que temos com esse design pattern são:

  • Distinção de qual caminho de fato executará;
  • Evitar métodos longos com switch case / ifs + Navigator.pushNamed…
  • Se um desenvolvedor do seu time está alterando ou implementando um novo trecho de uma estratégia especifica ele evita conflitos do git deste mapeamento.

Vamos ao código:

Criamos uma interface com um método execute() para que as outras estratégias implementem a execução:

import '../domain/entities/push_notification_data_entity.dart';

abstract interface class NotificationStrategy {
void execute(PushNotificationDataEntity data);
}

E agora criamos as nossas estratégias:

import 'notification_navigation_strategy.dart';

class OpenOrderStrategy implements NotificationStrategy {
@override
void execute(data) {
Modular.to.pushNamed(
'${AppRoutes.initial}'
'${AppRoutes.account}'
'${AccountRoutes.myOrders}'
'${OrdersRoutes.orderDetail}'
'/${data.identifier}',
);
}
}
import '../../../shared/utils/launcher_utils.dart';
import 'notification_navigation_strategy.dart';

class OpenLinkStrategy implements NotificationStrategy {
@override
void execute(data) {
final link = data.identifier;
LauncherUtils.openLink(link); // utilidade do package launcher
}
}
import 'notification_navigation_strategy.dart';
import 'package:flutter_modular/flutter_modular.dart';

class OpenOrderStrategy implements NotificationStrategy {
@override
void execute(data) {
// supondo que você tem uma rota nomeada "/orders/:id"
Modular.to.pushNamed('orders/${data.identifier}');
}
}
import 'notification_navigation_strategy.dart';

class OpenPageStrategy implements NotificationStrategy {
@override
void execute(data) {
Modular.to.pushNamed(data.identifier);
}
}

Agora que temos as nossas estratégias, devemos criar uma classe para executar-las, essa classe será uma Factory .

import '../domain/entities/push_notification_data_entity.dart';
import '../domain/enums/notification_action_type_enum.dart';
import 'notification_navigation_strategy.dart';
import 'open_link_strategy.dart';
import 'open_order_strategy.dart';
import 'open_page_strategy.dart';

class PushNotificationFactory {
PushNotificationDataEntity pushNotificationEntity;
NotificationStrategy? strategy;

PushNotificationFactory.create(this.pushNotificationEntity) {
switch (pushNotificationEntity.action) {
case NotificationActionTypeEnum.openPage:
strategy = OpenPageStrategy();
case NotificationActionTypeEnum.openOrder:
strategy = OpenOrderStrategy();
case NotificationActionTypeEnum.openLink:
strategy = OpenLinkStrategy();
default:
throw Exception('Strategy not mapped');
}
}

void execute() {
strategy?.execute(pushNotificationEntity);
}
}

Então, quando criarmos a classe através do construtor nomeado .create, iremos atribuir a estratégia utilizada por essa classe e posteriormente chamando o método .execute onde acontecerá de fato a ação.

Agora podemos voltar na classe PushNotifications e importar essa factory e suas dependências de model, entity e enum.

A estrutura de diretórios ficará da seguinte forma:

Se você configurou corretamente, neste ponto do artigo você já pode fazer um teste de requisição para acionar a notificação e observar as estratégias sendo executadas de acordo com a "action" do payload.

Agora vamos fazer com que seja feita esta mesma abordagem quando o aplicativo estiver encerrado, isto é, quando o usuário com o aplicativo fechado e recebeu uma notificação e abriu o aplicativo em dado momento.

Neste caso vou reaproveitar uma tela de splash (onde acontece algumas chamadas assíncronas requeridas para a utilização do aplicativo)

Criaremos uma Service e uma Controller.

import 'package:firebase_messaging/firebase_messaging.dart';

class PushNotificationService {
/// Este método irá verificar qual é a notificação que ele tocou.
Future<RemoteMessage?> getInitialMessage() async {
final message = await FirebaseMessaging.instance.getInitialMessage();
return message;
}
}
import 'package:firebase_messaging/firebase_messaging.dart';

import 'core/push_notification/service/push_notification_service.dart';

class SplashController {
PushNotificationService pushNotificationService;
// Recebemos o nosso serviço via injeção de dependência
SplashController(this.pushNotificationService);

Future<RemoteMessage?>? getInitialPushMessage() async {
return await pushNotificationService.getInitialMessage();
}
}

Não esqueça de registrar estes serviços no seu módulo (neste exemplo estou utilizando o modular, se estiver utilizando o provider ou get_it, mantenha a lógica do registro destas classes):

import 'package:flutter_modular/flutter_modular.dart';

class LoginModule extends Module {

@override
void binds(i) {
// Services
i.add<PushNotificationService>(PushNotificationService.new);
// Controllers
i.add<SplashController>(SplashController.new);
}

@override
void routes(r) {
r.child(
'/splash',
child: (_) => SplashPage(
controller: Modular.get<SplashController>(),
),
);
}
}

Em seguida, na SplashPage recebemos o SplashController que contém o serviço de obter a notificação:

import 'package:flutter/material.dart';
import '../../../../../../core/push_notification/data/model/push_notification_data_model.dart';
import '../../../../../../core/push_notification/strategies/push_notification_factory.dart';

class SplashPage extends StatefulWidget {
const SplashPage({required this.controller, super.key});

final SplashController controller;
@override
State<SplashPage> createState() => _SplashPageState();
}

class _SplashPageState extends State<SplashPage> {
SplashController get splashController => widget.controller;

@override
void initState() {
super.initState();
_init();
}

Future<void> _init() async {
final message = await splashController.getInitialPushMessage();
if (message != null) {
return WidgetsBinding.instance.addPostFrameCallback(
(_) => PushNotificationFactory.create(
PushNotificationDataModel.fromJson(message.data),
).execute(),
);
}
}
@override
Widget build(BuildContext context) {
return CircularProgressIndicator();
}

Pronto! Dessa forma você deve ser direcionado para a tela desejada, ou melhor, para as estratégias desejadas de acordo com a ação da notificação tanto quanto o aplicativo minimizado quanto o aplicativo encerrado.

Portanto, que este artigo sirva como uma orientação para desenvolvedores em busca de uma implementação eficaz de navegação por push notifications. Que cada toque em uma notificação seja uma porta de entrada para uma experiência significativa, guiando os usuários para onde eles desejam estar.

--

--