大家在開發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
來看看產生專案的資料夾結構:
可以看到上層的demo是一個標準Flutter plugin專案,包含lib資料夾,用來存放plugin的dart API。另外也包含Android/iOS子專案,用來編寫plugin的原生程式碼。demo內還有一個example,則是標準Flutter APP專案,用來提供使用demo plugin的範例程式碼。這裡同樣包含Android/iOS子專案,用來作為Flutter APP的wrapper。因為這裡總共有六個專案,而我們將會瀏覽到Flutter/Android的部份(因為我真的很不想用Xcode),小心不要搞錯了。
# 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這個函數。
// 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又是從哪來的?讓我們繼續看下去。
# 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部份。
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,呼叫對應的原生函數並回傳結果。大功告成!
最後的最後讓我們以一張精美的流程圖總結今天學到的東西,我們下次見。