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アプリに導入される仕組みを追います。
flutter-plugins-dependencies
に書き込む
1. pluginの一覧をまず、導入の対象となる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]。
dev.flutter.flutter-plugin-loader
により、pluginのネイティブコードがsubprojectとして追加される
4. 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]。
flutter-plugins-dependencies
に書き込む (Androidと同様)
1. pluginの一覧を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)
}
}
Podfile
内のflutter_install_all_ios_pods
で、pluginのネイティブコードがPodとして追加される
3. 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]。
xcodebuild
コマンドにより、iOSアプリのビルドが開始する
4. 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