Flutter實戰之瞭解外掛(Plugins)功能篇

JulyYu發表於2019-08-14

前言

在開發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功能在兩個原生平臺的程式碼實現。

Flutter實戰之瞭解外掛(Plugins)功能篇

下面從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工作原理

Flutter實戰之瞭解外掛(Plugins)功能篇

再多提一點,在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實戰。詳見文件

後續若在開發需求中有涉及到自定義外掛功能可以再單獨介紹開發自定義外掛篇。

參考

相關文章