Flutter 與原生之間的交互

文件的方式

flutterNative都具備對系統(tǒng)文件進行讀寫。這樣就提供了一種思路。用于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的方式講所需要的參數(shù)傳入到FlutterRouter參數(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為我們提供如下項目模版。

image-20210401113543581.png

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

當你需要一個純Flutter開發(fā)的項目的時候,你就可以考慮使用這套模版來構建你的項目。你可以嘗試著創(chuàng)建這樣類型的項目,會發(fā)現(xiàn)其中的項目的目錄結構如下。

image-20210402104249986.png

注意,這里的android文件夾和ios文件,前面并沒有帶有.這個和接下來要解釋的Flutter Module有所區(qū)別。

Flutter Module

當你需要把你編寫的Flutter代碼,以AAR的方式內嵌到原生的時候,可以嘗試使用這樣的方式,來創(chuàng)建自己的Flutter 項目。我們嘗試的創(chuàng)建一個Flutter Module項目查看下。

image-20210402105412812.png

從上圖,我們可以發(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的項目,查看下項目結構。

image-20210402113552267.png

Flutter Plugin的項目結構于Flutter Application類似,這樣意味著,你可以在Native的文件夾中存放代碼,也不會被Pub Clean刪除。當然它與Flutter Application還有有所區(qū)別的

  1. 其中多了一個example的文件夾用于寫用例代碼,方便單獨運行
  2. pubspec.yaml里面多了一個聲明當前項目的插件類。而這個插件就會在原生啟動引擎的時候被調用
  3. 這個項目工程最后會以AAR的方式被導入到項目中,而Flutter ApplicationAPP
Flutter Package

這個就是構建一個純dart的項目。

創(chuàng)建和使用插件

  1. 使用IDEA創(chuàng)建一個默認模版的插件。
  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的調用。
插件的注冊流程

我們大概了解下插件的注冊流程。這樣有助于我們對代碼的調試以及整個插件的執(zhí)行流程的理解。當我們新建一個Flutter Plugin的項目的時候,默認會有一個android文件夾被保留,并且執(zhí)行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.");
          }
        }
      

完成插件流程的分析之后,我們可以考慮一下,系統(tǒng)自帶的插件是否存在有一些問題。

原生 plugin 存在的問題
  1. MethodChannel屬于硬編碼到項目中,iosandroid統(tǒng)一性很差
  2. _channel.invokeMethod的返回值沒有強制類型,三端統(tǒng)一需要溝通成本較大。
  3. 不利于后續(xù)的迭代

Pigeon的方式

創(chuàng)建和使用pigeon

  1. 在項目的pubspec.yaml文件中導入pigeon的依賴。
  2. 然后你需要考驗DartFlutter需要哪些接口和數(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);
}
  1. 當我們定義好兩端所需要的數(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"
  1. 執(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);
        }
      }
    }
  }
  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="我是測試數(shù)據(jù)");
image-20210428151326016.png

Pigeon的原理和代碼解析器

  1. 首先pigeon是依據(jù)約定好的協(xié)議,生成對應的代碼。從而從程序上出發(fā)來約束對應的接口。

  2. 當我們執(zhí)行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. 最終會根據(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);');
          });
        });
      }
    });
  });
}
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容