【Flutter】了解插件(Plugins)功能

3,611 阅读6分钟

前言

在开发Flutter应用过程中会涉及到平台相关接口调用,例如数据库操作、相机调用、外部浏览器跳转等业务场景。其实Flutter自身并不支持直接在平台上实现这些,但在实际开发中我们会发现Pub.dev上会提供需要支持这些功能的package。事实上这些packages是为Flutter开发的插件包,通过插件包接口去调用指定平台API从而实现原生平台上特定功能。

举个例子

像Pub上发布的sqlite、url_launchr、shared_preference等这些开源库其实都是平台功能插件。它们内部分别在Android和iOS两个平台上实现 了功能代码,通过MethodChannel调用当前对应平台接口。

SharePreference

这里举例shared_preference插件提供了Android和iOS本地持久化存储功能。若是Android开发者那么你对sharePreference应该不陌生,它是以键值对的方式保存内容,支持存储Boolean、Int、String、Double等基本数据类型。 当项目引用sharePreference插件后可以在项目Flutter Plugins中找到它,同时看到sp功能在两个原生平台的代码实现。

下面从sp插件的Dart代码入手学习如何实现原生代码调用。

创建通道

sp插件Dart代码文件share_preferences.dart中第一行代码就是声明一个MethodChannel对象,加载路径为plugins.flutter.io/shared_preferences,该路径就是调用原生接口的类文件名。

const MethodChannel _kChannel =
    MethodChannel('plugins.flutter.io/shared_preferences');

可以在插件代码中找到原生实现类。

  • iOS
    SharedPreferencesPlugin.m文件中的声明
static NSString *const CHANNEL_NAME = @"plugins.flutter.io/shared_preferences";
  • Android
    在Android中就是包名路径,包名路径是颠倒的。
package io.flutter.plugins.sharedpreferences;
public class SharedPreferencesPlugin implements MethodCallHandler {
    ..
}

初始化键值对

SharedPreferences是以单例模式创建。初始化过程通过_getSharedPreferencesMap方法获取到本地所有持久化数据。

static const String _prefix = 'flutter.';
  static SharedPreferences _instance;
  static Future<SharedPreferences> getInstance() async {
    if (_instance == null) {
      final Map<String, Object> preferencesMap =
          await _getSharedPreferencesMap();
      _instance = SharedPreferences._(preferencesMap);
    }
    return _instance;
  }

通过channel调用原生接口

_getSharedPreferencesMap方法中主要关注_kChannel.invokeMapMethod<String, Object>('getAll');,通过原生通道调用在原生中的"getAll"方法。

static Future<Map<String, Object>> _getSharedPreferencesMap() async {
    //关键代码 调用原生中的"getAll"方法
    final Map<String, Object> fromSystem =
        await _kChannel.invokeMapMethod<String, Object>('getAll');
    assert(fromSystem != null);
    // Strip the flutter. prefix from the returned preferences.
    final Map<String, Object> preferencesMap = <String, Object>{};
    for (String key in fromSystem.keys) {
      assert(key.startsWith(_prefix));
      preferencesMap[key.substring(_prefix.length)] = fromSystem[key];
    }
    return preferencesMap;
  }

Android原生方法调用

原生调用类需要实现Flutter的MethodCallHandler接口,而MethodCallHandler也只有一个接口方法,返回MethodCall和MethodChannel.Result两个参数。

MethodCall类包含两个参数method和argument:method为调用的方法名称;argument是Dart的传参Object。MethodChannel.Result则是回调接口,用于返回原生与Flutter交互结果,提供三个回调接口调用:success(成功)、error(失败)、notImplemented(接口未知)。在成功或失败接口中可以返回Object回传给Flutter。

public class SharedPreferencesPlugin implements MethodCallHandler {
    ......
}
public interface MethodCallHandler {
    @UiThread
    void onMethodCall(@NonNull MethodCall var1, @NonNull MethodChannel.Result var2);
}

iOS的OC同样需要实现接口:@interface FLTSharedPreferencesPlugin : NSObject

再回头看SharedPreferencePlugin中实现onMethodCall接口,通过获取MethodCall的method得到调用需要调用的方法,然后通过argument获取传参,再调用原生preferences去做存储操作,最后根据调用情况通过result回调接口通知Flutter操作。当然其他操作也是如此,只要定义好call.method就能实现想要的结果。

 @Override
  public void onMethodCall(MethodCall call, MethodChannel.Result result) {
    String key = call.argument("key");
    try {
      switch (call.method) {
        case "setBool":
          commitAsync(preferences.edit().putBoolean(key, (boolean) call.argument("value")), result);
          break;
        case "setDouble":
        ...
        case "commit":
        // We've been committing the whole time.
         result.success(true);
        
        ......
        default:
          result.notImplemented();
          break;
}

打通通道

Flutter Service包文件中platorm_channel.dart中实现了MethodChannel类。

class MethodChannel {
  /// None of [name], [binaryMessenger], or [codec] may be null.
  const MethodChannel(this.name, [this.codec = const StandardMethodCodec(), this.binaryMessenger = defaultBinaryMessenger ])
    : assert(name != null),
      assert(binaryMessenger != null),
      assert(codec != null);
    ......
}

同时在Flutter支持的JavaSDK中也能找到同样的类文件MethodChannel,两者具有相同的成员变量和方法。可以说是打通平台关键类。在JavaSDK中MethodChannel是一个final类,同Flutter的MethodChannel拥有三个成员变量:BinaryMessenger、BinaryMessenger、MethodCodec。BinaryMessenger和MethodCodec是两个接口成员。

public final class MethodChannel {
    private static final String TAG = "MethodChannel#";
    private final BinaryMessenger messenger;
    private final String name;
    private final MethodCodec codec;
    public MethodChannel(BinaryMessenger messenger, String name) {
        this(messenger, name, StandardMethodCodec.INSTANCE);
    }
    //方法的调用则是通过BinaryMessenger messenger成员发送
    @UiThread
    public void invokeMethod(String method, @Nullable Object arguments, MethodChannel.Result callback) {
        this.messenger.send(this.name, this.codec.encodeMethodCall(new MethodCall(method, arguments)), callback == null ? null : new MethodChannel.IncomingResultHandler(callback));
    }
    ......
}

Dart中MethodChannel和原生类的MethodChannel拥有相同成员和方法也证实了原生和Flutter可相互调用。

这里举例webview_flutter插件源码,在FlutterWebViewClient.java中有一个方法通过methodChannel.invokeMethod调用onPageFinished方法。

private void onPageFinished(WebView view, String url) {
    Map<String, Object> args = new HashMap<>();
    args.put("url", url);
    methodChannel.invokeMethod("onPageFinished", args);
  }

可以在webview_method_channel.dart中确实也看到了methodCall接口实现并接受onPageFinished方法。也就了解除了在Flutter中可以调用原生方法外,原生同样可以调用Flutter方法也就是说实现了双向通行功能,这也为混合开发提供了可能性。

Future<bool> _onMethodCall(MethodCall call) async {
    switch (call.method) {
        ......
      case 'onPageFinished':
        _platformCallbacksHandler.onPageFinished(call.arguments['url']);
        return null;
    }
    throw MissingPluginException(
        '${call.method} was invoked but has no handler');
  }

插件注册

Android的Java代码中实现插件注册,实例化插件MethodChannel过程让PluginRegistry.Registrar的BinaryMessenger作为通信通道,注册插件名称。然后setMethodCallHandler将插件对象通过messenger进行发送。

  //SharedPreferencesPlugin实现MethodChannel注册方法
  public static void registerWith(PluginRegistry.Registrar registrar) {
   //实例化方法通道,设置通道和插件名称
    MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME);
    SharedPreferencesPlugin instance = new SharedPreferencesPlugin(registrar.context());
    //通过registrar将插件发送
    channel.setMethodCallHandler(instance);
  }
  .......
  //全局插件入口GeneratedPluginRegistrant,所有插件都在这注册并只注册一次
  public final class GeneratedPluginRegistrant {
  public static void registerWith(PluginRegistry registry) {
    if (alreadyRegisteredWith(registry)) {
      return;
    }
    ....
    SharedPreferencesPlugin.registerWith(registry.registrarFor("io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin"));
  } 
  ......
  //Flutter的MainActivity对全局插件注册
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);
  }

而PluginRegistry对象的源头需要追踪到FlutterActivity中FlutterActivityDelegate,然后是Delegate内部成员FlutterView。FlutterView通过DartExecutor实现了BinaryMessenger接口,在DartExecutor由DartMessenger真正去负责发送管道通信消息。后续更深入的代码则就交付给JNI Native层去做处理就不展开继续深入介绍。更多细节可以参考Gityuan的深入理解Flutter引擎启动 和闲鱼理解Platform Channel工作原理

再多提一点,在FlutterView源码中看到View做初始化操作时实例化了必要的平台通道。这样一来也就明白Flutter调用多平台特性的能力是原来如此,开发者开发自定义插件是在原有平台通道基础进行拓展。

this.navigationChannel = new NavigationChannel(this.dartExecutor);
this.keyEventChannel = new KeyEventChannel(this.dartExecutor);
this.lifecycleChannel = new LifecycleChannel(this.dartExecutor);
this.localizationChannel = new LocalizationChannel(this.dartExecutor);
this.platformChannel = new PlatformChannel(this.dartExecutor);
this.systemChannel = new SystemChannel(this.dartExecutor);
this.settingsChannel = new SettingsChannel(this.dartExecutor);
final PlatformPlugin platformPlugin = new PlatformPlugin(activity, this.platformChannel);

PS: 若平台底层接口在高低版本存在差异性则对于后期适配还是会造成比较大的影响,反观平台SDK一般情况也不太会有过大的接口变化,但顾虑多也不是件坏事。

实现自定义插件

官网有介绍如何自行开发插件包Demo实战。详见文档

后续若在开发需求中有涉及到自定义插件功能可以再单独介绍开发自定义插件篇。

参考