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に書き込まれます。具体的には、以下のような流れで処理されます。
-
flutter build apkの実態であるBuildApkCommandは、FlutterCommandを継承している[5]。flutter build apkコマンドの実行時に、FlutterCommand.run()が呼ばれ、その中でFlutterCommand.verifyThenRunCommand()が呼ばれる[6]。 -
FlutterCommand.verifyThenRunCommand()からFlutterCommand.regeneratePlatformSpecificToolingIfApplicableが呼ばれる[7]。 -
FlutterProject.regeneratePlatformSpecificToolingが呼ばれる[8]。 -
FlutterProject.ensureReadyForPlatformSpecificTooling()が呼ばれる[9]。 -
refreshPluginsList()が呼ばれる[10]。 -
_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);
}
}
}
GeneratedPluginRegistrantはregisterWithメソッドを持ち、これはFlutterEngineに対して、各PluginのMethodChannelを登録する役割を持ちます。これにより、MethodChannelを経由して、Pluginのネイティブコードとのやりとりが可能となります。
GeneratedPluginRegistrant.registerWithは、Androidアプリの起動時に、FlutterActivityのconfigureFlutterEngine内で実行されます[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として追加されるまでの流れについても解説しました。
-
Package types: https://docs.flutter.dev/packages-and-plugins/developing-packages#types ↩︎
-
Developing plugin packages: https://docs.flutter.dev/packages-and-plugins/developing-packages#plugin ↩︎
-
pub.dev: https://pub.dev/ ↩︎
-
build_apk.dart: https://github.com/flutter/flutter/blob/master/packages/flutter_tools/lib/src/commands/build_apk.dart ↩︎
-
BuildApkCommand: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/commands/build_apk.dart#L17 ↩︎ -
FlutterCommand.run(): https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/runner/flutter_command.dart#L1560 ↩︎ -
FlutterCommand.verifyThenRunCommand(): https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/runner/flutter_command.dart#L1871 ↩︎ -
FlutterCommand.regeneratePlatformSpecificToolingIfApplicable: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/runner/flutter_command.dart#L1915 ↩︎ -
FlutterProject.regeneratePlatformSpecificTooling: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/project.dart#L340 ↩︎ -
FlutterProject.ensureReadyForPlatformSpecificTooling(): https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/project.dart#L373 ↩︎ -
refreshPluginsList(): https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/flutter_plugins.dart#L1185 ↩︎ -
FlutterProject.ensureReadyForPlatformSpecificTooling(): https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/project.dart#L392 ↩︎ -
injectPlugins: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/flutter_plugins.dart#L1246 ↩︎ -
FlutterActivity.configureFlutterEngine: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java#L1356 ↩︎ -
BuildApkCommand.runCommand: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/commands/build_apk.dart#L131 ↩︎ -
AndroidGradleBuilder.buildGradleApp: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/android/gradle.dart#L441 ↩︎ -
FlutterAppPluginLoaderPlugin: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/gradle/src/main/kotlin/FlutterAppPluginLoaderPlugin.kt#L26 ↩︎ -
build_ios.dart: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/commands/build_ios.dart#L33 ↩︎ -
_writeIOSPluginRegistrant: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/flutter_plugins.dart#L780 ↩︎ -
_BuildIOSSubCommand.runCommand: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/commands/build_ios.dart#L758 ↩︎ -
buildXcodeProject(): https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/ios/mac.dart#L305 ↩︎ -
flutter_install_plugin_pods: https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/bin/podhelper.rb#L290 ↩︎ -
buildXcodeProject(): https://github.com/flutter/flutter/blob/fc2878142382dd7f8714d68baeccda1e1101b16c/packages/flutter_tools/lib/src/ios/mac.dart#L310 ↩︎
Discussion