Flutter 集成第三方推送遇到的问题

1,641 阅读7分钟

Flutter 的开发过程中写 UI 是真的舒服,尤其是它的 HotReload。但是 App 光有界面显然是不够的,用过某些国产的 App 就能感受到这些 Appp 推送的能力。然而 Flutter 由于它的跨平台型,导致平台相关的代码都需要经过通道才来调用。不过好在一些厂家集成了各个平台的推送通道,这个时候只需要集成它家的 SDK 即可完成 Flutter 的推送需求。

一开始我用的是极光推送,虽然集成起来很快很简单,但是它的免费版本并不支持厂商通道,而且集成之后会发现 App 需要申请许许多多的权限,而这些权限对于一个简单的工具 App 是完全没有必要的。说实话,我也看不出来这些权限和推送有啥关系。唯一的理解就是,方便极光收集个人信息。然后极光收集的信息你还看不到,必须开通你懂的才能看。于是我果断换成了 Mob 推送。

不得不吐槽一下,Mob 官网写的文档有点乱,Flutter 相关的内容写的很少,需要参考 Android 的配置文档才能整明白。这里的流程无非是

  • 配置 Android 的依赖,引入 MobSDK
  • 配置好 AppKey, AppSecret
  • 配置厂商推送
  • 引入 Flutter 插件
  • 完成推送代码

经过以上简单的步骤之后,就完成了 Mobpush 的接入。不得不说看起来确实很简单的。在 mobpush 的后台发送一条推送信息,就可以在 App 上收到了。如果 App 在线就走 mob 的 TCP 通道,如果离线就走厂商通道。是不是很舒服呢?

自定义 Action

细心的话,就会发现 Mobpush 提供两种消息。一种是推送通知,另一种是自定义消息推送。第一种就是常见的推送,会在下拉的通知里面出现;另一种是 App 自定义推送,例如知乎的 App 内推送,它只会在 App 内出现。

BotToast提供的Notification演示

为了实现点击推送的动作,这里需要自己制定一下约束字段,将数据放在 extrasMap  里面即可。

点击推送启动 App 时的动作

有一个场景很常见,即当你点击一个 App 的推送通知时的动作,例如知乎,你点进去之后就可以之间看到相关回答,而不是进入到主页。

但是这一点 MobPush 的 Flutter 插件是无法做到的,即在 Flutter 里面是无法收到离线厂商推送的数据的。为此,我专门询问了 Mob 的客服,得到的回复是你需要自己处理厂商通道传递的数据。

好吧,光靠 Flutter 是真的难顶,必须要自己动手写安卓原生的代码。所以,我研究了一下 MobPush 的安卓示例代码。

	@Override
	protected void onNewIntent(Intent intent) {
		super.onNewIntent(intent);
		//需要调用setIntent方法,不然后面获取到的getIntent都试上一次传的数据
		setIntent(intent);
		dealPushResponse(intent);
	}

	private void dealPushResponse(Intent intent) {
		if (intent != null) {
			//获取厂商打开首页点击数据
			JSONArray jsonArray = MobPushUtils.parseMainPluginPushIntent(intent);
			System.out.println("parseMainPluginPushIntent:" + jsonArray);

			new PlayloadDelegate().playload(this, intent.getExtras());
		}
	}

可以看到,点击数据是从 intent 的携带的 extras  获取的。进一步研究一下 PayloadDelegate  里面有一段很关键的注释

/**
 * 推送消息携带附加数据处理类
 * >.附加数据的获取可在点击通知的落地页里的onCreate()或onNewIntent()里通过相关Intent的getExtras进行获取
 * >.落地页的附加数据大致分两种情况:1>MobPush自身通道和小米、魅族、华为三大厂商通道:
 * 1>默认落地页的情况下,针对MobPush自身通道和小米、魅族、华为三大厂商通道,需通过[msg]固定的key来获取数据
 * 2>配置scheme指定页的情况下,针对MobPush自身通道和小米、魅族、华为三大厂商通道,MobPush内部已经处理跳转指定页,并可通过Intent的getExtras用[data]进行获取指定页的附加数据
 * <p>
 * 2>OPPO通道:默认落地页和配置scheme指定页都是打开默认启动页(OPPO系统控制打开默认启动页),在默认启动页中
 * 附加数据都在[pluginExtra]的固定key中获取,如果配置了scheme指定页,对于OPPO通道来说是不生效的,需自行在代码
 * 中通过[mobpush_link_k]来获取scheme处理跳转,如果同时携带了scheme的附加数据则需通过[mobpush_link_v]获取数据
 * <p>
 * 3>FCM通道: 默认落地页和配置scheme指定页都是打开默认启动页(google 服务控制打开默认启动页),而且附加字段完全暴露在从Intent的getExtras的Bundle中,
 * 配置了scheme,对于FCM通道来说也是不生效的,需自行在代码中通过[mobpush_link_k]来获取scheme处理跳转,如果同时携带了scheme的附加数据则需通过[mobpush_link_v]获取数据
 */

    public void playload(Context context, Bundle bundle) {
        if (bundle == null) {
            return;
        }

        try {
            Set<String> keySet = bundle.keySet();
            if (keySet == null || keySet.isEmpty()) {
                return;
            }
            HashMap<String, Object> map = new HashMap<String, Object>();
            for (String key : keySet) {
                System.out.println("MobPush playload bundle------------->" + key);
                System.out.println("MobPush playload bundle------------->" + bundle.get(key));

                if (key.equals(MOB_PUSH_OPPO_EXTRA_DATA)) {

                    map = parseOPPOPlayload(bundle);

                } else if (key.equals(MOB_PUSH_NORMAL_PLAYLOAD_KEY)) {

                    map = parseNormalPlayload(bundle);

                } else {
                    Object object = bundle.get(key);
                    System.out.println(">>>>>>key: " + key + ", object: " + object);
                    map.put(key, String.valueOf(object));
                }
            }
            if (map != null && !map.isEmpty()) {
                realPerform(context, map);
            }
        } catch (Throwable throwable) {
            Log.e("MobPush", throwable.getMessage());
        }
    }

可以看到 payload  里面是在处理不同的厂商的数据,通过判断是否有关键厂商的字段,然后分别处理。将数据保存在 map  里面,进行 action  动作的处理。

这样就知道如何获取厂商数据了,接下来的问题就是如何将数据传递给 flutter 。很容易想到,可以自己写一个 channel  来传递数据。但是这样成本太高了,我的方案就是修改 mobpush_plugin 的这个插件,让它能传递厂商的数据。

为此需要了解一下 mobpush_plugin  是如何传递推送数据的。

    /**
     * Plugin registration.
     */
    public static void registerWith(Registrar registrar) {
        final MethodChannel channel = new MethodChannel(registrar.messenger(), "mob.com/mobpush_plugin");
        channel.setMethodCallHandler(new MobpushPlugin());

        MobpushReceiverPlugin.registerWith(registrar);

        createMobPushReceiver();
        MobPush.addPushReceiver(mobPushReceiver);
    }

在注册 mobpush_plugin  时候,它额外注册了一个 MobpushReceiverPlugin  的类。

    /**
     * Plugin registration.
     */
    public static void registerWith(Registrar registrar) {
        final EventChannel channel = new EventChannel(registrar.messenger(), "mobpush_receiver");
        channel.setStreamHandler(new MobpushReceiverPlugin());
    }

在这个类里面,接受 MobSDK  的消息,然后传递给 Flutter

    private MobPushReceiver createMobPushReceiver(final EventChannel.EventSink event) {
        mobPushReceiver = new MobPushReceiver() {
            @Override
            public void onCustomMessageReceive(Context context, MobPushCustomMessage mobPushCustomMessage) {
                HashMap<String, Object> map = new HashMap<String, Object>();
                map.put("action", 0);
                map.put("result", hashon.fromJson(hashon.fromObject(mobPushCustomMessage)));
                event.success(hashon.fromHashMap(map));
            }

            @Override
            public void onNotifyMessageReceive(Context context, MobPushNotifyMessage mobPushNotifyMessage) {
                HashMap<String, Object> map = new HashMap<String, Object>();
                map.put("action", 1);
                map.put("result", hashon.fromJson(hashon.fromObject(mobPushNotifyMessage)));
                event.success(hashon.fromHashMap(map));
            }

            @Override
            public void onNotifyMessageOpenedReceive(Context context, MobPushNotifyMessage mobPushNotifyMessage) {
                HashMap<String, Object> map = new HashMap<String, Object>();
                map.put("action", 2);
                map.put("result", hashon.fromJson(hashon.fromObject(mobPushNotifyMessage)));
                event.success(hashon.fromHashMap(map));
            }

            @Override
            public void onTagsCallback(Context context, String[] tags, int operation, int errorCode) {

            }

            @Override
            public void onAliasCallback(Context context, String alias, int operation, int errorCode) {

            }
        };
        return mobPushReceiver;
    }

    @Override
    public void onListen(Object o, EventChannel.EventSink eventSink) {
        mobPushReceiver = createMobPushReceiver(eventSink);
        MobPush.addPushReceiver(mobPushReceiver);
    }

这个 onListen  就是 Flutter 那边进行监听的时候调用的。这样推送数据就传给了 Flutter,插件还很贴心的区分了不同的推送类型。这里我们需要关注的就是 onNotifyMessageOpenedReceive  这个类型。

为了让 Flutter 收到启动时候的消息,需要修改 onListen  这个方法。首先给这类添加一个初始推送消息。

private static MobPushNotifyMessage initNotifyMessage;  //初始推送消息
private static long defaultDelaySpan;					//第一次发送等待间隔

 public static void setInitNotifyMessage(MobPushNotifyMessage msg) {
     setInitNotifyMessage(msg, 1500);
 }

 public static void setInitNotifyMessage(MobPushNotifyMessage msg, long delaySpan) {
      initNotifyMessage = msg;
      defaultDelaySpan = delaySpan;
 }

这个时候需要安卓在启动的时候调用 setInitNotifyMessage  用了初始化第一次推送通知。


class MainActivity : FlutterActivity() {
    private val tag = "MainActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        Log.d(tag, "onCreate")
        val pushExtra = intent.extras ?: return
        handleNotifyOpen(pushExtra)
    }

    private fun handleNotifyOpen(data: Bundle) {
        Log.d(tag, "NotifyOpen: ${data.toString()}")
        val message = MobPushNotifyMessage()
        val pushData = data.getString("pushData") ?: return
        val pushJson = JSONObject(pushData)
        val extraMap = HashMap<String, String>()
        for (key in pushJson.keys()) {
            extraMap[key] = pushJson.getString(key)
        }
        message.extrasMap = extraMap
        MobpushReceiverPlugin.setInitNotifyMessage(message)
    }

    override fun onNewIntent(nIntent: Intent) {
        super.onNewIntent(nIntent)
        intent = nIntent
    }

    override fun onDestroy() {
        super.onDestroy()
        MobpushReceiverPlugin.setInitNotifyMessage(null)
        intent.extras?.clear()
    }
}

这样在 onCreate  里面判断 intent  是否携带了推送数据,如果携带了就将其转换成 MobPushNotifyMessage 然后传递给 mobpush_plugin 。这样 Flutter 就能接受到启动的推送通知了。

这里需要注意一点就是 onNewIntent  这个方法,如果没有这个方法, intent  携带的数据可能是上一次的数据,导致推送重复/失败。

Sharesdk 和 mobpush_plugin 冲突问题

集成了推送的同时也集成了 mobshare 的第三方认证,但是这样会导致 appkey is illegal  的错误。 客服给的解决办法是,将 AppKey  和 AppSecret  写到 AndroidManifest.xml  里面。

 				<meta-data
                android:name="Mob-AppKey"
                android:value="appkey" />

				<meta-data
                android:name="Mob-AppSecret"
                android:value="appsecret" />

小结

只会 Flutter 是不够的,平台相关的代码还是需要好好掌握的。不然出了问题都不知道是啥情况。