美文网首页
Flutter 与原生之间的交互

Flutter 与原生之间的交互

作者: ___刘辉良 | 来源:发表于2021-04-29 10:22 被阅读0次

    文件的方式

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

    相关文章

      网友评论

          本文标题:Flutter 与原生之间的交互

          本文链接:https://www.haomeiwen.com/subject/fmiirltx.html