Flutter 与原生之间的交互

文件的方式

flutterNative都具备对系统文件进行读写。这样就提供了一种思路。用于FlutterNative之间进行交互。

// 指定文件名称
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的方式讲所需要的参数传入到FlutterRouter参数中,这样的话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为我们提供如下项目模版。

image-20210401113543581.png

  1. Flutter Aplication
  2. Flutter Plugin
  3. FLutter package
  4. Flutter Module
Flutter Aplication

当你需要一个纯Flutter开发的项目的时候,你就可以考虑使用这套模版来构建你的项目。你可以尝试着创建这样类型的项目,会发现其中的项目的目录结构如下。

image-20210402104249986.png

注意,这里的android文件夹和ios文件,前面并没有带有.这个和接下来要解释的Flutter Module有所区别。

Flutter Module

当你需要把你编写的Flutter代码,以AAR的方式内嵌到原生的时候,可以尝试使用这样的方式,来创建自己的Flutter 项目。我们尝试的创建一个Flutter Module项目查看下。

image-20210402105412812.png

从上图,我们可以发现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的项目,查看下项目结构。

image-20210402113552267.png

Flutter Plugin的项目结构于Flutter Application类似,这样意味着,你可以在Native的文件夹中存放代码,也不会被Pub Clean删除。当然它与Flutter Application还有有所区别的

  1. 其中多了一个example的文件夹用于写用例代码,方便单独运行
  2. pubspec.yaml里面多了一个声明当前项目的插件类。而这个插件就会在原生启动引擎的时候被调用
  3. 这个项目工程最后会以AAR的方式被导入到项目中,而Flutter ApplicationAPP
Flutter Package

这个就是构建一个纯dart的项目。

创建和使用插件

  1. 使用IDEA创建一个默认模版的插件。
  2. 编写插件相关的逻辑代码。(可以借助原生的api完成自己所需要的功能)
  3. 导入到需要插件的调用工程并且通过如下代码进行调用。
  4. 这样即可完成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
  1. 当我们使用ymal文件导入插件的时候,就具备了dart的能力。
  2. 当我们使用pub run的时候,会将插件代码注册到原生中。
  3. 当我们启动FlutterEngine的时候,这些编写的插件会被初始化,并且等待dart的调用。
插件的注册流程

我们大概了解下插件的注册流程。这样有助于我们对代码的调试以及整个插件的执行流程的理解。当我们新建一个Flutter Plugin的项目的时候,默认会有一个android文件夹被保留,并且执行pub clean的时候,不会被删除。这样,当我们的插件被别的项目使用的时候,会被整合到一个GeneratedPluginRegistrant的类中。这个类会被FlutterEngine所调用,并且挂载到整个Flutter的生命周期中。

  1. 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;
    }
    
  2. 然后将便利之后的插件信息注册到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}}
      }
    }
    ''';
    
    1. 当我们开始使用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 存在的问题
  1. MethodChannel属于硬编码到项目中,iosandroid统一性很差
  2. _channel.invokeMethod的返回值没有强制类型,三端统一需要沟通成本较大。
  3. 不利于后续的迭代

Pigeon的方式

创建和使用pigeon

  1. 在项目的pubspec.yaml文件中导入pigeon的依赖。
  2. 然后你需要考验DartFlutter需要哪些接口和数据。原生调用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);
}
  1. 当我们定义好两端所需要的数据结构后,就可以使用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"
  1. 执行上述命令后,会在对应的文件夹中创建对应的协议代码,我们需要把我们的实现注入到对应的代码中
 /** 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);
        }
      }
    }
  }
  1. pigeon 本身不会自动注入GeneratedPluginRegistrant中,这就意味这你需要手动将pigeon生成的代码注入到FlutterEngine中。(销毁的时候,记得反注册)。
   ToastUtils.ToastApi.setup(flutterPluginBinding.binaryMessenger){
      Toast.makeText(flutterPluginBinding.getApplicationContext(),it.content,Toast.LENGTH_SHORT).show();
   }
  1. 最终我们就可以在dart中调用Native的代码
ToastApi().showToast(ToastContent()..content="我是测试数据");
image-20210428151326016.png

Pigeon的原理和代码解析器

  1. 首先pigeon是依据约定好的协议,生成对应的代码。从而从程序上出发来约束对应的接口。

  2. 当我们执行flutter pub run pigeon这个命令的时候,会被pigeon这个库中的/bin/pigeon.dartmain方法所解析。

////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;
  }

  1. 最终会根据对应的格式,生成对应的代码。

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);');
          });
        });
      }
    });
  });
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容