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閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容