原文地址: Flutter學習(9)——Flutter外掛實現(Flutter呼叫Android原生) | Stars-One的雜貨小窩
最近需要給一個Flutter專案加個apk完整性檢測,需要去拿到當前安裝apk的md5數值,由於Flutter中無法實現,需要呼叫原生Android程式碼才能實現,於是花了些時間研究了下外掛的實現,特此記錄
步驟說明
1.開啟android資料夾
flutter中有個ios和android的資料夾,分別對應的Android和Ios的原生程式碼
我們想要實現FLutter呼叫原生程式碼,在裡面寫原生程式碼即可
在android資料夾中,新建有個類,Android可以選擇Java或者是Kotlin程式碼編寫即可
android目錄結構其實就是常見的Android專案目錄
然後使用Android Studio開啟,右鍵選單,選擇flutter
-> Open Android module in Android Studio
之後可以看到已經像Android開發一樣開啟了一個專案(當然,這裡你也可以自己使用Android Studio去選擇那個android資料夾,將其當做專案開啟即可)
2.新建Activity
此Activity需要繼承FlutterActivity
,並重寫configureFlutterEngine
方法,在此方法中進行外掛的初始化
public class MainActivity extends FlutterActivity {
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
//外掛例項的註冊...
//這個是必寫,別刪除!!
GeneratedPluginRegistrant.registerWith(flutterEngine);
}
}
那麼這裡需要外掛的例項,外掛的例項怎麼來呢?其實就是自己寫個類,然後實現Flutter提供的FlutterPlugin
介面
3.原生程式碼編寫
新建一個類,實現FlutterPlugin
介面,建立一個MethodChannel
物件,利用此物件的setMethodCallHandler
方法設定方法處理回撥,裡面通過判斷方法名來調我們原生寫的方法
public class MyTestPlugin implements FlutterPlugin {
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
//可以利用binding物件獲取到Android中需要的Context物件
//Context applicationContext = binding.getApplicationContext();
//設定channel名稱,之後flutter中也要一樣
MethodChannel channel = new MethodChannel(binding.getFlutterEngine().getDartExecutor(), "test-plugin");
//把當前的MethodCallHandler設定
channel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
String method = call.method;
if (method.equals("getText")) {
//呼叫原生的方法,這裡為了方便,我就把方法寫在當前類了
String str = getText();
//將結果返回給flutter
result.success(str);
//這裡也有error的方法,可以看情況使用
//result.error("code", "message", "detail");
} else {
//Flutter傳過來id方法名沒有找到,就調此方法
result.notImplemented();
}
}
});
}
private String getText() {
return "hello world";
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
}
}
如果你想要一個Application的context上下文物件,可以在onAttachedToEngine()
方法中使用binding的getApplicationContext()
方法獲取,如下程式碼
Context applicationContext = binding.getApplicationContext();
如果是想要獲取當前Activity的context物件,可以讓當前類實現ActivityAware介面,不過略顯繁瑣,一般用Application的context物件應該可以滿足大部分要求了,看情況選擇吧
private Context context;
@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
context = binding.getActivity();
}
@Override
public void onDetachedFromActivityForConfigChanges() {
}
@Override
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
}
@Override
public void onDetachedFromActivity() {
context = null;
}
4.Activity中註冊外掛
之前在第二步中的Activity中,補上註冊的程式碼
public class MainActivity extends FlutterActivity {
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
//外掛例項的註冊...
flutterEngine.getPlugins().add(new MyTestPlugin());
GeneratedPluginRegistrant.registerWith(flutterEngine);
}
}
5.flutter中外掛初始化和封裝
在flutter中建立一個檔案,檔名和class名任意,只是用來宣告和初始化上述的Java類
class Md5Plugin{
//注意,這裡的名稱需要和Android原生中定義的一樣
static const MethodChannel _channel = MethodChannel("apk_md5");
static Future<String> getMd5() async{
//傳遞一個方法名,即呼叫Android的原生方法
return await _channel.invokeMethod("getMd5");
}
}
還記得之前寫的方法名的判斷嗎?這裡就是傳一個方法名,之後就會觸發回撥,之後即可得到返回結果
PS:注意,調Android原生的方法都是非同步操作!
6.flutter頁面中使用外掛
之後在對應的page檔案對應程式碼處中呼叫即可
Md5Plugin.getMd5().then(value=>{
//相關操作
});
如果想使用同步程式碼,可以這樣寫
var result = Md5Plugin.getMd5()
PS:測試的時候注意,如果是改了原生層程式碼(Java或Kotlin),最好將專案重新執行,不要使用Flutter的熱過載功能(除非你只動了flutter的程式碼)
傳參補充
上述的例子中,並沒有涉及到傳參,這裡再補充講解下我自己的研究使用
這裡只講Flutter如何給Android原生傳參
FLutter中呼叫方法(即上述的第五步操作):
class Md5Plugin{
//注意,這裡的名稱需要和Android原生中定義的一樣
static const MethodChannel _channel = MethodChannel("apk_md5");
static Future<String> getMd5() async{
//傳字串給Android
var param = "hello";
//傳遞一個方法名,即呼叫Android的原生方法
//注意這裡的第二個引數
return await _channel.invokeMethod("getMd5",param);
}
}
Android中的接收(上述的第三步):
在判斷方法名之後,即可通過對應的方法獲取資料(需要型別轉換)
public class MyTestPlugin implements FlutterPlugin {
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
...
//把當前的MethodCallHandler設定
channel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
String method = call.method;
if (method.equals("getText")) {
//注意這裡的獲取資料(強轉)
String packageName = (String)call.arguments;
省略...
} else {
//Flutter傳過來id方法名沒有找到,就調此方法
result.notImplemented();
}
}
});
}
...
}
上述的程式碼只是傳單個資料,如果是要穿多個資料要怎麼辦呢?
由於invokeMethod()
方法裡只支援傳單個資料,所以我們需要傳map或是json格式的資料給到Android原生
Flutter傳送資料:
var param = {"myKey":"hello"}
//傳遞一個方法名,即呼叫Android的原生方法
//注意這裡的第二個引數
return await _channel.invokeMethod("getMd5",param);
Android接收資料:
String packageName = call.argument("myKey");
這裡有點要注意,call中有個arguments屬性和arguments()方法,如下圖
flutter中傳過來的資料是map或json的,就得用arguments()來獲取引數據;否則就是使用arguments屬性
當然,如果傳過來的資料是map或json型別,call提供了一個方便快捷的方法,我們可以直接使用argument(key)
來直接獲取key對應的數值(注意這裡也需要型別強轉,注意型別需要對應)
最後這裡給出Flutter與Java的對應的型別表:
Dart | Android |
---|---|
null | null |
bool | java.lang.Boolean |
int | java.lang.Integer |
int, if 32 bits not enough | java.lang.Long |
double | java.lang.Double |
String | java.lang.String |
Uint8List | byte[] |
Int32List | int[] |
Int64List | long[] |
Float64List | double[] |
List | java.util.ArrayList |
Map | java.util.HashMap |
程式碼參考
點選檢視原始碼(ApkMd5CheckPlugin類)
package com.example.taiji_lianjiang.checkplugin;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.text.TextUtils;
import com.example.taiji_lianjiang.BuildConfig;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class ApkMd5CheckPlugin implements MethodChannel.MethodCallHandler, FlutterPlugin, ActivityAware {
public static ApkMd5CheckPlugin getInstance() {
return new ApkMd5CheckPlugin();
}
private MethodChannel channel;
private Activity context;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
//設定channel名稱,之後flutter中也要一樣
channel = new MethodChannel(binding.getFlutterEngine().getDartExecutor(), "apk_md5");
//把當前的MethodCallHandler設定
channel.setMethodCallHandler(this);
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
String method = call.method;
if (method.equals("getMd5")) {
String md5 = getMd5(context);
if (!TextUtils.isEmpty(md5)) {
result.success(md5);
} else {
result.error("101", "獲取md5失敗", "");
}
} else {
result.notImplemented();
}
}
//獲取你重新自身的安裝包位置 一般在/data/app/包名/xxx.apk
private String getApkPath(Context context) {
try {
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(BuildConfig.APPLICATION_ID, PackageManager.GET_META_DATA);
ApplicationInfo applicationInfo = packageInfo.applicationInfo;
return applicationInfo.publicSourceDir; // 獲取當前apk包的絕對路徑
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return "";
}
//獲取hash值 整個apk的 注意 這裡程式碼不太嚴謹 demo隨便敲的 跑通就行了
private String getMd5(Context context) {
String apkPath = getApkPath(context);
StringBuffer sb = new StringBuffer("");
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(readFileToByteArray(new File(apkPath)));
byte b[] = md.digest();
int d;
for (int i = 0; i < b.length; i++) {
d = b[i];
if (d < 0) {
d = b[i] & 0xff;
// 與上一行效果等同
// i += 256;
}
if (d < 16)
sb.append("0");
sb.append(Integer.toHexString(d));
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString().toUpperCase();
}
private byte[] readFileToByteArray(File file) throws IOException {
InputStream in = null;
try {
in = new FileInputStream(file);
return toByteArray(in, file.length());
} finally {
in.close();
}
}
private byte[] toByteArray(InputStream input, long size) throws IOException {
if (size > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Size cannot be greater than Integer max value: " + size);
}
return toByteArray(input, (int) size);
}
private byte[] toByteArray(InputStream input, int size) throws IOException {
if (size < 0) {
throw new IllegalArgumentException("Size must be equal or greater than zero: " + size);
}
if (size == 0) {
return new byte[0];
}
byte[] data = new byte[size];
int offset = 0;
int readed;
while (offset < size && (readed = input.read(data, offset, size - offset)) != -1) {
offset += readed;
}
if (offset != size) {
throw new IOException("Unexpected readed size. current: " + offset + ", excepted: " + size);
}
return data;
}
@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
context = binding.getActivity();
}
@Override
public void onDetachedFromActivityForConfigChanges() {
}
@Override
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
}
@Override
public void onDetachedFromActivity() {
context = null;
}
}