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

推薦閱讀更多精彩內容