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 // 項目配置文件
image.png
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