文件的方式
flutter和Native都具備對系統(tǒng)文件進行讀寫。這樣就提供了一種思路。用于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的方式講所需要的參數(shù)傳入到Flutter的Router參數(shù)中,這樣的話Flutter在渲染之前可以通過解析Router參數(shù)將所需要的參數(shù)解析出來。
// 原生數(shù)據(jù)獲取
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程序獲取到這些參數(shù)
/// flutter 解析數(shù)據(jù)
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開發(fā)的項目的時候,你就可以考慮使用這套模版來構建你的項目。你可以嘗試著創(chuàng)建這樣類型的項目,會發(fā)現(xiàn)其中的項目的目錄結構如下。

注意,這里的android文件夾和ios文件,前面并沒有帶有.這個和接下來要解釋的Flutter Module有所區(qū)別。
Flutter Module
當你需要把你編寫的Flutter代碼,以AAR的方式內嵌到原生的時候,可以嘗試使用這樣的方式,來創(chuàng)建自己的Flutter 項目。我們嘗試的創(chuàng)建一個Flutter Module項目查看下。

從上圖,我們可以發(fā)現(xiàn)Flutter Module的項目和Flutter Application的項目存放Native的代碼文件名稱都一樣,但是Flutter Module會把存放Native的代碼設置為隱藏文件,也就是在文件名稱前面加.。
我們在編寫Flutter Module的時候,經(jīng)常使用到Flutter Clean命令,會將.android和.ios進行刪除。也就意味著,你在Flutter Module編寫的Native的代碼都會被刪除。具體Flutter Clean所執(zhí)行的邏輯如下。
@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);
}
在真正開發(fā)中,我們的的確確有一些與Flutter之間的相互需要用Native的代碼來實現(xiàn)。而且我們的代碼又不希望被刪除。這個時候,我們就要使用到Flutter Plugin來進行實現(xiàn)。
Flutter Plugin
還是創(chuàng)建一個Flutter Plugin的項目,查看下項目結構。

Flutter Plugin的項目結構于Flutter Application類似,這樣意味著,你可以在Native的文件夾中存放代碼,也不會被Pub Clean刪除。當然它與Flutter Application還有有所區(qū)別的
- 其中多了一個
example的文件夾用于寫用例代碼,方便單獨運行 -
pubspec.yaml里面多了一個聲明當前項目的插件類。而這個插件就會在原生啟動引擎的時候被調用 - 這個項目工程最后會以
AAR的方式被導入到項目中,而Flutter Application是APP
Flutter Package
這個就是構建一個純dart的項目。
創(chuàng)建和使用插件
- 使用
IDEA創(chuàng)建一個默認模版的插件。 - 編寫插件相關的邏輯代碼。(可以借助原生的
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;
}
}

- 當我們使用
ymal文件導入插件的時候,就具備了dart的能力。 - 當我們使用
pub run的時候,會將插件代碼注冊到原生中。 - 當我們啟動
FlutterEngine的時候,這些編寫的插件會被初始化,并且等待dart的調用。
插件的注冊流程
我們大概了解下插件的注冊流程。這樣有助于我們對代碼的調試以及整個插件的執(zhí)行流程的理解。當我們新建一個Flutter Plugin的項目的時候,默認會有一個android文件夾被保留,并且執(zhí)行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."); } }
-
完成插件流程的分析之后,我們可以考慮一下,系統(tǒng)自帶的插件是否存在有一些問題。
原生 plugin 存在的問題
-
MethodChannel屬于硬編碼到項目中,ios與android統(tǒng)一性很差 -
_channel.invokeMethod的返回值沒有強制類型,三端統(tǒng)一需要溝通成本較大。 - 不利于后續(xù)的迭代
Pigeon的方式
創(chuàng)建和使用pigeon
- 在項目的
pubspec.yaml文件中導入pigeon的依賴。 - 然后你需要考驗
Dart和Flutter需要哪些接口和數(shù)據(jù)。原生調用Flutter代碼需要用FlutterApi注解,而Flutter調用原生的Api則需要HostApi注解。
import 'package:pigeon/pigeon.dart';
/// 傳遞給原生的參數(shù)
class ToastContent {
String? content;
bool? center;
}
/// flutter 調用原生的方法
@HostApi()
abstract class ToastApi {
/// 接口協(xié)議
void showToast(ToastContent content);
}
- 當我們定義好兩端所需要的數(shù)據(jù)結構后,就可以使用
pigeon來自動話生成代碼了。
flutter pub run pigeon
# 定義好的協(xié)議,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"
- 執(zhí)行上述命令后,會在對應的文件夾中創(chuàng)建對應的協(xié)議代碼,我們需要把我們的實現(xiàn)注入到對應的代碼中
/** 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="我是測試數(shù)據(jù)");

Pigeon的原理和代碼解析器
首先
pigeon是依據(jù)約定好的協(xié)議,生成對應的代碼。從而從程序上出發(fā)來約束對應的接口。當我們執(zhí)行
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;
}
最終會根據(jù)對應的格式,生成對應的代碼。
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);');
});
});
}
});
});
}