iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 11
0
Mobile Development

Why Flutter why? 從表層到底層,從如何到為何。系列 第 11

days[10] = "Plugin是怎麼運作的?"

大家在開發Flutter的時候應該免不了會使用到Plugin吧,或許你也已經熟悉幫助Plugin實現原生溝通的Platform Channel,甚至可能你自己就開發過Plugin,但你有沒有好奇過Flutter是怎麼把這一切串起來的呢?從我們在pubspec.yaml新增一個plugin dependency,到能夠呼叫Flutter端的Plugin API,到原生端能接收到訊息,這中間到底發生了什麼事?今天就讓我們來一探究竟。

我們先以flutter CLI,在D槽底下產生一個demo plugin專案作為今天的範例:

D:\>flutter create --template=plugin --platforms=android,ios demo

來看看產生專案的資料夾結構:
https://ithelp.ithome.com.tw/upload/images/20200911/20129053sLxNDn8FrT.png
可以看到上層的demo是一個標準Flutter plugin專案,包含lib資料夾,用來存放plugin的dart API。另外也包含Android/iOS子專案,用來編寫plugin的原生程式碼。demo內還有一個example,則是標準Flutter APP專案,用來提供使用demo plugin的範例程式碼。這裡同樣包含Android/iOS子專案,用來作為Flutter APP的wrapper。因為這裡總共有六個專案,而我們將會瀏覽到Flutter/Android的部份(因為我真的很不想用Xcode),小心不要搞錯了。


/demo/example (Flutter App Project)

# in pubspec.yaml

dependencies:
  flutter:
    sdk: flutter

  demo:
    # When depending on this package from a real application you should use:
    #   demo: ^x.y.z
    # See https://dart.dev/tools/pub/dependencies#version-constraints
    # The example app is bundled with the plugin so we use a path dependency on
    # the parent directory to use the current plugin's version.
    path: ../

這裡我們透過路徑path: ../直接引入本地的dependency,也就是上一層的demo plugin專案。


# in .flutter-plugins

# This is a generated file; do not edit or check into version control.
demo=D:\\demo\\

# in .flutter-plugins-dependencies
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"demo","path":"D:\\\\demo\\\\","dependencies":[]}],"android":[{"name":"demo","path":"D:\\\\demo\\\\","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"demo","dependencies":[]}],"date_created":"2020-09-11 20:50:56.862087","version":"1.20.2"}

這兩個檔案是根據我們的pubspec.yaml設定,在pub get之後產生的。一個紀錄plugin的路徑,一個紀錄plugin的dependency的路徑。


// in lib/main.dart
....
    try {
      platformVersion = await Demo.platformVersion;
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }
....

App很單純的使用deom plugin,呼叫template產生的platformVersion這個函數。


/demo/example/android (Android Project)

// in settings.gradle

apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"

// in app_plugin_loader.gradle
....
def object = new JsonSlurper().parseText(pluginsFile.text)
....
object.plugins.android.each { androidPlugin ->
  ....
  def pluginDirectory = new File(androidPlugin.path, 'android')
  ....
  include ":${androidPlugin.name}"
  project(":${androidPlugin.name}").projectDir = pluginDirectory
}

雖然是Groovy但其實也不難懂,基本上就是讀取剛剛的.flutter-plugins-dependencies,然後在我們的settings.gradle裡加入

include ":demo"
project(":demo").projectDir = D:\\demo\\android

也就是說,demo/example/android專案,引入了demo/android專案,作為它的一個子專案(Whaaat?)。


// in app/build.gradle
....
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
....

// in flutter.gradle
....
    /**
     * Configures the Flutter plugin dependencies.
     *
     * The plugins are added to pubspec.yaml. Then, upon running `flutter pub get`,
     * the tool generates a `.flutter-plugins` file, which contains a 1:1 map to each plugin location.
     * Finally, the project's `settings.gradle` loads each plugin's android directory as a subproject.
     */
    private void configurePlugins() {
        if (!buildPluginAsAar()) {
            getPluginList().each this.&configurePluginProject
            getPluginDependencies().each this.&configurePluginDependencies
            return
        }
        project.repositories {
            maven {
                url "${getPluginBuildDir()}/outputs/repo"
            }
        }
        getPluginList().each { pluginName, pluginPath ->
            configurePluginAar(pluginName, pluginPath, project)
        }
    }
....

flutter.gradle程式碼稍微複雜一點,但這裡主要功能就是把剛剛加入demo/example/android專案的demo/android專案,設定成demo/example/android/app的dependency(whaaaat?),如此一來我們就能在demo/example/android/app/..../GeneratedPluginRegistrant.java中,取得來自demo/android/.../DemoPlugin.kt的DemoPlugin。
我們也可以透過設定將demo/android包成aar再來建立dependency,但我們這邊是直接使用project source進行。
值得注意的是這裡的官方註解完全不是在註解這個函數在做什麼,而是呼叫這個函數之前發生了什麼事。


in app/src/main/java/io.flutter.plugins/GeneratedPluginRegistrant.java

@Keep
public final class GeneratedPluginRegistrant {
  public static void registerWith(@NonNull FlutterEngine flutterEngine) {
    flutterEngine.getPlugins().add(new com.example.demo.DemoPlugin());
  }
}

這也是自動產生的一個class,在呼叫時會傳入一個FlutterEngine,並把DemoPlugin(和其它所有Plugin,每一個Plugin都會在這裡產生一行code)加入FlutterEngine。我們可以在這裡下一個中斷點,看看是誰來呼叫registerWith:

// in FlutterActivity

  private static void registerPlugins(@NonNull FlutterEngine flutterEngine) {
    try {
      Class<?> generatedPluginRegistrant =
          Class.forName("io.flutter.plugins.GeneratedPluginRegistrant");
      Method registrationMethod =
          generatedPluginRegistrant.getDeclaredMethod("registerWith", FlutterEngine.class);
      registrationMethod.invoke(null, flutterEngine);
    } catch (Exception e) {
      Log.w(
          TAG,
          "Tried to automatically register plugins with FlutterEngine ("
              + flutterEngine
              + ") but could not find and invoke the GeneratedPluginRegistrant.");
    }
  }

我們在FlutterActivity中找到registerPlugins函數,裡面使用reflection的方式找到了自動產生的GeneratedPluginRegistrant.registerWith。那麼是誰產生它?如何產生?DemoPlugin又是從哪來的?讓我們繼續看下去。


/demo (Flutter Plugin Project)

# in pubspec.yaml

flutter:
  # This section identifies this Flutter project as a plugin project.
  # The 'pluginClass' and Android 'package' identifiers should not ordinarily
  # be modified. They are used by the tooling to maintain consistency when
  # adding or updating assets for this project.
  plugin:
    platforms:
      android:
        package: com.example.demo
        pluginClass: DemoPlugin
      ios:
        pluginClass: DemoPlugin

當我們執行pub get時,就會透過這裡的設定產生GeneratedPluginRegistrant,並在registerWith註冊一個被命名為com.example.demo.DemoPlugin的物件。而這個DemoPlugin則是我們一開始透過flutter create產生的,也就是我們的plugin實際要實作的Android部份。


/demo/android (Android Project)

public class DemoPlugin: FlutterPlugin, MethodCallHandler {
  private lateinit var channel : MethodChannel

  override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
    channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "demo")
    channel.setMethodCallHandler(this);
  }

  override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
    if (call.method == "getPlatformVersion") {
      result.success("Android ${android.os.Build.VERSION.RELEASE}")
    } else {
      result.notImplemented()
    }
  }

  override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
    channel.setMethodCallHandler(null)
  }

  // This static function is optional and equivalent to onAttachedToEngine. It supports the old
  // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting
  // plugin registration via this function while apps migrate to use the new Android APIs
  // post-flutter-1.12 via https://flutter.dev/go/android-project-migration.
  //
  // It is encouraged to share logic between onAttachedToEngine and registerWith to keep
  // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called
  // depending on the user's project. onAttachedToEngine or registerWith must both be defined
  // in the same class.
  companion object {
    @JvmStatic
    fun registerWith(registrar: Registrar) {
      val channel = MethodChannel(registrar.messenger(), "demo")
      channel.setMethodCallHandler(DemoPlugin())
    }
  }
}

這就是我們實作Plugin的原生Android部份的地方。注意我稍微改了一下template code,把舊API的registerWith移到最下方了,有興趣的人可以看一下它的註解。新API要求我們繼承FlutterPlugin,並實作onAttachedToEngine, onMethodCall, onDetachedFromEngine三個函數,其中onMethodCall就是魔法發生的地方了,在這裡實作我們想要的原生功能吧!


// in lib/demo.dart
class Demo {
  static const MethodChannel _channel =
      const MethodChannel('demo');

  static Future<String> get platformVersion async {
    final String version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
}

最後這裡就是設計我們的plugin API的地方,開啟一個MethodChannel,呼叫對應的原生函數並回傳結果。大功告成!


最後的最後讓我們以一張精美的流程圖總結今天學到的東西,我們下次見。
https://ithelp.ithome.com.tw/upload/images/20200911/20129053Dag2mjzODL.png


上一篇
days[9] = "為什麼需要依賴注入?(下)"
下一篇
days[11] = "為什麼要有key?"
系列文
Why Flutter why? 從表層到底層,從如何到為何。30

尚未有邦友留言

立即登入留言