Flutter实现Plugin的一些总结,以openinstall.io的flutter的sdk为例

首先创建一个 Plugin 的 flutter 工程。我们会得到一些生成的目录和代码。
例如:

public class MyFlutterPlugin implements FlutterPlugin, MethodCallHandler{
  /// 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;

  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
    channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "my_flutter_plugin");
    channel.setMethodCallHandler(this);
  }

  @Override
  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
    if (call.method.equals("getPlatformVersion")) {
      result.success("Android " + android.os.Build.VERSION.RELEASE);
    } else {
      result.notImplemented();
    }
  }

  @Override
  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
    channel.setMethodCallHandler(null);
  }
}

这个 MyFlutterPlugin ,工程会生成一个基础的。然后我们可以根据需求对这个类进行定制开发。比如我们需要在 flutter 的页面点击一个按钮,然后能在 MyFlutterPlugin 里得到对应的监听关联,就可以里用 MethodChannel 来实现。有点类似 Android 的 webview 里 h5 和 原生之间通信。

Flutter 提供了一套 PlatformChannel 机制用于 FlutterAndroid 的通信,主要分为三种类型:
1、MethodChannel:主要用于传递方法调用,FlutterNative(Android)之间进行方法调用时可以使用,是一种双向的通信方式
2、EventChannel:主要用于用户数据流的通信,如:手机电量变化,网络连接变化等。这种方式只能 Native(Android)Flutter 发送数据,是一种单向的通信方式
3、BaseicMessageChannel:主要用于传递各种类型数据,它支持的类型有很多,如:String,半结构化信息等,是一种双向的通信方式

    @Override
    public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
        if (METHOD_INSTALL.equalsIgnoreCase(call.method)) {
            ...省略
        }
    }

以下是 openinstall.io 的实现代码:

public class OpeninstallFlutterPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegistry.NewIntentListener {

    private static final String TAG = "OpenInstallPlugin";

    @Deprecated
    private static final String METHOD_INIT_PERMISSION = "initWithPermission";
    @Deprecated
    private static final String METHOD_WAKEUP = "registerWakeup";

    private static final String METHOD_CONFIG = "config";
    private static final String METHOD_CLIPBOARD_ENABLED = "clipBoardEnabled";
    private static final String METHOD_SERIAL_ENABLED = "serialEnabled";
    private static final String METHOD_INIT = "init";
    private static final String METHOD_INSTALL_RETRY = "getInstallCanRetry";
    private static final String METHOD_INSTALL = "getInstall";
    private static final String METHOD_REGISTER = "reportRegister";
    private static final String METHOD_EFFECT_POINT = "reportEffectPoint";
    private static final String METHOD_SHARE = "reportShare";

    private static final String METHOD_OPID = "getOpid";

    private static final String METHOD_WAKEUP_NOTIFICATION = "onWakeupNotification";
    private static final String METHOD_INSTALL_NOTIFICATION = "onInstallNotification";

    private MethodChannel channel = null;
    private ActivityPluginBinding activityPluginBinding;
    private FlutterPluginBinding flutterPluginBinding;
    private Intent intentHolder = null;
    private volatile boolean initialized = false;
    private Configuration configuration = null;

    private boolean alwaysCallback = false;


    @Override
    public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
        flutterPluginBinding = binding;
        channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "openinstall_flutter_plugin");
        channel.setMethodCallHandler(this);
    }

    @Override
    public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
        activityPluginBinding = binding;

        binding.addOnNewIntentListener(this);
        wakeup(binding.getActivity().getIntent());
    }

    @Override
    public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
        activityPluginBinding = binding;
        binding.addOnNewIntentListener(this);
    }

    @Override
    public void onMethodCall(MethodCall call, @NonNull final Result result) {
        Log.d(TAG, "invoke " + call.method);
        if (METHOD_CONFIG.equalsIgnoreCase(call.method)) {
            config(call);
            result.success("OK");
        } else if (METHOD_CLIPBOARD_ENABLED.equalsIgnoreCase(call.method)) {
            Boolean enabled = call.argument("enabled");
            OpenInstall.clipBoardEnabled(enabled == null ? true : enabled);
            result.success("OK");
        } else if (METHOD_SERIAL_ENABLED.equalsIgnoreCase(call.method)) {
            Boolean enabled = call.argument("enabled");
            OpenInstall.serialEnabled(enabled == null ? true : enabled);
            result.success("OK");
        } else if (METHOD_INIT.equalsIgnoreCase(call.method)) {
            Boolean box = call.argument("alwaysCallback");
            alwaysCallback = box == null ? false : box;
            init();
            result.success("OK");
        } else if (METHOD_INIT_PERMISSION.equalsIgnoreCase(call.method)) {
            Boolean box = call.argument("alwaysCallback");
            alwaysCallback = box == null ? false : box;
            initWithPermission();
            result.success("OK");
        } else if (METHOD_WAKEUP.equalsIgnoreCase(call.method)) {
            result.success("OK");
        } else if (METHOD_INSTALL.equalsIgnoreCase(call.method)) {
            Integer seconds = call.argument("seconds");
            OpenInstall.getInstall(new AppInstallListener() {
                @Override
                public void onInstallFinish(AppData appData, Error error) {
                    Map<String, Object> data = data2Map(appData);
                    boolean shouldRetry = error!=null && error.shouldRetry();
                    data.put("shouldRetry", shouldRetry);
                    if(error != null) {
                        data.put("message", error.getErrorMsg());
                    }
                    channel.invokeMethod(METHOD_INSTALL_NOTIFICATION, data);
                }
            }, seconds == null ? 0 : seconds);
            result.success("OK");
        } else if (METHOD_INSTALL_RETRY.equalsIgnoreCase(call.method)) {
            Integer seconds = call.argument("seconds");
            OpenInstall.getInstallCanRetry(new AppInstallRetryAdapter() {
                @Override
                public void onInstall(AppData appData, boolean shouldRetry) {
                    Map<String, Object> data = data2Map(appData);
                    data.put("retry", String.valueOf(shouldRetry)); // 2.4.0 之前的版本返回
                    data.put("shouldRetry", shouldRetry);  // 以后保存统一
                    channel.invokeMethod(METHOD_INSTALL_NOTIFICATION, data);
                }
            }, seconds == null ? 0 : seconds);
            result.success("OK");
        } else if (METHOD_REGISTER.equalsIgnoreCase(call.method)) {
            OpenInstall.reportRegister();
            result.success("OK");
        } else if (METHOD_EFFECT_POINT.equalsIgnoreCase(call.method)) {
            String pointId = call.argument("pointId");
            Integer pointValue = call.argument("pointValue");
            if(TextUtils.isEmpty(pointId) || pointValue == null){
                Log.w(TAG, "pointId is empty or pointValue is null");
//                result.error("ERROR", "pointId is empty or pointValue is null", null);
            }else{
                Map<String, String> extraMap = call.argument("extras");
                OpenInstall.reportEffectPoint(pointId, pointValue, extraMap);
            }
            result.success("OK");
        } else if (METHOD_SHARE.equalsIgnoreCase(call.method)) {
            String shareCode = call.argument("shareCode");
            String sharePlatform = call.argument("platform");
            final Map<String, Object> data = new HashMap<>();
            if(TextUtils.isEmpty(shareCode) || TextUtils.isEmpty(sharePlatform)){
                data.put("message",  "shareCode or platform is empty");
                data.put("shouldRetry", false);
                result.success(data);
            }else {
                OpenInstall.reportShare(shareCode, sharePlatform, new ResultCallback<Void>() {
                    @Override
                    public void onResult(@Nullable Void v, @Nullable Error error) {
                        boolean shouldRetry = error!=null && error.shouldRetry();
                        data.put("shouldRetry", shouldRetry);
                        if(error != null) {
                            data.put("message", error.getErrorMsg());
                        }
                        result.success(data);
                    }
                });
            }
        } else if (METHOD_OPID.equalsIgnoreCase(call.method)) {
            String opid = OpenInstall.getOpid();
            result.success(opid);
        } else {
            result.notImplemented();
        }
    }

    private void config(MethodCall call) {

        Configuration.Builder builder = new Configuration.Builder();

        if (call.hasArgument("androidId")) {
            String androidId = call.argument("androidId");
            builder.androidId(androidId);
        }
        if (call.hasArgument("serialNumber")) {
            String serialNumber = call.argument("serialNumber");
            builder.serialNumber(serialNumber);
        }
        if (call.hasArgument("adEnabled")) {
            Boolean adEnabled = call.argument("adEnabled");
            builder.adEnabled(checkBoolean(adEnabled));
        }
        if (call.hasArgument("oaid")) {
            String oaid = call.argument("oaid");
            builder.oaid(oaid);
        }
        if (call.hasArgument("gaid")) {
            String gaid = call.argument("gaid");
            builder.gaid(gaid);
        }
        if(call.hasArgument("imeiDisabled")){
            Boolean imeiDisabled = call.argument("imeiDisabled");
            if (checkBoolean(imeiDisabled)) {
                builder.imeiDisabled();
            }
        }
        if (call.hasArgument("imei")) {
            String imei = call.argument("imei");
            builder.imei(imei);
        }
        if(call.hasArgument("macDisabled")){
            Boolean macDisabled = call.argument("macDisabled");
            if (checkBoolean(macDisabled)) {
                builder.macDisabled();
            }
        }
        if (call.hasArgument("mac")) {
            String macAddress = call.argument("mac");
            builder.macAddress(macAddress);
        }

        configuration = builder.build();
//        Log.d(TAG, String.format("Configuration: adEnabled=%s, oaid=%s, gaid=%s, macDisabled=%s, imeiDisabled=%s, "
//                        + "androidId=%s, serialNumber=%s, imei=%s, mac=%s",
//                configuration.isAdEnabled(), configuration.getOaid(), configuration.getGaid(),
//                configuration.isMacDisabled(), configuration.isImeiDisabled(),
//                configuration.getAndroidId(), configuration.getSerialNumber(),
//                configuration.getImei(), configuration.getMacAddress()));

    }

    private boolean checkBoolean(Boolean bool) {
        if (bool == null) return false;
        return bool;
    }

    private void init() {
        Context context = flutterPluginBinding.getApplicationContext();
        if (context != null) {
            OpenInstall.init(context, configuration);
            initialized = true;
            if (intentHolder != null) {
                wakeup(intentHolder);
                intentHolder = null;
            }
        } else {
            Log.d(TAG, "Context is null, can't init");
        }
    }

    @Deprecated
    private void initWithPermission() {
        Activity activity = activityPluginBinding.getActivity();
        if (activity == null) {
            Log.d(TAG, "Activity is null, can't initWithPermission, replace with init");
            init();
        } else {
            activityPluginBinding.addRequestPermissionsResultListener(permissionsResultListener);
            OpenInstall.initWithPermission(activity, configuration, new Runnable() {
                @Override
                public void run() {
                    activityPluginBinding.removeRequestPermissionsResultListener(permissionsResultListener);
                    initialized = true;
                    if (intentHolder != null) {
                        wakeup(intentHolder);
                        intentHolder = null;
                    }
                }
            });
        }
    }

    @Override
    public boolean onNewIntent(Intent intent) {
        wakeup(intent);
        return false;
    }


    private void wakeup(Intent intent) {
        if (initialized) {
            Log.d(TAG, "getWakeUp : alwaysCallback=" + alwaysCallback);
            if (alwaysCallback) {
                OpenInstall.getWakeUpAlwaysCallback(intent, new AppWakeUpListener() {
                    @Override
                    public void onWakeUpFinish(AppData appData, Error error) {
                        if (error != null) { // 可忽略,仅调试使用
                            Log.d(TAG, "getWakeUpAlwaysCallback : " + error.getErrorMsg());
                        }
                        channel.invokeMethod(METHOD_WAKEUP_NOTIFICATION, data2Map(appData));
                    }
                });
            } else {
                OpenInstall.getWakeUp(intent, new AppWakeUpAdapter() {
                    @Override
                    public void onWakeUp(AppData appData) {
                        channel.invokeMethod(METHOD_WAKEUP_NOTIFICATION, data2Map(appData));
                    }
                });
            }
        } else {
            intentHolder = intent;
        }
    }

    @Deprecated
    private final PluginRegistry.RequestPermissionsResultListener permissionsResultListener =
            new PluginRegistry.RequestPermissionsResultListener() {
                @Override
                public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
                    OpenInstall.onRequestPermissionsResult(requestCode, permissions, grantResults);
                    return false;
                }
            };

    private static Map<String, Object> data2Map(AppData data) {
        Map<String, Object> result = new HashMap<>();
        if (data != null) {
            result.put("channelCode", data.getChannel());
            result.put("bindData", data.getData());
        }
        return result;
    }

    @Override
    public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {

    }

    @Override
    public void onDetachedFromActivityForConfigChanges() {

    }

    @Override
    public void onDetachedFromActivity() {

    }
}

在 Flutter 里需要写一个 openinstall_flutter_plugin.dart,去处理和 Android 原生代码的交互

import 'dart:async';
import 'dart:io';

import 'package:flutter/services.dart';

typedef Future EventHandler(Map<String, Object> data);

class OpeninstallFlutterPlugin {
  // 单例
  static final OpeninstallFlutterPlugin _instance = new OpeninstallFlutterPlugin._internal();

  factory OpeninstallFlutterPlugin() => _instance;

  OpeninstallFlutterPlugin._internal();

  Future defaultHandler() async {}

  late EventHandler _wakeupHandler;
  late EventHandler _installHandler;

  static const MethodChannel _channel = const MethodChannel('openinstall_flutter_plugin');

  // 已废弃
  // 旧版本使用,保留一段时间,防止 npm 自动升级使用最新版本插件出现问题
  void config(bool adEnabled, String? oaid, String? gaid) {
    print("config(bool adEnabled, String? oaid, String? gaid) 后续版本将移除,请使用configAndroid(Map options)");
    if (Platform.isAndroid) {
      var args = new Map();
      args["adEnabled"] = adEnabled;
      args["oaid"] = oaid;
      args['gaid'] = gaid;
      _channel.invokeMethod('config', args);
    } else {
      // 仅使用于 Android 平台
    }
  }

  // 广告平台配置,请参考文档
  void configAndroid(Map options) {
    if (Platform.isAndroid) {
      _channel.invokeMethod('config', options);
    } else {
      // 仅使用于 Android 平台
    }
  }

  // 关闭剪切板读取
  void clipBoardEnabled(bool enabled){
    if (Platform.isAndroid) {
      var args = new Map();
      args["enabled"] = enabled;
      _channel.invokeMethod('clipBoardEnabled', args);
    } else {
      // 仅使用于 Android 平台
    }
  }

  // 已废弃
  // 关闭SerialNumber读取
  void serialEnabled(bool enabled){
    print("serialEnabled(bool enabled) 后续版本将移除,请使用configAndroid(Map options)");
    if (Platform.isAndroid) {
      var args = new Map();
      args["enabled"] = enabled;
      _channel.invokeMethod('serialEnabled', args);
    } else {
      // 仅使用于 Android 平台
    }
  }

    
    //设置参数并初始化
    //options可设置参数:
    //AdPlatformEnable:必要,是否开启广告平台统计功能
    //ASAEnable:必要,是否开启ASA功能
    //ASADebug:可选,使用ASA功能时是否开启debug模式,正式环境中请关闭
    //idfaStr:可选,用户可以自行传入idfa字符串,不传则插件内部会获取,通过其它插件获取的idfa字符串一般格式为xxxx-xxxx-xxxx-xxxx
  void configIos(Map options) {
    if (Platform.isAndroid) {
      //仅使用于 iOS 平台
    } else {
      _channel.invokeMethod("config", options);
    }
  }

  // wakeupHandler 拉起回调.
  // alwaysCallback 是否总是有回调。当值为true时,只要触发了拉起方法调用,就会有回调
  // permission 初始化时是否申请 READ_PHONE_STATE 权限,已废弃。请用户自行进行权限申请
  void init(EventHandler wakeupHandler, {bool alwaysCallback = false, bool permission = false}) {
    _wakeupHandler = wakeupHandler;
    _channel.setMethodCallHandler(_handleMethod);
    _channel.invokeMethod("registerWakeup");
    if (Platform.isAndroid) {
      var args = new Map();
      args["alwaysCallback"] = alwaysCallback;
      if (permission) {
        print("initWithPermission 后续版本将移除,请自行进行权限申请");
        _channel.invokeMethod("initWithPermission", args);
      } else {
        _channel.invokeMethod("init", args);
      }
    } else {
      print("插件版本>=2.3.1后,由于整合了广告和ASA系统,iOS平台将通过用户手动调用init方法初始化SDK,需要广告平台或者ASA统计服务的请在init方法前调用configIos方法配置参数");
    }
  }


  // SDK内部将会一直保存安装数据,每次调用install方法都会返回值。
  // 如果调用install获取到数据并处理了自己的业务,后续不想再被触发,那么可以自己在业务调用成功时,设置一个标识,不再调用install方法
  void install(EventHandler installHandler, [int seconds = 10]) {
    var args = new Map();
    args["seconds"] = seconds;
    this._installHandler = installHandler;
    _channel.invokeMethod('getInstall', args);
  }

  // 只有在用户进入应用后在较短时间内需要返回安装参数,但是又不想影响参数获取精度时使用。
  // 在shouldRetry为true的情况下,后续再次通过install依然可以获取安装数据
  // 通常情况下,请使用 install 方法获取安装参数
  void getInstallCanRetry(EventHandler installHandler, [int seconds = 3]) {
    if (Platform.isAndroid) {
      var args = new Map();
      args["seconds"] = seconds;
      this._installHandler = installHandler;
      _channel.invokeMethod('getInstallCanRetry', args);
    } else {
      // 仅使用于 Android 平台
    }
  }

  void reportRegister() {
    _channel.invokeMethod('reportRegister');
  }

  void reportEffectPoint(String pointId, int pointValue, [Map<String, String>? extraMap]) {
    var args = new Map();
    args["pointId"] = pointId;
    args["pointValue"] = pointValue;
    if(extraMap != null){
      args["extras"] = extraMap;
    }
    _channel.invokeMethod('reportEffectPoint', args);
  }

  Future<Map<Object?, Object?>> reportShare(String shareCode, String platform) async {
    var args = new Map();
    args["shareCode"] = shareCode;
    args["platform"] = platform;
    Map<Object?, Object?> data = await _channel.invokeMethod('reportShare', args);
    return data;
  }

  Future<String?> getOpid() async {
    print("getOpid 当初始化未完成时,将返回空,请在业务需要时再获取,并且使用时做空判断");
    String? opid = await _channel.invokeMethod('getOpid');
    return opid;
  }

  Future _handleMethod(MethodCall call) async {
    print(call.method);
    switch (call.method) {
      case "onWakeupNotification":
        return _wakeupHandler(call.arguments.cast<String, Object>());
      case "onInstallNotification":
        return _installHandler(call.arguments.cast<String, Object>());
      default:
        throw new UnsupportedError("Unrecognized Event");
    }
  }
}

本质上通过 Flutter 实现 Plugin 的这个方案,里面还是用到了 Android 的 OpenInstall_v2.8.1.jar 的依赖库,然后通过 Plugin 的方式进行包装。
本文中的源码,可以在这里找到:https://github.com/OpenInstall/openinstall-flutter-plugin

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

推荐阅读更多精彩内容