31 How to Realize Native Push Capabilities

31 How to Realize Native Push Capabilities #

Hello, I’m Chen Hang.

In the previous article, I shared with you how to use Provider to maintain the shared data state of Flutter components. In Flutter, state is data, and through the encapsulation, injection, and read-write of data resources, we can not only achieve data sharing between components, but also precisely control the granularity of UI refresh to avoid unnecessary component refresh.

In fact, data sharing not only exists within the client, but also between the server and the client. For example, when there are new comments on a Weibo post or major news events occur, we need to push these status change messages from the server to the client in real time to notify users of new content. Sometimes, we also use push notifications to deliver precise marketing messages to specific user profiles.

It can be said that message push is an important means of enhancing user stickiness and promoting user growth. So, what is the process of message push like?

Message Push Workflow #

With so many mobile pushes every day, it seems simple to us. However, message push is a complex business application scenario involving five parties: business server, third-party push service provider, operating system push service, user terminal, and mobile application.

On iOS, Apple Push Notification service (APNs) handles the message notification needs for all applications. On the other hand, Android, in its native form, provides a cloud messaging mechanism similar to Firebase called Firebase Cloud Messaging (FCM), which allows for unified push hosting service.

When an application needs to send message notifications, the message is first sent by the application’s server to Apple or Google, and then sent to the device via APNs or FCM. Once the device’s operating system completes the parsing, it ultimately delivers the message to the corresponding application. The diagram below illustrates this process.

Figure 1 Native message push process

However, Google services are not stable in mainland China. Therefore, Chinese Android phones usually replace Google services with their own services and customize a set of push standards. For developers, this undoubtedly increases the adaptation burden. Therefore, for Android, we usually use third-party push services such as JPush and Umeng Push.

Although these third-party push services use self-built long connections and cannot enjoy the optimization of the operating system’s underlying layers, they share push channels for all apps using push services. As long as one application using a third-party push service is not killed by the system, messages can be delivered in a timely manner.

On the other hand, these third-party services simplify the process of establishing a connection between the business server and the mobile push service, allowing our business server to complete message push with simple API calls.

To maintain the consistency between Android and iOS solutions, we also use third-party push services that encapsulate APNs communication on iOS.

The workflow of third-party push services is shown in the following diagram.

Figure 2 Workflow of third-party push service

The capabilities and access processes provided by most third-party push service providers are generally consistent. Considering the relatively active community and ecosystem of JPush, today we will take JPush as an example to see how to use native push capabilities in Flutter applications.

Native Push Access Process #

To receive push notification messages in Flutter, we need to expose the native push capabilities to the Flutter application. This involves integrating the native code host (JPush SDK) with the native project and providing a mechanism for receiving push messages through method channels in the Dart layer.

Plugin Project #

In the 26th article, we learned how to register native code host callbacks in the Flutter application entry point in the native project, allowing Dart to call native interfaces. This approach is simple and straightforward, suitable for scenarios where there is minimal interaction and clear data flow between the Dart layer and the native interfaces.

However, for modules like push notifications that involve multiple data flows between Dart and native code and have a large amount of code, this coupled approach in the project is not conducive to independent development and maintenance. In this case, we need to use the plugin project provided by Flutter to encapsulate it separately.

A Flutter plugin project is similar to a normal application project, with both the android and ios directories where the platform-specific logic code is placed. The plugin project also includes an example project, which is a regular Flutter application project that references the plugin code. We can directly debug the plugin functionality through the example project.

Figure 3 Plugin project directory structure

After understanding the overall project directory structure, we need to go to the flutter_push_plugin.dart file where the Dart plugin code is located, and implement the Dart layer’s push interface encapsulation.

Dart Interface Implementation #

To accurately deliver messages, we need to provide an identifier for the app on the phone, such as a token or ID. Once the identifier is reported, we can wait for the business server to send us messages.

Since we need to use third-party push services like JPush, we need to perform some pre-association and binding of application information, as well as SDK initialization. As we can see, for an app, the process of integrating push can be broken down into the following three steps:

  1. Initialize the JPush SDK.
  2. Obtain the address ID.
  3. Register for message notifications.

These three steps correspond to the calling of three native interfaces in the Dart layer that need to be encapsulated: setup, registrationID, and setOpenNotificationHandler.

The first two interfaces are called on the method channel to invoke methods provided by the native code host, while the registration of the callback function setOpenNotificationHandler is the opposite. It is the native code host that calls the Dart layer’s event callback on the method channel. Therefore, we need to register a reverse callback method on the method channel for the native code host, so that the native code host can notify it directly when it receives a message.

In addition, considering that push is a capability shared by the entire application, we encapsulated the FlutterPushPlugin class as a singleton:

// Flutter Push Plugin
class FlutterPushPlugin {
  // Singleton
  static final FlutterPushPlugin _instance = new FlutterPushPlugin.private(const MethodChannel('flutter_push_plugin'));
  // Method channel
  final MethodChannel _channel;
  // Event callback
  EventHandler _onOpenNotification;
  // Constructor
  FlutterPushPlugin.private(MethodChannel channel) : _channel = channel {
    // Register the reverse callback method for the native code host so that it can execute the onOpenNotification method
    _channel.setMethodCallHandler(_handleMethod);
  }
  // Initialize the JPush SDK
  setupWithAppID(String appID) {
    _channel.invokeMethod("setup", appID);
  }
  // Register for message notifications
  setOpenNotificationHandler(EventHandler onOpenNotification) {
    _onOpenNotification = onOpenNotification;
  }

  // Register the reverse callback method for the native code host so that it can execute the onOpenNotification method
  Future<Null> _handleMethod(MethodCall call) {
    switch (call.method) {
      case "onOpenNotification":
        return _onOpenNotification(call.arguments);
      default:
        throw new UnsupportedError("Unrecognized Event");
    }
  }
  // Obtain the address ID
  Future<String> get registrationID async {
    final String regID = await _channel.invokeMethod('getRegistrationID');
    return regID;
  }
}

The Dart layer acts as the proxy for the native code host, and as you can see, the interface design at this level is relatively simple. Next, we will separately implement the Android and iOS platforms for push.

Android Interface Implementation #

Considering that the push configuration work on the Android platform is relatively simple, we will start by opening the android project in the example directory using Android Studio for plugin development. Note that because the Android subproject depends on the build artifacts of the Flutter project for its runtime, you need to ensure that the entire project has been built at least once before opening the Android project for development, otherwise, the IDE will report an error.

Note: The following steps refer to the JPush Android SDK integration guide.

First, we need to add the JPush SDK, namely jpush and jcore, to the build.gradle file in the plugin project:

dependencies {
  implementation 'cn.jiguang.sdk:jpush:3.3.4'
  implementation 'cn.jiguang.sdk:jcore:2.1.2'
}

Then, in the native interface FlutterPushPlugin class, we sequentially provide the implementations of the three Dart-layer interfaces: setup, getRegistrationID, and onOpenNotification, using the JPush Android SDK.

It is worth noting that since the information binding for the JPush Android SDK is set in the application’s packaging configuration and does not need to be completed through code (only needed for iOS), the Android version of the setup method is an empty implementation:

public class FlutterPushPlugin implements MethodCallHandler {
  // Registrar, usually refers to MainActivity
  public final Registrar registrar;
  // Method channel
  private final MethodChannel channel;
  // Plugin instance
  public static FlutterPushPlugin instance;
  // Register the plugin
  public static void registerWith(Registrar registrar) {
    // Register the method channel
    final MethodChannel channel = new MethodChannel(registrar.messenger(), "flutter_push_plugin");
    instance = new FlutterPushPlugin(registrar, channel);
    channel.setMethodCallHandler(instance);
    // Move the JPush SDK initialization up to the plugin registration
    JPushInterface.setDebugMode(true);
    JPushInterface.init(registrar.activity().getApplicationContext());
  }
  // Private constructor
  private FlutterPushPlugin(Registrar registrar, MethodChannel channel) {
    this.registrar = registrar;
    this.channel = channel;
    import 'dart:async';

    import 'package:flutter/services.dart';

    /// 插件的Flutter部分
    class FlutterPushPlugin {
      static const MethodChannel _channel =
          const MethodChannel('flutter_push_plugin');

      /// 推送消息打开回调
      static Future<dynamic> onOpenNotification(
          Map<String, dynamic> notification) async {
        dynamic result = await _channel.invokeMethod("onOpenNotification", notification);
        return result;
      }
    }

然后,在iOS工程中,找到极光SDK提供的文件JPUSHService.m,将其中的onOpenNotification方法调用替换为下面的代码: replay_result(JPMethodOpenNotification, userInfo, [notification, jiguang]); 如果没有在flutter_push_plugin中创建名为回调的函数的话,需要创建一个名为onOpenNotification的回调,类里添加 方法如下: 如果iOS在前台需要弹窗提示,可以在创建回调再调用 ```objc - (void)onOpenNotification:(NSDictionary *)notification { [[NSNotificationCenter defaultCenter] postNotificationName:kJPFDidReceiveRemoteNotification object:notification]; if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) { UIWindow *window = [[[UIApplication sharedApplication] delegate] window]; [NTESBoxToast showToast:@“推送内容” inView:window.rootViewController.view]; }

  }
//接口初始化函数
#import "AppDelegate+JPush.h"
#import <JPush/JPUSHService.h>
#ifdef NSFoundationVersionNumber_iOS_9_x_Max
#import <UserNotifications/UserNotifications.h>
#endif
#import "GeneratedPluginRegistrant.h"
@implementation AppDelegate (JPush)
- (void)jpushNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(NSInteger))completionHandler {
  
  NSDictionary * userInfo = notification.request.content.userInfo;
  if([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
    [JPUSHService handleRemoteNotification:userInfo];
    NSLog(@"iOS前台收到远程通知:%@", [self logDic:userInfo]);

    //设置根控制器的角标
    [UIApplication sharedApplication].applicationIconBadgeNumber = 0;
    
    // iOS 10 收到远程通知 选中效果处理
    completionHandler(UNNotificationPresentationOptionSound|UNNotificationPresentationOptionAlert);
  }else{
    // 判断为本地通知
  }
}
//点击推送消息调用的函数
- (void)jpushNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler {
  NSDictionary * userInfo = response.notification.request.content.userInfo;
  if([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
    [JPUSHService handleRemoteNotification:userInfo];
    NSLog(@"iOS后台或启动状态下点击远程通知:%@", [self logDic:userInfo]);
    NSString* content = userInfo[@"aps"][@"alert"];
      if ([content isKindOfClass:[NSDictionary class]]) {
          content = userInfo[@"aps"][@"alert"][@"body"];
      }
      FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
      FlutterMethodChannel* methodChannel = [FlutterMethodChannel methodChannelWithName:@"flutter_push_plugin" binaryMessenger:controller];
      [methodChannel invokeMethod:@"onOpenNotification" arguments:content];
    completionHandler();
  }else{
    // 判断为本地通知
  }
}
- (void)application:(UIApplication*)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken{
  [JPUSHService registerDeviceToken:deviceToken];
}
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
  //Optional
  NSLog(@"注册APNs失败%@",error);
}
- (void)application:(UIApplication *)application
handleActionWithIdentifier:(NSString *)identifier
forRemoteNotification:(NSDictionary *)userInfo
  completionHandler:(void (^)(void))completionHandler {
  // APNs通知的操作进行统计上报
  [[NSNotificationCenter defaultCenter] postNotificationName:kJPFDidReceiveRemoteNotification object:userInfo];
  completionHandler();  // 系统要求执行这个方法
}
/// test

@end

Figure 6: Example iOS Push Registration

By following the steps above, we have completed the process of binding the push certificate with the JPush information. Next, we return to Xcode and open the example project to do the final configuration.

First, we need to enable the Push Notifications option for the Application Target’s Capabilities in the example project, to enable push capability support for the application, as shown in the following figure:

Figure 7: Example iOS Push Configuration

Then, we need to switch to the Info panel of the Application Target and manually configure the NSAppTransportSecurity key-value pair to support JPush SDK’s non-HTTPS domain services:

Figure 8: Example iOS HTTP Support Configuration

Finally, in the Bundle identifier item under the Info tab, explicitly update it with the Bundle ID we just registered on the JPush website:

Figure 9: Bundle ID Configuration

With these steps, all the necessary native configuration work and interface implementations for the example project have been completed. Next, we can use the FlutterPushPlugin plugin in the main.dart file of the example project to implement native push functionality.

In the following code, we register the JPush service using the plugin singleton in the main function’s entry point. After that, we obtain the JPush registration address and set the push message callback during the application state initialization:

// Obtain the push plugin singleton
FlutterPushPlugin fpush = FlutterPushPlugin();
void main() {
  // Register JPush service with App ID (for iOS only)
  fpush.setupWithAppID("f861910af12a509b34e266c2");
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  // JPush registration address (regID)
  String _regID = 'Unknown';
  // Received push message
  String _notification = "";

  @override
  initState() {
    super.initState();
    // Register push message callback
    fpush.setOpenNotificationHandler((String message) async {
      // Refresh the UI to display the push message
      setState(() {
        _notification = message;
      });
    });
    // Obtain the push address (regID)
    initPlatformState();
  }
  
  initPlatformState() async {
    // Call the plugin's registration ID method
    String regID = await fpush.registrationID;
    // Refresh the UI to display the regID
    setState(() {
      _regID = regID;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Column(
            children: <Widget>[
              // Display the regID and received message
              Text('Running on: $_regID\n'),
              Text('Notification Received: $_notification')
            ],
          ),
        ),
      ),
    );
  }
}

After clicking run, we can see that our application can now obtain the JPush registration address:

Figure 10: iOS Example Run

Figure 11: Android Example Run

Next, let’s send a real push message through the JPush Developer Console. Select our app in the console, then enter the JPush Push Console. Now we can perform a push message test.

In the “Send Notification” section, change the notification title to “Test” and set the notification content to “JPush Test”. In the “Target Audience” section, since this is a test account, we can directly select “Broadcast to Everyone”. If you want to target specific recipients, you can also provide the JPush registration address (Registration ID) obtained in the application:

Figure 12: JPush Console

Click “Send Preview and Confirm”, and we can see that our application can not only be woken up by push messages from JPush, but can also receive message content forwarded by the native host in the Flutter application:

Figure 13: iOS Push Message

Figure 14: Android Push Message

Summary #

Okay, that’s all for today’s sharing. Let’s summarize what we’ve learned.

We have provided a Dart-level wrapping for the JPush SDK as a Flutter plugin project. The plugin project includes both the iOS and Android directories, where we can wrap the native code host to provide interfaces for Dart-level forward callbacks (such as initialization and obtaining JPush push addresses), as well as using method channels to forward push messages to Dart in a reverse callback manner.

Today, I shared with you a lot of logic for configuring, binding, and registering the native code host. It is not difficult to see that the push process has a long chain, involves many parties, and has complex configurations. In order to fully implement native push capabilities in Flutter, the main workload is focused on the native code host, and there is not much that can be done in the Dart layer.

I have posted the Flutter_Push_Plugin that I modified today on GitHub. You can download the plugin project and run it multiple times to experience the similarities and differences between a plugin project and a regular Flutter project, and deepen your understanding of the entire push message process. The Flutter_Push_Plugin provides the minimum set of functions to implement native push functionality, and you can improve this plugin according to your actual needs.

It is worth noting that today’s actual project demonstration was completed through the embedded example project. If you have a standalone Flutter project (such as Flutter_Push_Demo) that needs to integrate Flutter_Push_Plugin, the configuration steps are no different from the example project, with the only difference being that you need to explicitly declare the plugin’s dependency in the pubspec.yaml file:

dependencies:
  flutter_push_plugin:
    git:
      url: https://github.com/cyndibaby905/31_flutter_push_plugin.git

Thought Experiment #

In the native implementation of Flutter_Push_Plugin, when a user clicks on a push notification to wake up the Flutter application, we wait for 1 second before executing the corresponding Flutter callback notification to ensure Flutter completes initialization. Is there any room for optimization in this logic? How would you optimize it to allow Flutter code to receive push notifications faster?

Feel free to leave me a comment in the comment section to share your thoughts. I will be waiting for you in the next article! Thank you for listening, and feel free to share this article with more friends to read together.