Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

[firebase_messaging] Add support for handling messages in background #1900

Closed
wants to merge 16 commits into from
Closed
67 changes: 67 additions & 0 deletions packages/firebase_messaging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,74 @@ Note: When you are debugging on Android, use a device or AVD with Google Play se
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
```
#### Optionally handle background messages

By default background messaging is not enabled. To handle messages in the background:

1. Add an Application.java class to your app

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where in the App we should put this Java class, with the MainActivity or inside io.flutter.plugins?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JillyTaboga You should add it alongside your MainActivity.java file.


```
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`
collinjackson marked this conversation as resolved.
Show resolved Hide resolved
```
<application android:name=".Application" ...>
```
1. Define a top level method to handle background messages
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explicitly mention that this is a Dart method so that readers do not get confused?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

```
Future<dynamic> myBackgroundMessageHandler(Map<String, dynamic> message) {
if (message.containsKey('data')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you mention somewhere where a developer can find the protocol that is being used here? The reference to 'data' and 'notification' looks magical.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, it is in line with RemoteMessage fields.

// Handle data message
dynamic data = message['data'];
}

if (message.containsKey('notification')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can a message be both a "data" message and a "notification" message? If not, please consider using an if-else-if statement to make that explicit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RemoteMessages can contain both a data and notification part.

How the Android SDK passes the message over to the developer (and Plugin) depends on the state of the app.

// Handle notification message
dynamic notification = message['notification'];
}

// Or do work with other plugins, eg: write to RTDB.
FirebaseDatabase.instance.reference().child('foo').set('bar');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reference to another plugin might be confusing for the reader - it seems to come out of nowhere. Also, are you sure this works as expected? You're executing in a background isolate, which will have a different version of the FirebaseDatabase plugin than the main isolate. This line might suggest to readers that there is only 1 FirebaseDatabase plugin and that they're controlling it from this background isolate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is to suggest that the developer is able to perform work in the background. In this case writing some data to RTDB. I assume the dev could use the shared preferences plugin to store some data locally.

I have tried this and it does work as expected from my tests, is there some reason why getting an instance of RTDB (which is a singleton) in a different isolate would be an issue?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the implementation of a plugin ensures that it shares resources across all plugin instances then things are fine. My concern here is that we're suggesting that you can interact with any plugin from here, but any plugin that does not setup its own singleton behavior would not work as expected...

return Future<void>.value();
}
```
1. Set `onBackgroundMessage` handler when calling `configure`
kroikie marked this conversation as resolved.
Show resolved Hide resolved
```
_firebaseMessaging.configure(
onMessage: (Map<String, dynamic> message) async {
print("onMessage: $message");
_showItemDialog(message);
},
onBackgroundMessage: myBackgroundMessageHandler,
onLaunch: (Map<String, dynamic> message) async {
print("onLaunch: $message");
_navigateToItemDetail(message);
},
onResume: (Map<String, dynamic> message) async {
print("onResume: $message");
_navigateToItemDetail(message);
},
);
```

### iOS Integration

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@
import com.google.firebase.iid.InstanceIdResult;
import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.RemoteMessage;
import io.flutter.plugin.common.JSONMethodCodec;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.NewIntentListener;
import io.flutter.plugin.common.PluginRegistry.Registrar;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/** FirebaseMessagingPlugin */
Expand All @@ -41,9 +44,17 @@ public class FirebaseMessagingPlugin extends BroadcastReceiver
public static void registerWith(Registrar registrar) {
final MethodChannel channel =
new MethodChannel(registrar.messenger(), "plugins.flutter.io/firebase_messaging");
final MethodChannel backgroundCallbackChannel =
new MethodChannel(
registrar.messenger(),
"plugins.flutter.io/android_fcm_background",
JSONMethodCodec.INSTANCE);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, why do we use JSONMethodCodec here instead of the default?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is just a artifact from the alarm manager plugin I don't think it is necessary, the default codec should be sufficient.

final FirebaseMessagingPlugin plugin = new FirebaseMessagingPlugin(registrar, channel);
registrar.addNewIntentListener(plugin);
channel.setMethodCallHandler(plugin);
backgroundCallbackChannel.setMethodCallHandler(plugin);

FlutterFirebaseMessagingService.setBackgroundChannel(backgroundCallbackChannel);
}

private FirebaseMessagingPlugin(Registrar registrar, MethodChannel channel) {
Expand Down Expand Up @@ -99,7 +110,28 @@ private Map<String, Object> parseRemoteMessage(RemoteMessage message) {

@Override
public void onMethodCall(final MethodCall call, final Result result) {
if ("configure".equals(call.method)) {
if ("FcmDartService.start".equals(call.method)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with other plugins that use method names to represent both a class and a method (e.g. storage), consider using # here.

Also if the class you're calling is FlutterFirebaseMessagingService would it make sense to put that here as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used # for consistency.

FlutterFIrebaseMessagingService starts without the call from the Dart side. I'm happy to change the name but it should be something that indicates the setting up of the background channel.

kroikie marked this conversation as resolved.
Show resolved Hide resolved
long setupCallbackHandle = 0;
long backgroundMessageHandle = 0;
try {
List<Long> callbacks = ((ArrayList<Long>) call.arguments);
setupCallbackHandle = callbacks.get(0);
backgroundMessageHandle = callbacks.get(1);
} catch (Exception e) {
Log.e(TAG, "There was an exception when getting callback handle from dart side");
kroikie marked this conversation as resolved.
Show resolved Hide resolved
e.printStackTrace();
}
FlutterFirebaseMessagingService.setBackgroundSetupHandle(
kroikie marked this conversation as resolved.
Show resolved Hide resolved
this.registrar.context(), setupCallbackHandle);
FlutterFirebaseMessagingService.startBackgroundIsolate(
this.registrar.context(), setupCallbackHandle);
FlutterFirebaseMessagingService.setBackgroundMessageHandle(
this.registrar.context(), backgroundMessageHandle);
result.success(true);
} else if ("FcmDartService.initialized".equals(call.method)) {
FlutterFirebaseMessagingService.onInitialized();
result.success(true);
} else if ("configure".equals(call.method)) {
FirebaseInstanceId.getInstance()
.getInstanceId()
.addOnCompleteListener(
Expand Down
Loading