• Flutter学习(9)——Flutter插件实现(Flutter调用Android原生) Stars


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

    参考


    提问之前,请先看提问须知 点击右侧图标发起提问 联系我 或者加入QQ群一起学习 Stars-One安卓学习交流群 TornadoFx学习交流群:1071184701
  • 相关阅读:
    docker 容器启动初始化,centos镜像启动并执行
    odoo 分布式session 失效解决方案
    文件分布式存储 minio docker
    odoo reports 报表打印pdf初探
    odoo 分布式快速更新
    linux Warning: Stopping docker.service, but it can still be activated by:
    linux 查看80端口的连接数
    css flex 涨姿势了
    odoo 后台打印日志修改
    iOS 导航栏消失
  • 原文地址:https://www.cnblogs.com/stars-one/p/15730821.html
Copyright © 2020-2023  润新知