首先創建一個 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
機制用于 Flutter
和 Android
的通信,主要分為三種類型:
1、MethodChannel
:主要用于傳遞方法調用,Flutter
和 Native(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