appcraft_notifications 0.0.1
appcraft_notifications: ^0.0.1 copied to clipboard
Firebase Cloud Messaging and local notifications behind one API: foreground copies, taps, cold start and the FCM token, with no backend assumptions.
import 'dart:async';
import 'package:appcraft_notifications/appcraft_notifications.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart';
/// The channel of the application. Its id must match
/// `com.google.firebase.messaging.default_notification_channel_id` in the
/// Android manifest.
const AndroidNotificationChannel channel = AndroidNotificationChannel(
'appcraft_notifications_example',
'Example',
description: 'Notifications of the example application',
importance: Importance.max,
);
/// Public key of the Web Push certificate of the Firebase project.
///
/// Required to get a token on the web and ignored on the other platforms. Take
/// it from Project settings → Cloud Messaging → Web configuration.
const String vapidKey = 'YOUR_WEB_PUSH_CERTIFICATE_KEY';
/// Handles a message that arrived while the application is in the background.
///
/// A top-level function annotated with `@pragma('vm:entry-point')`: on Android
/// it runs in a separate isolate raised from scratch, so Firebase has to be
/// initialized here again and nothing of the running application is available.
/// This is the place to store the message; deduplicate by
/// [RemoteMessage.messageId], because the same message reaches the tap handler
/// as well.
///
/// Not called on the web at all — there the background handler is
/// `web/firebase-messaging-sw.js`.
@pragma('vm:entry-point')
Future<void> handleBackgroundMessage(RemoteMessage message) async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
debugPrint('background message ${message.messageId}');
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
FirebaseMessaging.onBackgroundMessage(handleBackgroundMessage);
final AppCraftNotifications notifications =
await AppCraftNotifications.create(
androidChannel: channel,
androidIcon: 'ic_notification',
vapidKey: vapidKey,
debugLogs: true,
);
runApp(ExampleApp(notifications: notifications));
}
/// The example application.
class ExampleApp extends StatelessWidget {
/// Creates the application around a ready [notifications].
const ExampleApp({required this.notifications, super.key});
/// The instance the application owns and passes around itself.
final AppCraftNotifications notifications;
@override
Widget build(BuildContext context) => MaterialApp(
title: 'appcraft_notifications',
theme: ThemeData(colorSchemeSeed: Colors.amber),
home: HomePage(notifications: notifications),
);
}
/// Shows everything the package reports.
class HomePage extends StatefulWidget {
/// Creates the page.
const HomePage({required this.notifications, super.key});
/// The instance of the package.
final AppCraftNotifications notifications;
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final List<String> _log = <String>[];
StreamSubscription<AppCraftNotification>? _messages;
StreamSubscription<AppCraftNotification>? _taps;
StreamSubscription<String>? _tokenChanges;
String? _token;
@override
void initState() {
super.initState();
_messages = widget.notifications.messages.listen(
(AppCraftNotification notification) =>
_add('message: ${notification.title} ${notification.data}'),
);
// Navigation by a payload is the responsibility of the application: the
// package knows nothing about routes.
_taps = widget.notifications.taps.listen(
(AppCraftNotification notification) =>
_add('tap: ${notification.data['route'] ?? notification.data}'),
);
_tokenChanges = widget.notifications.tokenChanges.listen((String token) {
// Register the new token on the server, revoke the previous one only
// after that succeeds, and only then overwrite the stored value.
setState(() => _token = token);
_add('token changed');
});
// The launch notification is taken when the navigator already exists.
final AppCraftNotification? launch = widget.notifications
.takeLaunchNotification();
if (launch != null) {
_add('launched from: ${launch.title} ${launch.data}');
}
}
@override
void dispose() {
_messages?.cancel();
_taps?.cancel();
_tokenChanges?.cancel();
super.dispose();
}
void _add(String line) => setState(() => _log.insert(0, line));
Future<void> _requestPermission() async {
final AuthorizationStatus status = await widget.notifications
.requestPermission();
_add('permission: ${status.name}');
}
Future<void> _getToken() async {
final String? token = await widget.notifications.getToken();
setState(() => _token = token);
_add(token == null ? 'no token yet' : 'token received');
}
Future<void> _show() => widget.notifications.show(
title: 'Local notification',
body: 'Shown by the application itself',
data: const <String, String>{'route': '/orders/1'},
);
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('appcraft_notifications')),
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: <Widget>[
FilledButton(
onPressed: _requestPermission,
child: const Text('Request permission'),
),
FilledButton(
onPressed: _getToken,
child: const Text('Get token'),
),
FilledButton(onPressed: _show, child: const Text('Show local')),
],
),
),
if (_token != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SelectableText(_token!, maxLines: 2),
),
const Divider(),
Expanded(
child: ListView.builder(
itemCount: _log.length,
itemBuilder: (BuildContext context, int index) =>
ListTile(dense: true, title: Text(_log[index])),
),
),
],
),
);
}