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 内出现。
为了实现点击推送的动作,这里需要自己制定一下约束字段,将数据放在 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
携带的数据可能是上一次的数据,导致推送重复/失败。
- 修改后的插件 github.com/dreamer2q/M…
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 是不够的,平台相关的代码还是需要好好掌握的。不然出了问题都不知道是啥情况。