🔍

Plugin Packageの動作原理:各プラットフォーム (iOS/Android) 固有の実装がFlutterアプリに導入される仕組み

に公開

Flutterアプリで利用可能なPackageの一種に、Plugin Packageがあります[1]
Plugin Packageとは、プラットフォーム非依存なDartコードだけでなく、各プラットフォーム固有の実装を含むPackageです。
本記事では、Plugin PackageをFlutterアプリに追加した際に、各プラットフォーム固有の実装がAndroidアプリあるいはiOSアプリに導入される仕組みを、ソースコードから解明します。

Plugin Packageの構造

はじめに、Plugin Packageのファイル構造を確認します。

以下のコマンドを実行すると、AndroidならびにiOS向けの固有実装を含むPlugin Package (名称はhello) の初期テンプレートを作成することができます[2]

$ flutter create --org com.example --template=plugin --platforms=android,ios -a kotlin -i swift hello

上記のコマンドで作成されたプロジェクトは、以下のようなファイル構造となります (一部省略)。

hello/
├── lib/ # プラットフォーム非依存なDartコードを含むディレクトリ
│   ├── hello.dart
│   ├── hello_method_channel.dart
│   └── hello_platform_interface.dart
├── android/ # Android向けのコードを含むディレクトリ
│   ├── build.gradle
│   ├── settings.gradle
│   └── src/main/kotlin/com/example/hello/
│       └── HelloPlugin.kt
├── ios/ # iOS向けのコードを含むディレクトリ
│   ├── Classes/
│   │   └── HelloPlugin.swift
│   └── hello.podspec
└── pubspec.yaml

libディレクトリに、プラットフォーム非依存なDartコードが含まれます。
一方、androidおよびiosディレクトリに、各プラットフォームに固有のコードが含まれます。

このようなPlugin PackageがFlutterアプリに追加された際に、各プラットフォームに固有のコードが、AndroidアプリあるいはiOSアプリにそれぞれどのように導入されるのかを解説します。

Plugin Packageが配布される仕組み

まず、前提知識として、Plugin Packageが配布される仕組みに触れます。

FlutterのPackageは一般にpub.dev[3]から配布されます。
flutter pub getコマンドの実行時に、pub.devから、Packageのソースコードが圧縮されたファイルがダウンロードされます。ソースコード自体が配布されることは、pub.devのWebサイトにアクセスし、あるPackageの「Versions」ページから、あるバージョンのPackageを手動ダウンロードすれば確認できます。
これは、ソースコード自体ではなくコンパイル済みのバイトコードが配布されるMaven Repositoryのような仕組みとは異なります

flutter pub getコマンドによりダウンロードされたPackageは、ローカルマシン上にキャッシュされ、Flutterアプリのビルド時に参照されます。
macOSで開発している場合、~/.pub-cache/hosted/pub.dev内にダウンロードされたPackageがキャッシュされます。

Plugin PackageがAndroidアプリに導入される仕組み

flutter pub getコマンドにより、Android向けのコード (JavaあるいはKotlin) を含むPackageのソースコードが、ローカルにキャッシュされることを述べました。

このセクションでは、Flutterアプリのビルド時、すなわちflutter buildコマンドの実行時に、どのようにAndroid向けのコードがFlutterアプリに組み込まれるのかを解説します。

flutterリポジトリのpackages/flutter_tools/lib/src/commands/build_apk.dart[4]に、AndroidアプリのAPKをビルドするコマンド (flutter build apk) のソースコードがあります。
このファイルを起点に、Android向けのコードがFlutterアプリに導入される仕組みを追います。

1. pluginの一覧をflutter-plugins-dependenciesに書き込む

まず、導入の対象となるpluginの一覧がpubspec.yamlから抽出され、.flutter-plugins-dependenciesに書き込まれます。具体的には、以下のような流れで処理されます。

  1. flutter build apkの実態であるBuildApkCommandは、FlutterCommandを継承している[5]flutter build apkコマンドの実行時に、FlutterCommand.run()が呼ばれ、その中でFlutterCommand.verifyThenRunCommand()が呼ばれる[6]
  2. FlutterCommand.verifyThenRunCommand()からFlutterCommand.regeneratePlatformSpecificToolingIfApplicableが呼ばれる[7]
  3. FlutterProject.regeneratePlatformSpecificToolingが呼ばれる[8]
  4. FlutterProject.ensureReadyForPlatformSpecificTooling()が呼ばれる[9]
  5. refreshPluginsList()が呼ばれる[10]
  6. _writeFlutterPluginsList()が呼ばれる。pubspec.yamlからplugin一覧が抽出され、.flutter-plugins-dependenciesファイルに書き込まれる[11]

2. 各PluginをFlutterEngineに登録して、Flutterアプリとネイティブコードのやりとりを可能とする

先述したFlutterProject.ensureReadyForPlatformSpecificTooling() (1-4のステップ) 内で、上述したflutter-plugins-dependenciesへの書き込みの後に、injectPlugins()が呼ばれます[12]

injectPlugins内では、最終的にGeneratedPluginRegistrant.javaファイルが生成されます[13]GeneratedPluginRegistrant.javaは、以下のような構造をしています。

  public final class GeneratedPluginRegistrant {
    public static void registerWith(@NonNull FlutterEngine flutterEngine) {
      // 各pluginについて、`FlutterEngine.getPlugins().add()`を呼ぶ。
      try {
        flutterEngine.getPlugins().add(new com.example.plugin.PluginClass());
      } catch (Exception e) {
        Log.e(TAG, "Error registering plugin...", e);
      }
    }
  }

GeneratedPluginRegistrantregisterWithメソッドを持ち、これはFlutterEngineに対して、各PluginのMethodChannelを登録する役割を持ちます。これにより、MethodChannelを経由して、Pluginのネイティブコードとのやりとりが可能となります

GeneratedPluginRegistrant.registerWithは、Androidアプリの起動時に、FlutterActivityconfigureFlutterEngine内で実行されます[14]

3. Gradleタスクが実行され、Androidアプリのビルドが開始する

2.でGeneratedPluginRegistrantが作成された後、BuildApkCommand.runCommand内で、AndroidGradleBuilder.buildApkが呼ばれます[15]
最終的にAndroidGradleBuilder.buildGradleApp内でgradle assembleが呼ばれ、Androidアプリのビルドが開始します[16]

4. dev.flutter.flutter-plugin-loaderにより、pluginのネイティブコードがsubprojectとして追加される

Flutterプロジェクトのandroidディレクトリ内のsettings.gradleを見ると、以下のようにdev.flutter.flutter-plugin-loaderが存在するでしょう。

plugins {
    id "dev.flutter.flutter-plugin-loader" version "1.0.0"
}

3.で呼ばれたGradleタスクの実行時に、settings.gradleが読み込まれる段階で、このGradle pluginが実行されます。

このGradle pluginは、ステップ1で作成された.flutter-plugins-dependenciesを参照し、AndroidアプリのGradleプロジェクトに対して、各PluginのAndroid向けのネイティブコードをsubprojectとして追加します。

Gradle pluginの実装は、FlutterAppPluginLoaderPlugin.ktに存在します[17]
FlutterAppPluginLoaderPlugin.apply内のソースコードの一部を以下に示します。各pluginをincludeして、subprojectとして追加していることが分かります。

        NativePluginLoaderReflectionBridge
            .getPlugins(settings.extraProperties, flutterProjectRoot)
            .forEach { androidPlugin ->
                val pluginDirectory = File(androidPlugin["path"] as String, "android")
                check(
                    pluginDirectory.exists()
                ) { "Plugin directory does not exist: ${pluginDirectory.absolutePath}" }
                val pluginName = androidPlugin["name"] as String
                settings.include(":$pluginName")
                settings.project(":$pluginName").projectDir = pluginDirectory
            }

以上、Plugin Package内のAndroid向けのコードが、最終的にAndroidアプリにsubprojectとして追加されるまでの流れを解説しました。

Plugin PackageがiOSアプリに導入される仕組み

続いて、Plugin Package内のiOS向けのコードが、iOSアプリに導入される仕組みを解説します。
今回は、packages/flutter_tools/lib/src/commands/build_ios.dartを起点とします[18]

1. pluginの一覧をflutter-plugins-dependenciesに書き込む (Androidと同様)

Androidと同様に、FlutterCommand.run()を起点として、flutter-plugins-dependenciesにpluginの一覧が書き込まれます。

2. 各PluginをFlutterEngineに登録して、Flutterアプリとネイティブコードのやりとりを可能とする (Androidと同様)

AndroidではGeneratedPluginRegistrant.javaファイルが生成されましたが、iOSでは_writeIOSPluginRegistrant()内でObjective-Cのファイル (GeneratedPluginRegistrant.hならびにGeneratedPluginRegistrant.m) が生成されます[19]

GeneratedPluginRegistrant.registerはAppDelegate内で呼ばれる必要があります。これはFlutterプロジェクトの初期化時にデフォルトで入っているでしょう。
これにより、iOSアプリの起動時に、MethodChannel経由でPluginのネイティブコードとのやりとりが可能となります。

@main
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

3. Podfile内のflutter_install_all_ios_podsで、pluginのネイティブコードがPodとして追加される

2.の後、_BuildIOSSubCommand.runCommand内でbuildXcodeProject()が呼ばれます[20]
buildXcodeProject()内でprocessPodsIfNeeded()が呼ばれ、pod installが実行されます[21]

FlutterプロジェクトのPodfileを見ると、flutter_install_all_ios_podsがデフォルトで存在するでしょう。

target 'Runner' do
  use_frameworks!

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
  target 'RunnerTests' do
    inherit! :search_paths
  end
end

このflutter_install_all_ios_pods内で呼ばれるflutter_install_plugin_podsで、1.のステップで作成された.flutter-plugins-dependenciesファイルが参照され、Plugin PackageのiOS向けのネイティブコードがPodとして追加されます[22]

4. xcodebuildコマンドにより、iOSアプリのビルドが開始する

3.のステップの後、buildXcodeProject()内で、xcodebuildコマンドが実行されて、iOSアプリのビルドが開始します[23]


以上、Plugin Package内のiOS向けのネイティブコードが、最終的にiOSアプリにPodとして追加されるまでの流れについても解説しました。

脚注
  1. Package types: https://docs.flutter.dev/packages-and-plugins/developing-packages#types ↩︎

  2. Developing plugin packages: https://docs.flutter.dev/packages-and-plugins/developing-packages#plugin ↩︎

  3. pub.dev: https://pub.dev/ ↩︎

  4. build_apk.dart: https://github.com/flutter/flutter/blob/master/packages/flutter_tools/lib/src/commands/build_apk.dart ↩︎

  5. BuildApkCommand: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/commands/build_apk.dart#L17 ↩︎

  6. FlutterCommand.run(): https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/runner/flutter_command.dart#L1560 ↩︎

  7. FlutterCommand.verifyThenRunCommand(): https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/runner/flutter_command.dart#L1871 ↩︎

  8. FlutterCommand.regeneratePlatformSpecificToolingIfApplicable: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/runner/flutter_command.dart#L1915 ↩︎

  9. FlutterProject.regeneratePlatformSpecificTooling: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/project.dart#L340 ↩︎

  10. FlutterProject.ensureReadyForPlatformSpecificTooling(): https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/project.dart#L373 ↩︎

  11. refreshPluginsList(): https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/flutter_plugins.dart#L1185 ↩︎

  12. FlutterProject.ensureReadyForPlatformSpecificTooling(): https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/project.dart#L392 ↩︎

  13. injectPlugins: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/flutter_plugins.dart#L1246 ↩︎

  14. FlutterActivity.configureFlutterEngine: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java#L1356 ↩︎

  15. BuildApkCommand.runCommand: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/commands/build_apk.dart#L131 ↩︎

  16. AndroidGradleBuilder.buildGradleApp: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/android/gradle.dart#L441 ↩︎

  17. FlutterAppPluginLoaderPlugin: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/gradle/src/main/kotlin/FlutterAppPluginLoaderPlugin.kt#L26 ↩︎

  18. build_ios.dart: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/commands/build_ios.dart#L33 ↩︎

  19. _writeIOSPluginRegistrant: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/flutter_plugins.dart#L780 ↩︎

  20. _BuildIOSSubCommand.runCommand: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/commands/build_ios.dart#L758 ↩︎

  21. buildXcodeProject(): https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/ios/mac.dart#L305 ↩︎

  22. flutter_install_plugin_pods: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/bin/podhelper.rb#L290 ↩︎

  23. buildXcodeProject(): https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/ios/mac.dart#L310 ↩︎

Discussion