Flutter以友盟umeng为例开发插件全过程(自建私人仓库)

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创建项目,根据提示一步一步来就行了,截图如下:

image.png

如果对于kotlin或者swift不熟悉,或者第三方包的官方代码没有kotlin和swift实现,那么不选择底下kotlin和swift,官方是java和c


image.png

生成的项目目录主要包含以下内容:

  • android // Android 相关原生代码目录
  • ios // ios 相关原生代码目录
  • lib // Dart 代码目录,主要是创建“MethodChannel”,然后接收并处理来自原生平台发来的消息
  • example // 一个完整的调用了我们正在开发的插件的 Flutter App
  • pubspec.yaml // 项目配置文件


    image.png

Flutter 如何调用原生代码

image.png

上方来自官方的架构图已经足够清晰了,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旧的加载插件的方式,通过反射进行加载.
  • onAttachedToEngineonDetachedFromEngineFlutterPlugin 的接口方法,是flutter新的加载插件的方式.
  • onAttachedToActivityonDetachedFromActivityActivityAware 的接口方法,主要是用于获取当前flutter页面所处的Activity.
  • onMethodCallMethodCallHandler 的接口方法,主要用于接收Flutter端对原生方法调用的实现.

先打开友盟的官方文档https://developer.umeng.com/docs/119267/detail/118584
我们使用maven的方式集成,手动添加SDK地方式这里不展开,这种更新会比较麻烦。

maven自动集成

在工程build.gradle配置脚本中buildscript和allprojects段中添加【友盟+】SDK新maven仓库地址。

image.png

AndroidMainF添加权限

权限一般不会写在我们Libary的Android端里面,实在要写也没问题,但是管理就会比较麻烦


image.png

在example下面的Android端


image.png

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'
image.png

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的评分的


image.png

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账号验证,显然这里需要某些工具辅助。

image.png

浏览器跳转到Google认证

image.png
image.png

认证成功,则会继续下面的上传步骤

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代理设置:

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做了修改的版本,验证可以使用。

搭建步骤

  1. git clone https://github.com/dart-lang/pub_server.git
  2. cd pub_server 进入文件夹
  3. 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 文件如下图所示。

image.png

复制之后放入${flutterSDK Path}/bin/cache/dart-sdk/bin/snapshots/ 目录下


image.png

用txt编辑器打开${flutterSDK Path}/bin/cache/dart-sdk/bin/pub脚本文件(注意找对正确的目录,笔者第一次没注意在snapshot 找了半天)

image.png

将倒数第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

相关链接

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容