likes
comments
collection
share

Flutter 与原生之间的交互

作者站长头像
站长
· 阅读数 40

文件的方式

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为我们提供如下项目模版。 Flutter 与原生之间的交互

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

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

Flutter 与原生之间的交互

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

Flutter Module

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

从上图,我们可以发现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 与原生之间的交互

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

Flutter 与原生之间的交互

  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//flutter/basic/flutter_pigeon_plugin/ToastUtils.java 
 # 生成的 Java 报名
 --java_package "com.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="我是测试数据");

Flutter 与原生之间的交互

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);');
          });
        });
      }
    });
  });
}
转载自:https://juejin.cn/post/6956399829274591239
评论
请登录