Flutter學習(9)——Flutter插件實現(Flutter調用Android原生)


原文地址: 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;
    }
}

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM