Original address: Flutter learning (9) -- implementation of flutter plug-in (flutter calls Android native) | small nest of stars one
Recently, we need to add apk integrity detection to a Flutter project. We need to get the md5 value of the currently installed apk. Because it cannot be implemented in Flutter, we need to call the native Android code to implement it. Therefore, we spent some time studying the implementation of the next plug-in, which is hereby recorded
Step description
1. Open the android folder
There are IOS and Android folders in fluent, which correspond to the native code of Android and IOS respectively
We want to implement fluent to call the native code and write the native code in it
In the Android folder, a new class is created. Android can choose Java or Kotlin coding
Android directory structure is actually a common Android project directory
Then use Android Studio to open, right-click the menu, and select fluent - > Open Android module in Android Studio
After that, you can see that a project has been opened like Android Development (of course, you can use Android Studio to select the Android folder and open it as a project)
2. Create a new Activity
This Activity needs to inherit the fluteractivity and override the configureFlutterEngine method to initialize the plug-in
public class MainActivity extends FlutterActivity { @Override public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { //Registration of plug-in instances //This is a must, don't delete!! GeneratedPluginRegistrant.registerWith(flutterEngine); } }
So you need plug-in instances here. How do you get plug-in instances? In fact, you write your own class and implement the FlutterPlugin interface provided by Flutter
3. Native code writing
Create a new class, implement the FlutterPlugin interface, create a MethodChannel object, use the setMethodCallHandler method of this object to set the method processing callback, and call our native method by judging the method name
public class MyTestPlugin implements FlutterPlugin { @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { //You can use the binding object to obtain the Context object required in Android //Context applicationContext = binding.getApplicationContext(); //Set the channel name, and then it will be the same in the shuttle MethodChannel channel = new MethodChannel(binding.getFlutterEngine().getDartExecutor(), "test-plugin"); //Set the current MethodCallHandler to channel.setMethodCallHandler(new MethodChannel.MethodCallHandler() { @Override public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { String method = call.method; if (method.equals("getText")) { //Call the native method. For convenience, I'll write the method in the current class String str = getText(); //Return the result to fluent result.success(str); //There is also the error method, which can be used according to the situation //result.error("code", "message", "detail"); } else { //If the id method name passed by Flutter is not found, call this method result.notImplemented(); } } }); } private String getText() { return "hello world"; } @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { } }
If you want an Application context object, you can use the binding getApplicationContext() method in the onAttachedToEngine() method, as shown in the following code
Context applicationContext = binding.getApplicationContext();
If you want to obtain the context object of the current Activity, you can let the current class implement the ActivityAware interface, but it is a little cumbersome. Generally, the context object of Application should meet most requirements. Choose according to the situation
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. Register plug-ins in activity
Previously, fill in the registered code in the Activity in step 2
public class MainActivity extends FlutterActivity { @Override public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { //Registration of plug-in instances flutterEngine.getPlugins().add(new MyTestPlugin()); GeneratedPluginRegistrant.registerWith(flutterEngine); } }
5. Initialization and encapsulation of plug-ins in fluent
Create a file in fluent with arbitrary file name and class name, which is only used to declare and initialize the above Java classes
class Md5Plugin{ //Note that the name here needs to be the same as that defined in Android native static const MethodChannel _channel = MethodChannel("apk_md5"); static Future<String> getMd5() async{ //Pass a method name, that is, call the native method of Android return await _channel.invokeMethod("getMd5"); } }
Remember the judgment of method name written before? Here is to pass a method name, and then the callback will be triggered, and then the return result can be obtained
PS: note that the native methods of calling Android are asynchronous operations!
6. Using plug-ins in the shutter page
Then you can call it in the corresponding code of the corresponding page file.
Md5Plugin.getMd5().then(value=>{ //Related operations });
If you want to use synchronization code, you can write it like this
var result = Md5Plugin.getMd5()
PS: when testing, note that if you change the native layer code (Java or Kotlin), you'd better run the project again and don't use the hot overload function of flutter (unless you only move the flutter code)
Pass parameter supplement
In the above example, there is no reference to communication. Here I will add my own research use
Here we only talk about how Flutter passes parameters to Android natively
The calling method in FLutter (that is, the fifth step operation above):
class Md5Plugin{ //Note that the name here needs to be the same as that defined in Android native static const MethodChannel _channel = MethodChannel("apk_md5"); static Future<String> getMd5() async{ //Pass string to Android var param = "hello"; //Pass a method name, that is, call the native method of Android //Notice the second parameter here return await _channel.invokeMethod("getMd5",param); } }
Receiving in Android (step 3 above):
After determining the method name, you can obtain data through the corresponding method (type conversion is required)
public class MyTestPlugin implements FlutterPlugin { @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { ... //Set the current MethodCallHandler to channel.setMethodCallHandler(new MethodChannel.MethodCallHandler() { @Override public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { String method = call.method; if (method.equals("getText")) { //Note the acquired data (forced rotation) here String packageName = (String)call.arguments; ellipsis... } else { //If the id method name passed by Flutter is not found, call this method result.notImplemented(); } } }); } ... }
The above code is just a data flyer. What if you want to wear multiple data?
Because the invokeMethod() method only supports leaflet data, we need to transfer map or json format data to Android native
Data sent by Flutter:
var param = {"myKey":"hello"} //Pass a method name, that is, call the native method of Android //Notice the second parameter here return await _channel.invokeMethod("getMd5",param);
Android receive data:
String packageName = call.argument("myKey");
Note that there is an arguments attribute and an arguments() method in call, as shown in the following figure
If the data passed from the fluent is map or json, you have to use arguments() to obtain the parameter data; Otherwise, the arguments attribute is used
Of course, if the passed data is of map or json type, call provides a convenient and fast method. We can directly use argument(key) to directly obtain the value corresponding to the key (note that type coercion is also required here, and note that type correspondence is required)
Finally, the corresponding type table of fluent and Java is given here:
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 |
Code reference
Click to view the source code (ApkMd5CheckPlugin class)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) { //Set the channel name, and then it will be the same in the shuttle channel = new MethodChannel(binding.getFlutterEngine().getDartExecutor(), "apk_md5"); //Set the current MethodCallHandler to 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", "obtain md5 fail", ""); } } else { result.notImplemented(); } } //Get your own installation package location, usually in / data/app / package name / 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; // Gets the absolute path of the current apk package } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } return ""; } //Get the hash value of the whole apk. Note that the code here is not too rigorous. Just knock the demo to run through 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; // Same as the previous line // 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; } }