flutter_umeng_plus 插件可实现的功能:
- 同时适配于 Android Ios 两个平台
- 实现友盟多渠道统计
- 实现页面的进入与退出统计
- 实现自定义事件的统计
https://zhuanlan.zhihu.com/p/102749769
https://github.com/dongweiq/flutter_umpush
https://github.com/jpush/jverify-flutter-plugin
https://blog.csdn.net/yemeishu6033022/article/details/103970275
Flutter Plugin插件开发
1.创建Flutter Plugin插件项目
这里推荐使用Android Studio创建项目,根据提示一步一步来就行了,截图如下:
如果对于kotlin或者swift不熟悉,或者第三方包的官方代码没有kotlin和swift实现,那么不选择底下kotlin和swift,官方是java和c
生成的项目目录主要包含以下内容:
- android // Android 相关原生代码目录
- ios // ios 相关原生代码目录
- lib // Dart 代码目录,主要是创建“MethodChannel”,然后接收并处理来自原生平台发来的消息
- example // 一个完整的调用了我们正在开发的插件的 Flutter App
-
pubspec.yaml // 项目配置文件
Flutter 如何调用原生代码
上方来自官方的架构图已经足够清晰了,Flutter 通过 MethodChannel 发起某一方法的调用,然后原生平台收到消息后执行相应的实现(Java/Kotlin/Swift/Object-C)并异步地返回结果
FlutterView,视图层,SurfaceView子类。
FlutterNativeView,将视图层JIN部分抽离,和FlutterJNI相关的类。
DartExecutor,DartMessenger包装者类。
DartMessenger,二进制消息信使,通过FlutterJNI和底层(C++)通信,实现两个接口,同时具有发送和接收Dart消息功能。
FlutterJNI,JNI相关类,native方法。
BinaryMessenger,接口,向Dart层发送消息。
PlatformMessageHandler,接口,接收Dart层消息。
MethodChannel,通道,代表一个调用通道,即数据流管道。
原生和flutter之间数据交互类型有限制
在进行插件的开发时,就必定会涉及到原生和flutter之间的数据交互.这里需要注意的是,就像我们在进行react-native和JNI的开发时,并不是什么类型的数据都是支持交互的.下面我给出原生和flutter之间可交互的数据类型:
Dart | Android | iOS |
---|---|---|
null | null | nil (NSNull when nested) |
bool | java.lang.Boolean | NSNumber numberWithBool: |
int | java.lang.Integer | NSNumber numberWithInt: |
int, if 32 bits not enough | java.lang.Long | NSNumber numberWithLong: |
double | java.lang.Double | NSNumber numberWithDouble: |
String | java.lang.String | NSString |
Uint8List | byte[] | FlutterStandardTypedData typedDataWithBytes: |
Int32List | int[] | FlutterStandardTypedData typedDataWithInt32: |
Int64List | long[] | FlutterStandardTypedData typedDataWithInt64: |
Float64List | double[] | FlutterStandardTypedData typedDataWithFloat64: |
List | java.util.ArrayList | NSArray |
Map | java.util.HashMap | NSDictionary |
这里我们用得最多的就是bool、int、String、Map这几个类型了
2.实现插件功能
集成Android端
由于Flutter自动依赖插件的方式存在两个版本(Registrar和FlutterPluginBinding), 因此我们在实现Android的插件的时候,为了能提高兼容性,最好把这两种都实现一遍.所以,Android的插件需要实现FlutterPlugin, ActivityAware, MethodCallHandler这三个接口
- registerWith 静态方法是flutter旧的加载插件的方式,通过反射进行加载.
- onAttachedToEngine 和onDetachedFromEngine 是FlutterPlugin 的接口方法,是flutter新的加载插件的方式.
- onAttachedToActivity 和onDetachedFromActivity 是ActivityAware 的接口方法,主要是用于获取当前flutter页面所处的Activity.
- onMethodCall 是MethodCallHandler 的接口方法,主要用于接收Flutter端对原生方法调用的实现.
先打开友盟的官方文档https://developer.umeng.com/docs/119267/detail/118584
我们使用maven的方式集成,手动添加SDK地方式这里不展开,这种更新会比较麻烦。
maven自动集成
在工程build.gradle配置脚本中buildscript和allprojects段中添加【友盟+】SDK新maven仓库地址。
AndroidMainF添加权限
权限一般不会写在我们Libary的Android端里面,实在要写也没问题,但是管理就会比较麻烦
在example下面的Android端
Android具体实现
这里主要是根据文档,在onMethodCall完成调用
package com.philos.flutter_umeng_plus;
import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import com.umeng.analytics.MobclickAgent;
import com.umeng.commonsdk.UMConfigure;
import com.umeng.commonsdk.statistics.common.DeviceConfig;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.net.NetworkInterface;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;
import com.alibaba.fastjson.JSONObject;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
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.Registrar;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
/** FlutterUmengPlusPlugin */
public class FlutterUmengPlusPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private MethodChannel channel;
private WeakReference<Activity> activity;
private Application mApplication;
///重写之后io.flutter.plugins下自动生成的GeneratedPluginRegistrant调用的是无参的构造函数,编译不通过,由于不知道这里如何更改,所以换了initPlugin的方式
// private FlutterUmengPlusPlugin(Registrar registrar, MethodChannel channel) {
// this.activity = new WeakReference<>(registrar.activity());
// this.channel = channel;
// mApplication = (Application) registrar.context().getApplicationContext();
// }
public FlutterUmengPlusPlugin initPlugin(MethodChannel methodChannel, Registrar registrar) {
channel = methodChannel;
mApplication = (Application) registrar.context().getApplicationContext();
activity = new WeakReference<>(registrar.activity());
return this;
}
//此处是新的插件加载注册方式
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "flutter_umeng_plus");
channel.setMethodCallHandler(this);
mApplication = (Application) flutterPluginBinding.getApplicationContext();
}
// This static function is optional and equivalent to onAttachedToEngine. It supports the old
// pre-Flutter-1.12 Android projects. You are encouraged to continue supporting
// plugin registration via this function while apps migrate to use the new Android APIs
// post-flutter-1.12 via https://flutter.dev/go/android-project-migration.
//
// It is encouraged to share logic between onAttachedToEngine and registerWith to keep
// them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called
// depending on the user's project. onAttachedToEngine or registerWith must both be defined
// in the same class.
//此处是旧的插件加载注册方式
public static void registerWith(Registrar registrar) {
final MethodChannel channel = new MethodChannel(registrar.messenger(), "flutter_umeng_plus");
// channel.setMethodCallHandler(new FlutterUmengPlusPlugin(registrar,channel));
channel.setMethodCallHandler(new FlutterUmengPlusPlugin().initPlugin(channel, registrar));
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
if (call.method.equals("getPlatformVersion")) {
result.success("Android " + android.os.Build.VERSION.RELEASE);
} else if (call.method.equals("preInit")) {
preInit(call, result);
} else if (call.method.equals("init")) {
init(call, result);
} else if (call.method.equals("onPageStart")) {
onPageStart(call, result);
} else if (call.method.equals("onPageEnd")) {
onPageEnd(call, result);
} else if (call.method.equals("onResume")) {
///APP启动、使用时长等基础数据统计接口
MobclickAgent.onResume(activity.get());
} else if (call.method.equals("onPause")) {
///APP启动、使用时长等基础数据统计接口
MobclickAgent.onPause(activity.get());
} else if (call.method.equals("onKillProcess")) {
///程序退出时,用于保存统计数据的API。
///如果开发者调用kill或者exit之类的方法杀死进程,请务必在此之前调用onKillProcess方法,用来保存统计数据。
Log.d("onKillProcess", "context !=null"+(activity.get().getApplicationContext() != null));
MobclickAgent.onKillProcess(activity.get().getApplicationContext());;
} else if (call.method.equals("onEventObject")) {
onEventObject(call, result);
} else if (call.method.equals("onProfileSignIn")) {
////当用户使用自有账号登录时,可以这样统计:
if (call.hasArgument("userID")) {
String userID = (String)call.argument("userID");
Log.d("onProfileSignIn", "userID:"+userID);
MobclickAgent.onProfileSignIn(userID);;
}
} else if (call.method.equals("onProfileSignOff")) {
//登出
MobclickAgent.onProfileSignOff();;
} else if (call.method.equals("onProfileSignOff")) {
setLogEnabled(call, result);;
} else if (call.method.equals("getTestDeviceInfo")) {
// getTestDeviceInfo(activity.get());;
getDeviceInfo(activity.get());
} else if (call.method.equals("setProcessEvent")) {
/// 支持在子进程中统计自定义事件
///如果需要在某个子进程中统计自定义事件,则需保证在此子进程中进行SDK初始化。
if (call.hasArgument("enable")) {
UMConfigure.setProcessEvent((Boolean)call.argument("enable"));
}
} else {
result.notImplemented();
}
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
channel =null;
}
@Override
public void onAttachedToActivity(@NonNull final ActivityPluginBinding binding) {
this.activity = new WeakReference<>(binding.getActivity());
///这里可以处理权限请求代码
// checkPerssion();
// binding.addRequestPermissionsResultListener(new PluginRegistry.RequestPermissionsResultListener() {
// @Override
// public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
// switch (requestCode) {
// case mPermissionCode:
// if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_DENIED) {
// Toast.makeText(binding.getActivity(),"已拒绝访问设备上照片及文件权限!",Toast.LENGTH_SHORT).show();
// } else {
// initXXXXXX();
// }
// break;
// }
// return false;
// }
// });
}
@Override
public void onDetachedFromActivityForConfigChanges() {
}
@Override
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
}
@Override
public void onDetachedFromActivity() {
activity =null;
}
private void preInit(MethodCall call, Result result) {
String appKey = wrapAppKey(call,activity.get());
String channel = wrapChannel(call,activity.get());
UMConfigure.preInit(activity.get(),appKey,channel);
result.success(true);
}
/**
* 注意: 即使您已经在AndroidManifest.xml中配置过appkey和channel值,也需要在App代码中调
* 用初始化接口(如需要使用AndroidManifest.xml中配置好的appkey和channel值,
* UMConfigure.init调用中appkey和channel参数请置为null)。
*/
private void init(MethodCall call, Result result) {
String appKey = wrapAppKey(call,activity.get());
String channel = wrapChannel(call,activity.get());
Integer deviceType = null;
if (call.hasArgument("deviceType")) {
deviceType = (Integer)call.argument("deviceType");
} else {
deviceType = UMConfigure.DEVICE_TYPE_PHONE;
}
String pushSecret = null;
if (call.hasArgument("pushSecret")) {
pushSecret = (String)call.argument("pushSecret");
}
Boolean logEnable =null;
if (call.hasArgument("logEnable")) {
logEnable = (Boolean)call.argument("logEnable");
}
Boolean encrypt = null;
if (call.hasArgument("encrypt")) {
encrypt = (Boolean)call.argument("encrypt");
}
if(logEnable != null){
/**
* 设置组件化的Log开关
* 参数: boolean 默认为false,如需查看LOG设置为true
* 日志分为四种等级,方便用户查看:
* Error(打印SDK集成或运行时错误信息)。
* Warn(打印SDK警告信息)。
* Info(打印SDK提示信息)。
* Debug(打印SDK调试信息)。
*/
UMConfigure.setLogEnabled(logEnable);
}
// getTestDeviceInfo(activity.get());
UMConfigure.init(activity.get(),appKey,channel,deviceType,pushSecret);
if(encrypt != null){
UMConfigure.setEncryptEnabled(encrypt);
}
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// 大于等于4.4选用AUTO页面采集模式
MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.AUTO);
} else {
MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.MANUAL);
}
// interval 单位为毫秒,如果想设定为40秒,interval应为 40*1000.
MobclickAgent.setSessionContinueMillis(4*1000);
result.success(true);
}
private void setLogEnabled(MethodCall call, Result result) {
Boolean logEnable = (Boolean)call.argument("logEnable");
if (logEnable != null) UMConfigure.setLogEnabled(logEnable);
result.success(true);
}
///这个查询不到数据,暂时停用
public static String[] getTestDeviceInfo(Context context) {
Log.d("getTestDeviceInfo", "context==null :"+(context==null));
int hasWriteContactsPermission = context.checkSelfPermission(Manifest.permission.READ_PHONE_STATE);
Log.d("getTestDeviceInfo", "hasREAD_PHONE_STATE:"+hasWriteContactsPermission);
String[] deviceInfo = new String[2];
try {
if (context != null) {
deviceInfo[0] = DeviceConfig.getDeviceIdForGeneral(context);
deviceInfo[1] = DeviceConfig.getMac(context);
Log.d("UM", deviceInfo[0]);
Log.d("UM", deviceInfo[1]);
}
} catch (Exception e) {
}
return deviceInfo;
}
private void onPageStart(MethodCall call, Result result) {
String name = (String)call.argument("pageName");
Log.d("UM", "onPageStart: " + name);
MobclickAgent.onPageStart(name);
// MobclickAgent.onResume(activity.get());
result.success(null);
}
private void onPageEnd(MethodCall call, Result result) {
String name = (String)call.argument("pageName");
Log.d("UM", "onPageEnd: " + name);
MobclickAgent.onPageEnd(name);
// MobclickAgent.onPause(activity.get());
result.success(null);
}
///event id长度不能超过128个字节,key不能超过128个字节,value不能超过256个字节
//id、ts、du、token、device_name、device_model 、device_brand、country、city、
// channel、province、appkey、app_version、access、launch、pre_app_version、terminate、
// no_first_pay、is_newpayer、first_pay_at、first_pay_level、first_pay_source、first_pay_user_level、
// first_pay_versio是保留字段,不能作为event id 及key的名称;
public void onEventObject(MethodCall call, Result result) {
String eventId = (String)call.argument("eventId");
if (call.hasArgument("map")) {
Map<String,Object> map = JSONObject.parseObject((String)call.argument("map"));
// Map<String, Object> map = new HashMap<String, Object>(call.argument("map"));
MobclickAgent.onEventObject(activity.get(), eventId, map);
} else if (call.hasArgument("label")) {
MobclickAgent.onEvent(activity.get(), eventId, (String)call.argument("label"));
} else {
MobclickAgent.onEvent(activity.get(), eventId);
}
result.success(null);
}
public static String getChannel(Context context) {
try {
ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
String channel = appInfo.metaData.getString("UMENG_CHANNEL");
return channel;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();;
}
return null;
}
public static String getAppKey(Context context) {
try {
ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
String appKey = appInfo.metaData.getString("UMENG_APPKEY");
return appKey;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();;
}
return null;
}
public static String wrapChannel(MethodCall call,Context context){
String channel = null;
if (call.hasArgument("channel")) {
channel = (String)call.argument("channel");
}
if(channel ==null || channel.isEmpty()){
channel = getChannel(context);
}
return channel;
}
public static String wrapAppKey(MethodCall call,Context context){
String appKey = null;
if (call.hasArgument("appKey")) {
appKey = (String)call.argument("appKey");
}
if(appKey ==null || appKey.isEmpty()){
appKey = getAppKey(context);
}
return appKey;
}
public static String getDeviceInfo(Context context) {
try {
org.json.JSONObject json = new org.json.JSONObject();
android.telephony.TelephonyManager tm = (android.telephony.TelephonyManager) context
.getSystemService(Context.TELEPHONY_SERVICE);
String device_id = null;
if (checkPermission(context, Manifest.permission.READ_PHONE_STATE)) {
device_id = tm.getDeviceId();
}
String mac = getMac(context);
json.put("mac", mac);
if (TextUtils.isEmpty(device_id)) {
device_id = mac;
}
if (TextUtils.isEmpty(device_id)) {
device_id = android.provider.Settings.Secure.getString(context.getContentResolver(),
android.provider.Settings.Secure.ANDROID_ID);
}
json.put("device_id", device_id);
Log.d("getDeviceInfo", json.toString());
return json.toString();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static String getMac(Context context) {
String mac = "";
if (context == null) {
return mac;
}
if (Build.VERSION.SDK_INT < 23) {
mac = getMacBySystemInterface(context);
} else {
mac = getMacByJavaAPI();
if (TextUtils.isEmpty(mac)) {
mac = getMacBySystemInterface(context);
}
}
return mac;
}
@TargetApi(9)
private static String getMacByJavaAPI() {
try {
Enumeration< NetworkInterface > interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface netInterface = interfaces.nextElement();
if ("wlan0".equals(netInterface.getName()) || "eth0".equals(netInterface.getName())) {
byte[] addr = netInterface.getHardwareAddress();
if (addr == null || addr.length == 0) {
return null;
}
StringBuilder buf = new StringBuilder();
for (byte b : addr) {
buf.append(String.format("%02X:", b));
}
if (buf.length() > 0) {
buf.deleteCharAt(buf.length() - 1);
}
return buf.toString().toLowerCase(Locale.getDefault());
}
}
} catch (Throwable e) {
}
return null;
}
private static String getMacBySystemInterface(Context context) {
if (context == null) {
return "";
}
try {
WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
if (checkPermission(context, Manifest.permission.ACCESS_WIFI_STATE)) {
WifiInfo info = wifi.getConnectionInfo();
return info.getMacAddress();
} else {
return "";
}
} catch (Throwable e) {
return "";
}
}
public static boolean checkPermission(Context context, String permission) {
boolean result = false;
if (context == null) {
return result;
}
if (Build.VERSION.SDK_INT >= 23) {
try {
Class clazz = Class.forName("android.content.Context");
Method method = clazz.getMethod("checkSelfPermission", String.class);
int rest = (Integer) method.invoke(context, permission);
if (rest == PackageManager.PERMISSION_GRANTED) {
result = true;
} else {
result = false;
}
} catch (Throwable e) {
result = false;
}
} else {
PackageManager pm = context.getPackageManager();
if (pm.checkPermission(permission, context.getPackageName()) == PackageManager.PERMISSION_GRANTED) {
result = true;
}
}
return result;
}
}
集成 IOS 端
这里使用pod自动集成,手动集成的方式请查看文档
在flutter_umeng_plus.podspec 中的s.dependency 'Flutter'后面添加依赖
s.dependency 'UMCCommon' , '2.1.4'
s.dependency 'UMCAnalytics', '6.1.0'
s.dependency 'UMCCommonLog'
IOS具体实现代码
FlutterUmengPlusPlugin.m写IOS 具体代码实现
#import "FlutterUmengPlusPlugin.h"
#import <UMAnalytics/MobClick.h>
#import <UMCommon/UMCommon.h>
#import <UMCommonLog/UMCommonLogHeaders.h>
//#import <Foundation/NSJSONSerialization.h>
@implementation FlutterUmengPlusPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
FlutterMethodChannel* channel = [FlutterMethodChannel
methodChannelWithName:@"flutter_umeng_plus"
binaryMessenger:[registrar messenger]];
FlutterUmengPlusPlugin* instance = [[FlutterUmengPlusPlugin alloc] init];
[registrar addMethodCallDelegate:instance channel:channel];
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([@"getPlatformVersion" isEqualToString:call.method]) {
result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
} else if ([@"init" isEqualToString:call.method]) {
[self initSetup:call result:result];
} else if ([@"logPageView" isEqualToString:call.method]) {
[self logPageView:call result:result];
} else if ([@"onPageStart" isEqualToString:call.method]) {
[self onPageStart:call result:result];
} else if ([@"onPageEnd" isEqualToString:call.method]) {
[self onPageEnd:call result:result];
} else if ([@"onEventObject" isEqualToString:call.method]) {
[self event:call result:result];
result(nil);
} else if ([@"onProfileSignIn" isEqualToString:call.method]) {
[self onProfileSignIn:call result:result];
result(nil);
} else if ([@"onProfileSignOff" isEqualToString:call.method]) {
[self onProfileSignOff:call result:result];
result(nil);
} else if ([@"getTestDeviceInfo" isEqualToString:call.method]) {
[self getTestDeviceInfo:call result:result];
result(nil);
} else if ([@"enable" isEqualToString:call.method]) {
[self setLogEnable:call result:result];
result(nil);
} else {
result(FlutterMethodNotImplemented);
}
}
- (void)initSetup:(FlutterMethodCall *)call result:(FlutterResult)result {
NSString *appKey = call.arguments[@"appKey"];
NSString *channel = call.arguments[@"channel"];
if(!channel) channel =@"default";
BOOL logEnable = [call.arguments[@"logEnable"] boolValue];
BOOL encrypt = [call.arguments[@"encrypt"] boolValue];
//BOOL reportCrash = [call.arguments[@"reportCrash"] boolValue];
/// 开发者需要显示调用此函数,日志系统才能工作
[UMCommonLogManager setUpUMCommonLogManager];
if(!logEnable)[UMConfigure setLogEnabled:YES];
if(encrypt)[UMConfigure setEncryptEnabled:encrypt];
[UMConfigure initWithAppkey:appKey channel:channel];
//[MobClick setCrashReportEnabled:reportCrash];
//[UMErrorCatch initErrorCatch];
result(nil);
}
- (void)onPageStart:(FlutterMethodCall *)call result:(FlutterResult)result {
NSString *name = call.arguments[@"pageName"];
NSLog(@"onPageStart: %@", name);
[MobClick beginLogPageView:name];
result(nil);
}
- (void)onPageEnd:(FlutterMethodCall *)call result:(FlutterResult)result {
NSString *name = call.arguments[@"pageName"];
NSLog(@"onPageEnd: %@", name);
[MobClick endLogPageView:name];
result(nil);
}
- (void)logPageView:(FlutterMethodCall *)call result:(FlutterResult)result {
NSString *name = call.arguments[@"name"];
int seconds = [call.arguments[@"seconds"] intValue];
NSLog(@"logPageView: %@", name);
NSLog(@"logPageView: %d", seconds);
[MobClick logPageView:name seconds:seconds];
result(nil);
}
- (void)event:(FlutterMethodCall *)call result:(FlutterResult)result {
NSString *name = call.arguments[@"eventId"];
NSLog(@"event name: %@", name);
NSString *temp = call.arguments[@"map"];
if(temp){
//将字符串写到缓冲区。
NSData *data =[temp dataUsingEncoding:NSUTF8StringEncoding];
NSError *error1;
//解析json数据,使用系统方法 JSONObjectWithData: options: error:
NSDictionary *map = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:&error1];
//NSDictionary *map = [call.arguments[@"map"] NSDictionary];
NSLog(@"event map: %@", map);
if(map)[MobClick event:name attributes:map];
} else {
NSString *label = call.arguments[@"label"];
NSLog(@"event label: %@", label);
if(label)[MobClick event:name label:label];
}
result(nil);
}
- (void)onProfileSignIn:(FlutterMethodCall *)call result:(FlutterResult)result {
NSString *userID = call.arguments[@"userID"];
NSLog(@"event userID: %@", userID);
[MobClick profileSignInWithPUID:userID ];
result(nil);
}
- (void)onProfileSignOff:(FlutterMethodCall *)call result:(FlutterResult)result {
[MobClick profileSignOff];
result(nil);
}
- (void)getTestDeviceInfo:(FlutterMethodCall *)call result:(FlutterResult)result {
NSString *deviceID = [UMConfigure deviceIDForIntegration];
NSLog(@"集成测试的deviceID:%@", deviceID);
result(nil);
}
- (void)setLogEnable:(FlutterMethodCall *)call result:(FlutterResult)result {
BOOL *logEnable = [call.arguments[@"logEnable"] boolValue];
NSLog(@"event name: %@", logEnable);
[UMConfigure setLogEnabled:logEnable ];
result(nil);
}
@end
flutter端实现
注意flutter端的函数里面{}里面的参数需要和Android和iOS端的call.arguments中一一对应,部分只有Android端才有的代码也需要做好处理
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
class FlutterUmengPlus {
static const MethodChannel _channel =
const MethodChannel('flutter_umeng_plus');
static Future<String> get platformVersion async {
final String version = await _channel.invokeMethod('getPlatformVersion');
return version;
}
static Future<bool> preInit({
String iOSAppKey,
String androidAppKey,
String channel, //渠道标识
}) async {
if(Platform.isIOS){
return true;
}
assert((Platform.isAndroid && androidAppKey != null) ||
(Platform.isIOS && iOSAppKey != null));
Map<String, dynamic> args = {
"appKey": Platform.isAndroid ? androidAppKey : iOSAppKey,
};
if (channel != null) {
args["channel"] = channel;
}
await _channel.invokeMethod("preInit", args);
return true;
}
///初始化友盟所有组件产品
static Future<bool> init({
String iOSAppKey,
String androidAppKey,
String channel, //渠道标识
bool logEnable, //设置是否在console输出sdk的log信息.
bool encrypt, //设置是否对日志信息进行加密, 默认NO(不加密).设置为YES, umeng SDK 会将日志信息做加密处理
bool reportCrash,
int deviceType,
String pushSecret,
}) async {
assert((Platform.isAndroid && androidAppKey != null) ||
(Platform.isIOS && iOSAppKey != null));
Map<String, dynamic> args = {
"appKey": Platform.isAndroid ? androidAppKey : iOSAppKey
};
if (channel != null) {
args["channel"] = channel;
}
if (deviceType != null) {
args["deviceType"] = deviceType;
}
if (pushSecret != null) {
args["pushSecret"] = pushSecret;
}
if (logEnable != null) args["logEnable"] = logEnable;
if (encrypt != null) args["encrypt"] = encrypt;
if (reportCrash != null) args["reportCrash"] = reportCrash;
await _channel.invokeMethod("init", args);
return true;
}
/// 打开统计SDK调试模式
///设置是否在console输出sdk的log信息.
static Future<Null> setLogEnabled(bool logEnable) async {
Map<String, dynamic> args = {"logEnable": logEnable};
await _channel.invokeMethod("setLogEnabled", args);
}
static Future<Null> setReportCrash(bool reportCrash) async {
Map<String, dynamic> args = {"reportCrash": reportCrash};
await _channel.invokeMethod("reportCrash", args);
}
static Future<Null> getTestDeviceInfo() async {
await _channel.invokeMethod("getTestDeviceInfo", null);
}
///事件埋点
///label是单个统计,map是多参数统计,同时传只会统计map
///event id长度不能超过128个字节,key不能超过128个字节,value不能超过256个字节
// ignore_key是保留字段,不能作为event id 及key的名称;
static List<String> ignore_key =["id","ts","du", "token","device_name","device_model","device_brand",
"country","city","channel","province","appkey","app_version","access","launch","pre_app_version","terminate",
"no_first_pay","is_newpayer","first_pay_at","first_pay_level","first_pay_source","first_pay_user_level","first_pay_versio"];
static Future<Null> event(String eventId, {String label, Map<String, Object> map}) async {
assert(!ignore_key.contains(eventId)&&eventId.length<=128);
Map<String, dynamic> args = {"eventId": eventId};
if (label != null) args["label"] = label;
if(map != null){
args.addAll(map);
}
await _channel.invokeMethod("onEventObject", args);
}
///统计非Activity页面-打开
static Future<Null> onPageStart(String pageName) async {
await _channel.invokeMethod("onPageStart", {"pageName": pageName});
}
///统计非Activity页面-结束
static Future<Null> onPageEnd(String pageName) async {
await _channel.invokeMethod("onPageEnd", {"pageName": pageName});
}
///统计 Activity界面-打开
static Future<Null> onResume() async {
if (Platform.isAndroid) {
await _channel.invokeMethod("onResume");
}
}
///统计 Activity界面-结束
static Future<Null> onPause() async {
if (Platform.isAndroid) {
await _channel.invokeMethod("onPause");
}
}
static Future onKillProcess() async {
if (Platform.isAndroid) {
return await _channel.invokeMethod("onKillProcess");
}
}
static Future<Null> onProfileSignIn(String ID) async {
assert(ID != null&&ID.length<=64);
await _channel.invokeMethod("onProfileSignIn", {"userID": ID});
}
static Future<Null> onProfileSignOff() async {
await _channel.invokeMethod("onProfileSignOff");
}
static Future<Null> setProcessEvent(bool enable) async {
if (Platform.isAndroid) {
await _channel.invokeMethod("setProcessEvent", {"enable": enable});
}
}
}
在插件目录的example里测试
这里就不展开了,example的测试代码覆盖率是会影响pub的评分的
3.发布插件到官方仓库
插件发布遇到的坑最多,需要额外注意.
完善文档
建议将以下文档添加到插件项目中:
-
README.md
:介绍包的文件 -
CHANGELOG.md
记录每个版本中的更改 -
LICENSE
包含软件包许可条款的文件 - 所有公共API的API文档
发布插件
运行下面的命令进行发布:
flutter packages pub publish
国内因为某些不可名状的原因,大多数情况都是会出现各种问题的。
发布前检查
我们怎么发现这些问题呢?其实还有一个检查命令可以帮助大家发现各种问题。
首先是 pubspec.yaml。对 Flutter 插件来说,pubspec 里除了插件的依赖,还包含一些元信息,读者可以根据需要,把这些补上:
name: flutter_umeng_plus # 要发布的项目名称
description: Umeng Flutter plugin. # 项目描述
version: 0.0.1 # 发布的版本
homepage: http://localhost:8080 # 项目地址 https://gitee.com/philos/flutter_umeng_plus.git
authors: [philos <894266648@qq.com>] # 项目作者
另外,发布到 Pub 上的包需要包含一个 LICENSE,关于 LICENSE 文件,最简单的方法就是在 GitHub 创建仓库的时候选中一个。
最后可以通过--dry-run命令来测试,但是命令需要加上参数--server=https://pub.dartlang.org, 因为国内的开发者一般都设置了 PUB_HOSTED_URL=https://pub.flutter-io.cn和FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
flutter packages pub publish --dry-run --server=https://pub.dartlang.org
坑点:Flutter中文网搭建文档有毒
作为国内开发者,很多人都设置了flutter中文的环境配置问题,如下图所示:
这也是官方让我们配置的Flutter临时镜像,但是上面flutter packages pub publish默认是传到官方的地址的,所以肯定是会存在问题的。
发布命令加上--server=https://pub.dartlang.org指定服务器
# dart
pub publish --server http://${your domain}
# flutter
flutter packages pub publish --server http://${your pub_server domain}
# flutter packages pub publish --server=https://pub.dartlang.org
坑点:权限认证需要访问google账号
上面的命令默认是将插件发布到flutter插件平台,须要登录google账号进行认证.
Do you want to publish flutter_umeng_plus 0.0.1 (y/N)? y
Pub needs your authorization to upload packages on your behalf.
In a web browser, go to https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force&response_type=code&client_id=818368855108-8grd2eg9tj9f38os6f1urbcvsq399
u8n.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A56921&scope=openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email
Then click "Allow access".
在我们输入flutter packages pub publish
命令之后,我们会收到一条认证链接(r如上),点击就可以跳转Google账号验证,显然这里需要某些工具辅助。
浏览器跳转到Google认证
认证成功,则会继续下面的上传步骤
Waiting for your authorization...
Authorization received, processing...
过了一会console会继续打印
It looks like accounts.google.com is having some trouble.
Pub will wait for a while before trying to connect again.
OS Error: 信号灯超时时间已到
, errno = 121, address = accounts.google.com, port = 57341
出现上面这个证明还要 番羽 墙,这时在Android Studio中系统设置的配置的HTTP Proxy可能你CHeck connection是有效的,但是还是会出现以上问题,需要在Terminal中输入以下命令,重新执行本步骤即可。
全局模式科*学上网,查看HTTP代理设置:
设置代理,需要更换网络或重新启动的时候在android studio终端中设置:
//我这里是SS,不同的工具可能不一样。
//linux mac
export https_proxy=http://127.0.0.1:1087
export http_proxy=http://127.0.0.1:1087
//windows
set https_proxy=https://127.0.0.1:1087
set http_proxy=http://127.0.0.1:1087
如果出现如下结果,就证明发布成功了!
Looks great! Are you ready to upload your package (y/n)? y
Uploading...
Successfully uploaded package.
查看结果: https://pub.dartlang.org/
当然如果是公司私有代码,则需要建立私人的包中心
搭建pub私服
官方提供了一个建议服务器程序,也是由dart编写,github地址为https://github.com/kahnsen/pub_server,用这个别人fork做了修改的版本,验证可以使用。
搭建步骤
- git clone https://github.com/dart-lang/pub_server.git
- cd pub_server 进入文件夹
- pub get 拉取dart需要的依赖库(刚刚说了,这个服务端程序也是由dart编写的)
- 如果没有pub命令,可以去官网下载dart sdk
成功后调用dart example/example.dart -d /tmp/package-db,意思是运行example/example.dart文件。/tmp/package-db是参数,以后上传的包都在这个路径下。
git clone https://github.com/dart-lang/pub_server.git
cd pub_server
pub get
dart example/example.dart -d /tmp/package-db
运行成功后,命令行出现
Listening on http://localhost:8080
To make the pub client use this repository configure your shell via:
$ export PUB_HOSTED_URL=http://localhost:8080
说明已经运行成功了,服务器网址为http://localhost:8080。地址和端口号可以在example/example.dart文件中修改。
发布私人包
pubspec.yaml添加publish_to,默认pub.dev的话是不需要的
publish_to: http://localhost:8080
然后最后再设置下PUB_HOSTED_URL环境变量
//linux mac
export PUB_HOSTED_URL=http://localhost:8080
//windows
set PUB_HOSTED_URL=http://localhost:8080
pub get
注:在Windows中没有export 这个命令,应该采用
SET命令
本地包测试
如果Google环境有问题,很可能会报错如下:
It looks like accounts.google.com is having some trouble.
Pub will wait for a while before trying to connect again.
OS Error: 信号灯超时时间已到
, errno = 121, address = accounts.google.com, port = 63891
fan不了墙怎么办???那就绕过Google认证即可。
跳过google验证
下载项目:https://github.com/ameryzhu/pub
我们仍然用Android Studio打开,打开Terminal窗口,更新依赖:
pub get
然后执行
dart --snapshot=mypub.dart.snapshot bin/pub.dart
其中mypub可以任意取一个名字,完成之后会在项目根目录下多出来一个 mypub.dart.snapshot 文件如下图所示。
复制之后放入${flutterSDK Path}/bin/cache/dart-sdk/bin/snapshots/ 目录下
用txt编辑器打开${flutterSDK Path}/bin/cache/dart-sdk/bin/pub脚本文件(注意找对正确的目录,笔者第一次没注意在snapshot 找了半天)
将倒数第34行的:pub.dart.snapshot 替换为 mypub.dart.snapshot
后面运行其他项目的时候最好改回来,上传过程参考之前的步骤即可。
再次执行flutter packages pub publish
Publishing flutter_umeng_plus 0.0.1 to http://localhost:8080:
///省略提交的文件树打印
Looks great! Are you ready to upload your package (y/n)? y
Uploading...
Successfully uploaded package.
出现上面信息则是上传成功,接下来则是引用到我们的项目里面去了。
依赖我们发布的插件
在我们需要依赖的项目的yaml文件中 dependencies:下添加:
flutter_umeng_plus:
hosted:
name: flutter_umeng_plus
url: http://localhost:8080
version: ^0.0.1
注意:yaml中层级一定要用空格对齐(helloword一级,hosted和version二级,name和url3级),否则会报错;然后执行pub get即可
如果我们仅仅只需要在本地依赖,可以直接用路径依赖,免去发布的这一步:
flutter_umeng_plus:
path: ${xxxx}/Workspace/plugin/hellowork