Flutter 与原生之间的交互
文件的方式
flutter
和Native
都具备对系统文件进行读写。这样就提供了一种思路。用于Flutter
与Native
之间进行交互。
// 指定文件名称
public static final String FILE_NAME = "FlutterSharedPreferences";
public static final String KEY_NAME = "flutter.proxy";
// 存放文件内容
SpUtils.getInstance(FILE_NAME).put(KEY_NAME, result);
在flutter
应用程序中就可以获取到这个文件内容
void setProxyConfig() {
String proxyConfig = SpUtils.getString("proxy");
if (proxyConfig.isNotEmpty) {
ProxyEntity config = ProxyEntity.fromJson(json.decode(proxyConfig));
if (config.isOpen) {
ConstantConfig.localProxy = 'PROXY ${config.proxyUrl}';
}
}
}
路由的方式
由于Flutter
的引擎运行在Activity
或则Fragment
中。这样当我们渲染Flutter
的引擎前,就可以通过intent
的方式讲所需要的参数传入到Flutter
的Router
参数中,这样的话Flutter
在渲染之前可以通过解析Router
参数将所需要的参数解析出来。
// 原生数据获取
override fun getInitialRoute(): String {
var path = intent.getStringExtra(PATH)
if (path == null) {
path = DEFAULT_PAGE
}
val params = dispatchParam(intent.extras?.keySet())
var result = if (params["data"] != null) {
params["data"]!!.wrapParam()
} else {
params.wrapParam()
}
return "${path}?$result"
}
在flutter
程序获取到这些参数
/// flutter 解析数据
var baseParam = RouterConfig.getRouterParam(path);
if (baseParam != null) {
setServerUp(baseParam.serverUrl);
setProxyConfig();
setLanguageUp(baseParam.language);
UserConfig.setUserCode(baseParam.userCode, baseParam.token);
setOtherUp(baseParam);
}
插件的方式
在介绍插件的方式之前有必要先说下
Flutter
工程结构
Flutter 工程结构
目前flutter
为我们提供如下项目模版。
- Flutter Aplication
- Flutter Plugin
- FLutter package
- Flutter Module
Flutter Aplication
当你需要一个纯Flutter
开发的项目的时候,你就可以考虑使用这套模版来构建你的项目。你可以尝试着创建这样类型的项目,会发现其中的项目的目录结构如下。
注意,这里的android
文件夹和ios
文件,前面并没有带有.
这个和接下来要解释的Flutter Module
有所区别。
Flutter Module
当你需要把你编写的Flutter
代码,以AAR
的方式内嵌到原生的时候,可以尝试使用这样的方式,来创建自己的Flutter
项目。我们尝试的创建一个Flutter Module
项目查看下。
从上图,我们可以发现Flutter Module
的项目和Flutter Application
的项目存放Native
的代码文件名称都一样,但是Flutter Module
会把存放Native
的代码设置为隐藏文件,也就是在文件名称前面加.
。
我们在编写Flutter Module
的时候,经常使用到Flutter Clean
命令,会将.android
和.ios
进行删除。也就意味着,你在Flutter Module
编写的Native
的代码都会被删除。具体Flutter Clean
所执行的逻辑如下。
@override
Future<FlutterCommandResult> runCommand() async {
// Clean Xcode to remove intermediate DerivedData artifacts.
// Do this before removing ephemeral directory, which would delete the xcworkspace.
final FlutterProject flutterProject = FlutterProject.current();
if (globals.xcode.isInstalledAndMeetsVersionCheck) {
await _cleanXcode(flutterProject.ios);
await _cleanXcode(flutterProject.macos);
}
final Directory buildDir = globals.fs.directory(getBuildDirectory());
deleteFile(buildDir);
///删除 .dart_tool
deleteFile(flutterProject.dartTool);
///删除 .android
deleteFile(flutterProject.android.ephemeralDirectory);
deleteFile(flutterProject.ios.ephemeralDirectory);
deleteFile(flutterProject.ios.generatedXcodePropertiesFile);
deleteFile(flutterProject.ios.generatedEnvironmentVariableExportScript);
deleteFile(flutterProject.ios.compiledDartFramework);
deleteFile(flutterProject.linux.ephemeralDirectory);
deleteFile(flutterProject.macos.ephemeralDirectory);
deleteFile(flutterProject.windows.ephemeralDirectory);
deleteFile(flutterProject.flutterPluginsDependenciesFile);
deleteFile(flutterProject.flutterPluginsFile);
return const FlutterCommandResult(ExitStatus.success);
}
在真正开发中,我们的的确确有一些与Flutter
之间的相互需要用Native
的代码来实现。而且我们的代码又不希望被删除。这个时候,我们就要使用到Flutter Plugin
来进行实现。
Flutter Plugin
还是创建一个Flutter Plugin
的项目,查看下项目结构。
Flutter Plugin
的项目结构于Flutter Application
类似,这样意味着,你可以在Native
的文件夹中存放代码,也不会被Pub Clean
删除。当然它与Flutter Application
还有有所区别的
- 其中多了一个
example
的文件夹用于写用例代码,方便单独运行 -
pubspec.yaml
里面多了一个声明当前项目的插件类。而这个插件就会在原生启动引擎的时候被调用 - 这个项目工程最后会以
AAR
的方式被导入到项目中,而Flutter Application
是APP
Flutter Package
这个就是构建一个纯dart
的项目。
创建和使用插件
- 使用
IDEA
创建一个默认模版的插件。 - 编写插件相关的逻辑代码。(可以借助原生的
api
完成自己所需要的功能) - 导入到需要插件的调用工程并且通过如下代码进行调用。
- 这样即可完成
flutter
与原生代码完成通讯。
class FlutterSimplePlugin {
static const MethodChannel _channel =
const MethodChannel('flutter_simple_plugin');
static Future<String> get platformVersion async {
final String version = await _channel.invokeMethod('getPlatformVersion');
return version;
}
}
image-20210428112044487.png
- 当我们使用
ymal
文件导入插件的时候,就具备了dart
的能力。 - 当我们使用
pub run
的时候,会将插件代码注册到原生中。 - 当我们启动
FlutterEngine
的时候,这些编写的插件会被初始化,并且等待dart
的调用。
插件的注册流程
我们大概了解下插件的注册流程。这样有助于我们对代码的调试以及整个插件的执行流程的理解。当我们新建一个Flutter Plugin
的项目的时候,默认会有一个android
文件夹被保留,并且执行pub clean
的时候,不会被删除。这样,当我们的插件被别的项目使用的时候,会被整合到一个GeneratedPluginRegistrant
的类中。这个类会被FlutterEngine
所调用,并且挂载到整个Flutter
的生命周期中。
-
flutter/packages/flutter_tools/plugins.dart
中包含Flutter
项目解析的流程。我们查看对应的代码逻辑:/// 遍历插件信息,type=android List<Map<String, dynamic>> _extractPlatformMaps(List<Plugin> plugins, String type) { final List<Map<String, dynamic>> pluginConfigs = <Map<String, dynamic>>[]; for (final Plugin p in plugins) { final PluginPlatform platformPlugin = p.platforms[type]; if (platformPlugin != null) { pluginConfigs.add(platformPlugin.toMap()); } } return pluginConfigs; }
-
然后将便利之后的插件信息注册到
GeneratedPluginRegistrant
中const String _androidPluginRegistryTemplateNewEmbedding = ''' package io.flutter.plugins; import androidx.annotation.Keep; import androidx.annotation.NonNull; import io.flutter.embedding.engine.FlutterEngine; {{#needsShim}} import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistry; {{/needsShim}} /** * Generated file. Do not edit. * This file is generated by the Flutter tool based on the * plugins that support the Android platform. */ @Keep public final class GeneratedPluginRegistrant { public static void registerWith(@NonNull FlutterEngine flutterEngine) { {{#needsShim}} ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine); {{/needsShim}} {{#plugins}} {{#supportsEmbeddingV2}} flutterEngine.getPlugins().add(new {{package}}.{{class}}()); {{/supportsEmbeddingV2}} {{^supportsEmbeddingV2}} {{#supportsEmbeddingV1}} {{package}}.{{class}}.registerWith(shimPluginRegistry.registrarFor("{{package}}.{{class}}")); {{/supportsEmbeddingV1}} {{/supportsEmbeddingV2}} {{/plugins}} } } ''';
-
当我们开始使用
FlutterEngine
的时候,就会将这些插件注册到FlutterEngine
中private void registerPlugins() { try { Class<?> generatedPluginRegistrant = Class.forName("io.flutter.plugins.GeneratedPluginRegistrant"); Method registrationMethod = generatedPluginRegistrant.getDeclaredMethod("registerWith", FlutterEngine.class); registrationMethod.invoke(null, this); } catch (Exception e) { Log.w( TAG, "Tried to automatically register plugins with FlutterEngine (" + this + ") but could not find and invoke the GeneratedPluginRegistrant."); } }
-
完成插件流程的分析之后,我们可以考虑一下,系统自带的插件是否存在有一些问题。
原生 plugin 存在的问题
-
MethodChannel
属于硬编码到项目中,ios
与android
统一性很差 -
_channel.invokeMethod
的返回值没有强制类型,三端统一需要沟通成本较大。 - 不利于后续的迭代
Pigeon的方式
创建和使用pigeon
- 在项目的
pubspec.yaml
文件中导入pigeon
的依赖。 - 然后你需要考验
Dart
和Flutter
需要哪些接口和数据。原生调用Flutter
代码需要用FlutterApi
注解,而Flutter
调用原生的Api
则需要HostApi
注解。
import 'package:pigeon/pigeon.dart';
/// 传递给原生的参数
class ToastContent {
String? content;
bool? center;
}
/// flutter 调用原生的方法
@HostApi()
abstract class ToastApi {
/// 接口协议
void showToast(ToastContent content);
}
- 当我们定义好两端所需要的数据结构后,就可以使用
pigeon
来自动话生成代码了。
flutter pub run pigeon
# 定义好的协议,pigeon会解析这个类,按照一定格式生成
--input test/pigeon/toast_api.dart
# 生成的 dart 文件
--dart_out lib/toast.dart
# 生成的 Object-C 文件
--objc_header_out ios/Classes/toast.h
--objc_source_out ios/Classes/toast.m
# 生成的 Java 文件
--java_out android/src/main/kotlin/com/vv/life/flutter/basic/flutter_pigeon_plugin/ToastUtils.java
# 生成的 Java 报名
--java_package "com.vv.life.flutter.basic.flutter_pigeon_plugin"
- 执行上述命令后,会在对应的文件夹中创建对应的协议代码,我们需要把我们的实现注入到对应的代码中
/** Sets up an instance of `ToastApi` to handle messages through the `binaryMessenger`. */
static void setup(BinaryMessenger binaryMessenger, ToastApi api) {
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ToastApi.showToast", new StandardMessageCodec());
if (api != null) {
channel.setMessageHandler((message, reply) -> {
Map<String, Object> wrapped = new HashMap<>();
try {
@SuppressWarnings("ConstantConditions")
ToastContent input = ToastContent.fromMap((Map<String, Object>)message);
api.showToast(input);
wrapped.put("result", null);
}
catch (Error | RuntimeException exception) {
wrapped.put("error", wrapError(exception));
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
}
}
-
pigeon
本身不会自动注入GeneratedPluginRegistrant
中,这就意味这你需要手动将pigeon
生成的代码注入到FlutterEngine
中。(销毁的时候,记得反注册)。
ToastUtils.ToastApi.setup(flutterPluginBinding.binaryMessenger){
Toast.makeText(flutterPluginBinding.getApplicationContext(),it.content,Toast.LENGTH_SHORT).show();
}
- 最终我们就可以在
dart
中调用Native
的代码
ToastApi().showToast(ToastContent()..content="我是测试数据");
image-20210428151326016.png
Pigeon的原理和代码解析器
-
首先
pigeon
是依据约定好的协议,生成对应的代码。从而从程序上出发来约束对应的接口。 -
当我们执行
flutter pub run pigeon
这个命令的时候,会被pigeon
这个库中的/bin/pigeon.dart
的main
方法所解析。
////bin/pigeon.dart 命令入口
Future<void> main(List<String> args) async {
exit(await runCommandLine(args));
}
/// pigeon/lib/pigeon_lib.dart 文件
static PigeonOptions parseArgs(List<String> args) {
// Note: This function shouldn't perform any logic, just translate the args
// to PigeonOptions. Synthesized values inside of the PigeonOption should
// get set in the `run` function to accomodate users that are using the
// `configurePigeon` function.
final ArgResults results = _argParser.parse(args);
final PigeonOptions opts = PigeonOptions();
opts.input = results['input'];
opts.dartOut = results['dart_out'];
opts.dartTestOut = results['dart_test_out'];
opts.objcHeaderOut = results['objc_header_out'];
opts.objcSourceOut = results['objc_source_out'];
opts.objcOptions = ObjcOptions(
prefix: results['objc_prefix'],
);
opts.javaOut = results['java_out'];
opts.javaOptions = JavaOptions(
package: results['java_package'],
);
opts.dartOptions = DartOptions()..isNullSafe = results['dart_null_safety'];
return opts;
}
-
最终会根据对应的格式,生成对应的代码。
void _writeHostApi(Indent indent, Api api) {
assert(api.location == ApiLocation.host);
indent.writeln(
'/** Generated interface from Pigeon that represents a handler of messages from Flutter.*/');
indent.write('public interface ${api.name} ');
indent.scoped('{', '}', () {
for (final Method method in api.methods) {
final String returnType =
method.isAsynchronous ? 'void' : method.returnType;
final List<String> argSignature = <String>[];
if (method.argType != 'void') {
argSignature.add('${method.argType} arg');
}
if (method.isAsynchronous) {
final String returnType =
method.returnType == 'void' ? 'Void' : method.returnType;
argSignature.add('Result<$returnType> result');
}
indent.writeln('$returnType ${method.name}(${argSignature.join(', ')});');
}
indent.addln('');
indent.writeln(
'/** Sets up an instance of `${api.name}` to handle messages through the `binaryMessenger`. */');
indent.write(
'static void setup(BinaryMessenger binaryMessenger, ${api.name} api) ');
indent.scoped('{', '}', () {
for (final Method method in api.methods) {
final String channelName = makeChannelName(api, method);
indent.write('');
indent.scoped('{', '}', () {
indent.writeln('BasicMessageChannel<Object> channel =');
indent.inc();
indent.inc();
indent.writeln(
'new BasicMessageChannel<>(binaryMessenger, "$channelName", new StandardMessageCodec());');
indent.dec();
indent.dec();
indent.write('if (api != null) ');
indent.scoped('{', '} else {', () {
indent.write('channel.setMessageHandler((message, reply) -> ');
indent.scoped('{', '});', () {
final String argType = method.argType;
final String returnType = method.returnType;
indent.writeln('Map<String, Object> wrapped = new HashMap<>();');
indent.write('try ');
indent.scoped('{', '}', () {
final List<String> methodArgument = <String>[];
if (argType != 'void') {
indent.writeln('@SuppressWarnings("ConstantConditions")');
indent.writeln(
'$argType input = $argType.fromMap((Map<String, Object>)message);');
methodArgument.add('input');
}
if (method.isAsynchronous) {
final String resultValue =
method.returnType == 'void' ? 'null' : 'result.toMap()';
methodArgument.add(
'result -> { '
'wrapped.put("${Keys.result}", $resultValue); '
'reply.reply(wrapped); '
'}',
);
}
final String call =
'api.${method.name}(${methodArgument.join(', ')})';
if (method.isAsynchronous) {
indent.writeln('$call;');
} else if (method.returnType == 'void') {
indent.writeln('$call;');
indent.writeln('wrapped.put("${Keys.result}", null);');
} else {
indent.writeln('$returnType output = $call;');
indent.writeln(
'wrapped.put("${Keys.result}", output.toMap());');
}
});
indent.write('catch (Error | RuntimeException exception) ');
indent.scoped('{', '}', () {
indent.writeln(
'wrapped.put("${Keys.error}", wrapError(exception));');
if (method.isAsynchronous) {
indent.writeln('reply.reply(wrapped);');
}
});
if (!method.isAsynchronous) {
indent.writeln('reply.reply(wrapped);');
}
});
});
indent.scoped(null, '}', () {
indent.writeln('channel.setMessageHandler(null);');
});
});
}
});
});
}