From 55e86590abc423e8aa1248b13191a33b7f71fbd2 Mon Sep 17 00:00:00 2001 From: Genert Org Date: Wed, 28 Aug 2019 15:10:07 +0300 Subject: [PATCH 1/4] #47 - initial ios background handling implementation --- .../example/ios/Runner/Info.plist | 5 + .../ios/Classes/FirebaseMessagingPlugin.m | 151 +++++++++++++++++- .../lib/firebase_messaging.dart | 61 ++++++- 3 files changed, 209 insertions(+), 8 deletions(-) diff --git a/packages/firebase_messaging/example/ios/Runner/Info.plist b/packages/firebase_messaging/example/ios/Runner/Info.plist index ededae49efe7..1a05026fce18 100644 --- a/packages/firebase_messaging/example/ios/Runner/Info.plist +++ b/packages/firebase_messaging/example/ios/Runner/Info.plist @@ -24,6 +24,11 @@ 1 LSRequiresIPhoneOS + UIBackgroundModes + + fetch + remote-notification + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m b/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m index 225b86f99599..42b8bcf7721e 100644 --- a/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m +++ b/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m @@ -12,6 +12,11 @@ @interface FLTFirebaseMessagingPlugin () @end #endif +static NSString* backgroundSetupCallback = @"background_setup_callback"; +static NSString* backgroundMessageCallback = @"background_message_callback"; +static FlutterPluginRegistrantCallback registerPlugins = nil; +typedef void (^FetchCompletionHandler)(UIBackgroundFetchResult result); + static FlutterError *getFlutterError(NSError *error) { if (error == nil) return nil; return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %ld", error.code] @@ -21,8 +26,19 @@ @interface FLTFirebaseMessagingPlugin () @implementation FLTFirebaseMessagingPlugin { FlutterMethodChannel *_channel; + FlutterMethodChannel *_backgroundChannel; + NSObject *_registrar; + NSUserDefaults *_userDefaults; NSDictionary *_launchNotification; + NSMutableArray *_eventQueue; BOOL _resumingFromBackground; + FlutterEngine *_headlessRunner; + BOOL initialized; + FetchCompletionHandler fetchCompletionHandler; +} + ++ (void)setPluginRegistrantCallback:(FlutterPluginRegistrantCallback)callback { + registerPlugins = callback; } + (void)registerWithRegistrar:(NSObject *)registrar { @@ -30,7 +46,7 @@ + (void)registerWithRegistrar:(NSObject *)registrar { [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/firebase_messaging" binaryMessenger:[registrar messenger]]; FLTFirebaseMessagingPlugin *instance = - [[FLTFirebaseMessagingPlugin alloc] initWithChannel:channel]; + [[FLTFirebaseMessagingPlugin alloc] initWithChannel:channel registrar:registrar]; [registrar addApplicationDelegate:instance]; [registrar addMethodCallDelegate:instance channel:channel]; @@ -40,7 +56,7 @@ + (void)registerWithRegistrar:(NSObject *)registrar { } } -- (instancetype)initWithChannel:(FlutterMethodChannel *)channel { +- (instancetype)initWithChannel:(FlutterMethodChannel *)channel registrar:(NSObject *)registrar { self = [super init]; if (self) { @@ -52,6 +68,13 @@ - (instancetype)initWithChannel:(FlutterMethodChannel *)channel { NSLog(@"Configured the default Firebase app %@.", [FIRApp defaultApp].name); } [FIRMessaging messaging].delegate = self; + + // Setup background handling + _userDefaults = [NSUserDefaults standardUserDefaults]; + _eventQueue = [[NSMutableArray alloc] init]; + _registrar = registrar; + _headlessRunner = [[FlutterEngine alloc] initWithName:@"firebase_messaging_isolate" project:nil allowHeadlessExecution:YES]; + _backgroundChannel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/firebase_messaging_background" binaryMessenger:[_headlessRunner binaryMessenger]]; } return self; } @@ -75,6 +98,35 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; result(nil); + /* Even when the app is not active the `FirebaseMessagingService` extended by + * `FlutterFirebaseMessagingService` allows incoming FCM messages to be handled. + * + * `FcmDartService#start` and `FcmDartService#initialized` are the two methods used + * to optionally setup handling messages received while the app is not active. + * + * `FcmDartService#start` sets up the plumbing that allows messages received while + * the app is not active to be handled by a background isolate. + * + * `FcmDartService#initialized` is called by the Dart side when the plumbing for + * background message handling is complete. + */ + } else if ([@"FcmDartService#start" isEqualToString:method]) { + + } else if ([@"FcmDartService#initialized" isEqualToString:method]) { + /** + * Acknowledge that background message handling on the Dart side is ready. This is called by the + * Dart side once all background initialization is complete via `FcmDartService#initialized`. + */ + @synchronized(self) { + initialized = YES; + while ([_eventQueue count] > 0) { + NSArray* call = _eventQueue[0]; + [_eventQueue removeObjectAtIndex:0]; + + [self invokeMethod:call[0] callbackHandle:[call[1] longLongValue] arguments:call[2]]; + } + } + result(nil); } else if ([@"configure" isEqualToString:method]) { [FIRMessaging messaging].shouldEstablishDirectChannel = true; [[UIApplication sharedApplication] registerForRemoteNotifications]; @@ -135,6 +187,8 @@ - (void)applicationReceivedRemoteMessage:(FIRMessagingRemoteMessage *)remoteMess #endif - (void)didReceiveRemoteNotification:(NSDictionary *)userInfo { + NSLog(@"didReceiveRemoteNotification"); + if (_resumingFromBackground) { [_channel invokeMethod:@"onResume" arguments:userInfo]; } else { @@ -177,10 +231,23 @@ - (void)applicationDidBecomeActive:(UIApplication *)application { - (bool)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo - fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { - [self didReceiveRemoteNotification:userInfo]; - completionHandler(UIBackgroundFetchResultNoData); - return YES; + fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { + if (application.applicationState == UIApplicationStateBackground){ + //save this handler for later so it can be completed + fetchCompletionHandler = completionHandler; + + [self queueMethodCall:@"onMessageReceived" callbackName:backgroundMessageCallback arguments:userInfo]; + + if (!initialized){ + [self startBackgroundRunner]; + } + + } else { + [self didReceiveRemoteNotification:userInfo]; + completionHandler(UIBackgroundFetchResultNewData); + } + + return YES; } - (void)application:(UIApplication *)application @@ -214,4 +281,76 @@ - (void)messaging:(FIRMessaging *)messaging [_channel invokeMethod:@"onMessage" arguments:remoteMessage.appData]; } +- (void)setupBackgroundHandling:(int64_t)handle { + NSLog(@"Setting up Firebase background handling"); + + [self _saveCallbackHandle:backgroundMessageCallback handle:handle]; + + NSLog(@"Finished background setup"); +} + +- (void) startBackgroundRunner { + NSLog(@"Starting background runner"); + + int64_t handle = [self getCallbackHandle:backgroundMessageCallback]; + + FlutterCallbackInformation *info = [FlutterCallbackCache lookupCallbackInformation:handle]; + NSAssert(info != nil, @"failed to find callback"); + NSString *entrypoint = info.callbackName; + NSString *uri = info.callbackLibraryPath; + + [_headlessRunner runWithEntrypoint:entrypoint libraryURI:uri]; + [_registrar addMethodCallDelegate:self channel:_backgroundChannel]; + + // Once our headless runner has been started, we need to register the application's plugins + // with the runner in order for them to work on the background isolate. `registerPlugins` is + // a callback set from AppDelegate.m in the main application. This callback should register + // all relevant plugins (excluding those which require UI). + + NSAssert(registerPlugins != nil, @"failed to set registerPlugins"); + registerPlugins(_headlessRunner); +} + +- (int64_t)getCallbackHandle:(NSString *) key { + NSLog(@"Getting callback handle for key %@", key); + id handle = [_userDefaults objectForKey:key]; + if (handle == nil) { + return 0; + } + return [handle longLongValue]; +} + +- (void)_saveCallbackHandle:(NSString *)key handle:(int64_t)handle { + NSLog(@"Saving callback handle for key %@", key); + + [_userDefaults setObject:[NSNumber numberWithLongLong:handle] forKey:key]; +} + +- (void) queueMethodCall:(NSString *) method callbackName:(NSString*)callback arguments:(NSDictionary*)arguments { + NSLog(@"Queuing method call: %@", method); + int64_t handle = [self getCallbackHandle:callback]; + + @synchronized(self) { + if (initialized) { + [self invokeMethod:method callbackHandle:handle arguments:arguments]; + } else { + NSArray *call = @[method, @(handle), arguments]; + [_eventQueue addObject:call]; + } + } +} + +- (void) invokeMethod:(NSString *) method callbackHandle:(long)handle arguments:(NSDictionary*)arguments { + NSLog(@"Invoking method: %@", method); + NSArray* args = @[@(handle), arguments]; + + [_backgroundChannel invokeMethod:method arguments:args result:^(id _Nullable result) { + NSLog(@"%@ method completed", method); + if (self->fetchCompletionHandler!=nil) { + self->fetchCompletionHandler(UIBackgroundFetchResultNewData); + self->fetchCompletionHandler = nil; + } + }]; +} + @end diff --git a/packages/firebase_messaging/lib/firebase_messaging.dart b/packages/firebase_messaging/lib/firebase_messaging.dart index f40420c6dbe5..81d2a29da7f5 100644 --- a/packages/firebase_messaging/lib/firebase_messaging.dart +++ b/packages/firebase_messaging/lib/firebase_messaging.dart @@ -3,10 +3,13 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; +import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:meta/meta.dart'; import 'package:platform/platform.dart'; +import 'package:flutter/widgets.dart'; typedef Future MessageHandler(Map message); @@ -26,10 +29,49 @@ class FirebaseMessaging { const MethodChannel('plugins.flutter.io/firebase_messaging'), const LocalPlatform()); + /// Setup method channel to handle Firebase Cloud Messages received while + /// the Flutter app is not active. The handle for this method is generated + /// and passed to the Android side so that the background isolate knows where + /// to send background messages for processing. + /// + /// Your app should never call this method directly, this is only for use + /// by the firebase_messaging plugin to setup background message handling. + @visibleForTesting + static void fcmSetupBackgroundChannel( + {MethodChannel backgroundChannel = const MethodChannel( + 'plugins.flutter.io/firebase_messaging_background')}) async { + // Setup Flutter state needed for MethodChannels. + WidgetsFlutterBinding.ensureInitialized(); + + // This is where the magic happens and we handle background events from the + // native portion of the plugin. + backgroundChannel.setMethodCallHandler((MethodCall call) async { + if (call.method == 'handleBackgroundMessage') { + final CallbackHandle handle = + CallbackHandle.fromRawHandle(call.arguments['handle']); + final Function handlerFunction = + PluginUtilities.getCallbackFromHandle(handle); + try { + await handlerFunction( + Map.from(call.arguments['message'])); + } catch (e) { + print('Unable to handle incoming background message.'); + print(e); + } + return Future.value(); + } + }); + + // Once we've finished initializing, let the native portion of the plugin + // know that it can start scheduling handling messages. + backgroundChannel.invokeMethod('FcmDartService#initialized'); + } + final MethodChannel _channel; final Platform _platform; MessageHandler _onMessage; + MessageHandler _onBackgroundMessage; MessageHandler _onLaunch; MessageHandler _onResume; @@ -47,7 +89,7 @@ class FirebaseMessaging { } final StreamController _iosSettingsStreamController = - StreamController.broadcast(); + StreamController.broadcast(); /// Stream that fires when the user changes their notification settings. /// @@ -59,6 +101,7 @@ class FirebaseMessaging { /// Sets up [MessageHandler] for incoming messages. void configure({ MessageHandler onMessage, + MessageHandler onBackgroundMessage, MessageHandler onLaunch, MessageHandler onResume, }) { @@ -67,10 +110,24 @@ class FirebaseMessaging { _onResume = onResume; _channel.setMethodCallHandler(_handleMethod); _channel.invokeMethod('configure'); + if (onBackgroundMessage != null) { + _onBackgroundMessage = onBackgroundMessage; + final CallbackHandle backgroundSetupHandle = + PluginUtilities.getCallbackHandle(fcmSetupBackgroundChannel); + final CallbackHandle backgroundMessageHandle = + PluginUtilities.getCallbackHandle(_onBackgroundMessage); + _channel.invokeMethod( + 'FcmDartService#start', + { + 'setupHandle': backgroundSetupHandle.toRawHandle(), + 'backgroundHandle': backgroundMessageHandle.toRawHandle() + }, + ); + } } final StreamController _tokenStreamController = - StreamController.broadcast(); + StreamController.broadcast(); /// Fires when a new FCM token is generated. Stream get onTokenRefresh { From d534d904e16a8475012a17c5193283dcfe215fab Mon Sep 17 00:00:00 2001 From: Genert Org Date: Wed, 28 Aug 2019 16:20:48 +0300 Subject: [PATCH 2/4] #47 - readme update --- packages/firebase_messaging/README.md | 84 +++++++++++++++++++ .../example/ios/Runner/AppDelegate.m | 10 ++- .../ios/Classes/FirebaseMessagingPlugin.m | 2 +- 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/packages/firebase_messaging/README.md b/packages/firebase_messaging/README.md index 1d71ab81d848..f5986409fc59 100644 --- a/packages/firebase_messaging/README.md +++ b/packages/firebase_messaging/README.md @@ -55,6 +55,90 @@ Note: When you are debugging on Android, use a device or AVD with Google Play se ``` +#### Optionally handle background messages + + >Background message handling is intended to be performed quickly. Do not perform +long running tasks as they may not be allowed to finish by the Android system. +See [Background Execution Limits](https://developer.android.com/about/versions/oreo/background) +for more. + By default background messaging is not enabled. To handle messages in the background: + + 1. For Android add an Application.java class to your app + + ``` + package io.flutter.plugins.firebasemessagingexample; + + import io.flutter.app.FlutterApplication; + import io.flutter.plugin.common.PluginRegistry; + import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback; + import io.flutter.plugins.GeneratedPluginRegistrant; + import io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService; + + public class Application extends FlutterApplication implements PluginRegistrantCallback { + @Override + public void onCreate() { + super.onCreate(); + FlutterFirebaseMessagingService.setPluginRegistrant(this); + } + + @Override + public void registerWith(PluginRegistry registry) { + GeneratedPluginRegistrant.registerWith(registry); + } + } + ``` +1. Set name property of application in `AndroidManifest.xml` + ``` + + ``` +1. For iOS (Swift) + ``` + func callback(registry: FlutterPluginRegistry) { + GeneratedPluginRegistrant.register(with: registry) + } + + FLTFirebaseMessagingPlugin.setPluginRegistrantCallback(callback) + ``` + +1. Define a top level Dart method to handle background messages + ``` + Future myBackgroundMessageHandler(Map message) { + if (message.containsKey('data')) { + // Handle data message + dynamic data = message['data']; + } + + if (message.containsKey('notification')) { + // Handle notification message + dynamic notification = message['notification']; + } + + // Or do other work. + } + ``` + Note: the protocol of `data` and `notification` are in line with the + fields defined by a [RemoteMessage](https://firebase.google.com/docs/reference/android/com/google/firebase/messaging/RemoteMessage). +1. Set `onBackgroundMessage` handler when calling `configure` + ``` + _firebaseMessaging.configure( + onMessage: (Map message) async { + print("onMessage: $message"); + _showItemDialog(message); + }, + onBackgroundMessage: myBackgroundMessageHandler, + onLaunch: (Map message) async { + print("onLaunch: $message"); + _navigateToItemDetail(message); + }, + onResume: (Map message) async { + print("onResume: $message"); + _navigateToItemDetail(message); + }, + ); + ``` + Note: `configure` should be called early in the lifecycle of your application + so that it can be ready to receive messages as early as possible. See the + example app for a demonstration. ### iOS Integration diff --git a/packages/firebase_messaging/example/ios/Runner/AppDelegate.m b/packages/firebase_messaging/example/ios/Runner/AppDelegate.m index a4b51c88eb60..eec1108fdfe6 100644 --- a/packages/firebase_messaging/example/ios/Runner/AppDelegate.m +++ b/packages/firebase_messaging/example/ios/Runner/AppDelegate.m @@ -4,13 +4,19 @@ #include "AppDelegate.h" #include "GeneratedPluginRegistrant.h" +#import @implementation AppDelegate + +void callback(NSObject* registry) { + [GeneratedPluginRegistrant registerWithRegistry:registry]; +} - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - return [super application:application didFinishLaunchingWithOptions:launchOptions]; + [GeneratedPluginRegistrant registerWithRegistry:self]; + [FLTFirebaseMessagingPlugin setPluginRegistrantCallback:callback]; + return [super application:application didFinishLaunchingWithOptions:launchOptions]; } @end diff --git a/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m b/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m index 42b8bcf7721e..fd4692797174 100644 --- a/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m +++ b/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m @@ -289,7 +289,7 @@ - (void)setupBackgroundHandling:(int64_t)handle { NSLog(@"Finished background setup"); } -- (void) startBackgroundRunner { +- (void)startBackgroundRunner { NSLog(@"Starting background runner"); int64_t handle = [self getCallbackHandle:backgroundMessageCallback]; From dc7819167bc4c0c38892fd05c66fe26b8a4155f1 Mon Sep 17 00:00:00 2001 From: Genert Org Date: Wed, 28 Aug 2019 16:39:16 +0300 Subject: [PATCH 3/4] #47 - cleanup --- packages/firebase_messaging/CHANGELOG.md | 4 ++++ .../ios/Classes/FirebaseMessagingPlugin.m | 22 +++++-------------- packages/firebase_messaging/pubspec.yaml | 2 +- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/packages/firebase_messaging/CHANGELOG.md b/packages/firebase_messaging/CHANGELOG.md index e8448c875253..73f80180fe8b 100644 --- a/packages/firebase_messaging/CHANGELOG.md +++ b/packages/firebase_messaging/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.1.5 + + * Enable background message handling on iOS. + ## 5.1.4 * Update documentation to reflect new repository location. diff --git a/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m b/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m index fd4692797174..89f34b8c3805 100644 --- a/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m +++ b/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m @@ -73,7 +73,7 @@ - (instancetype)initWithChannel:(FlutterMethodChannel *)channel registrar:(NSObj _userDefaults = [NSUserDefaults standardUserDefaults]; _eventQueue = [[NSMutableArray alloc] init]; _registrar = registrar; - _headlessRunner = [[FlutterEngine alloc] initWithName:@"firebase_messaging_isolate" project:nil allowHeadlessExecution:YES]; + _headlessRunner = [[FlutterEngine alloc] initWithName:@"firebase_messaging_background" project:nil allowHeadlessExecution:YES]; _backgroundChannel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/firebase_messaging_background" binaryMessenger:[_headlessRunner binaryMessenger]]; } return self; @@ -98,20 +98,10 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; result(nil); - /* Even when the app is not active the `FirebaseMessagingService` extended by - * `FlutterFirebaseMessagingService` allows incoming FCM messages to be handled. - * - * `FcmDartService#start` and `FcmDartService#initialized` are the two methods used - * to optionally setup handling messages received while the app is not active. - * - * `FcmDartService#start` sets up the plumbing that allows messages received while - * the app is not active to be handled by a background isolate. - * - * `FcmDartService#initialized` is called by the Dart side when the plumbing for - * background message handling is complete. - */ } else if ([@"FcmDartService#start" isEqualToString:method]) { - + long handle = [call.arguments[0] longValue]; + [self saveCallbackHandle:backgroundMessageCallback handle:handle]; + result(nil); } else if ([@"FcmDartService#initialized" isEqualToString:method]) { /** * Acknowledge that background message handling on the Dart side is ready. This is called by the @@ -284,7 +274,7 @@ - (void)messaging:(FIRMessaging *)messaging - (void)setupBackgroundHandling:(int64_t)handle { NSLog(@"Setting up Firebase background handling"); - [self _saveCallbackHandle:backgroundMessageCallback handle:handle]; + [self saveCallbackHandle:backgroundMessageCallback handle:handle]; NSLog(@"Finished background setup"); } @@ -320,7 +310,7 @@ - (int64_t)getCallbackHandle:(NSString *) key { return [handle longLongValue]; } -- (void)_saveCallbackHandle:(NSString *)key handle:(int64_t)handle { +- (void) saveCallbackHandle:(NSString *)key handle:(int64_t)handle { NSLog(@"Saving callback handle for key %@", key); [_userDefaults setObject:[NSNumber numberWithLongLong:handle] forKey:key]; diff --git a/packages/firebase_messaging/pubspec.yaml b/packages/firebase_messaging/pubspec.yaml index aaf0a873c4a5..df38e5dbfa5b 100644 --- a/packages/firebase_messaging/pubspec.yaml +++ b/packages/firebase_messaging/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Firebase Cloud Messaging, a cross-platform messaging solution that lets you reliably deliver messages on Android and iOS. author: Flutter Team homepage: https://github.com/FirebaseExtended/flutterfire/tree/master/packages/firebase_messaging -version: 5.1.4 +version: 5.1.5 flutter: plugin: From 5ea6a9aeb718e25b602ad45796068206dd986d6e Mon Sep 17 00:00:00 2001 From: Genert Org Date: Fri, 13 Sep 2019 14:08:23 +0300 Subject: [PATCH 4/4] update firebase messaging example app for background handler --- .../firebase_messaging/example/lib/main.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/firebase_messaging/example/lib/main.dart b/packages/firebase_messaging/example/lib/main.dart index 685a3f86f2fa..ae30e141b546 100644 --- a/packages/firebase_messaging/example/lib/main.dart +++ b/packages/firebase_messaging/example/lib/main.dart @@ -7,7 +7,15 @@ import 'dart:async'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; +// myBackgroundMessageHandler must be either global or static otherwise you will encounter a runtime exception. +Future myBackgroundMessageHandler(Map message) async { + print("run background message handler - $message"); + + return Future.value(); +} + final Map _items = {}; + Item _itemForMessage(Map message) { final dynamic data = message['data'] ?? message; final String itemId = data['id']; @@ -18,19 +26,24 @@ Item _itemForMessage(Map message) { class Item { Item({this.itemId}); + final String itemId; StreamController _controller = StreamController.broadcast(); + Stream get onChanged => _controller.stream; String _status; + String get status => _status; + set status(String value) { _status = value; _controller.add(this); } static final Map> routes = >{}; + Route get route { final String routeName = '/detail/$itemId'; return routes.putIfAbsent( @@ -45,7 +58,9 @@ class Item { class DetailPage extends StatefulWidget { DetailPage(this.itemId); + final String itemId; + @override _DetailPageState createState() => _DetailPageState(); } @@ -139,6 +154,7 @@ class _PushMessagingExampleState extends State { void initState() { super.initState(); _firebaseMessaging.configure( + onBackgroundMessage: myBackgroundMessageHandler, onMessage: (Map message) async { print("onMessage: $message"); _showItemDialog(message);