目標:
1.Flutter插件是什么?有什么作用?
插件 (plugin) 是 package 的一種,全稱是 plugin package,我們簡稱為 plugin,中文叫插件。
2.怎么創建Flutter插件?
一、什么是插件
在flutter中,一個插件叫做一個package,使用packages的目的就是為了達到模塊化,可以創建出可被復用和共享的代碼,這和大多數編程語言中的模塊、包的概念相同。創建出來的package可以在pubspec.yaml中直接依賴。
1.1 package組成
Flutter插件組成
- 一個pubspec.yaml文件:一個元數據文件,聲明了聲明了package的名稱、版本、作者等信息。
- 一個lib文件夾:包含里package的公開代碼,文件夾至少需要存在<pakcage-name>.dart這個文件。
注意:<pakcage-name>.dart這個文件必須存在,因為這是方便使用的人快速import這個package來使用它,可以把它理解成一種必須要遵守的規則。
1.2 package分類
package可以分為兩種:純dart代碼的package和帶有特定平臺代碼的package。
- Dart packages:這是一個只有dart代碼的package,里面包含了flutter的特定功能,所以它依賴于flutter的framework,也決定了它只能用在flutter上。
- plugin packages:這是一個既包含了dart代碼編寫的api,又包含了平臺(Android/IOS)特定實現的package,可以被Android和ios調用。
- FFI 插件
用 Dart 語言編寫針對一個或多個特定平臺的 API,使用 Dart FFI (Android、iOS、macOS)。
> 上面應該很好理解,可以理解成java jar包和Android sdk的區別。而要開發的日志插件就是第二種。
二、插件開發
2.1 創建package
可以使用AS創建插件
然后點擊next。
?
然后點擊 Create按鈕,開始創建插件項目。
如果是采用Flutter命令創建項目
// 想要創建初始的 Flutter package,請使用帶有 --template=package 標志的 flutter create 命令:flutter create --template=package hello
2.2 項目文件結構
項目文件結構如下:
LICENSE 文件
大概率會是空的一個許可證文件。
- test/hello_test.dart 文件
Package 的?單元測試?文件。
- hello.iml 文件
由 IntelliJ 生成的配置文件。
- .gitignore 文件
告訴 Git 系統應該隱藏哪些文件或文件夾的一個隱藏文件。
- .metadata 文件
IDE 用來記錄某個 Flutter 項目屬性的的隱藏文件。
- pubspec.yaml 文件
pub 工具需要使用的,包含 package 依賴的 yaml 格式的文件。
- README.md 文件
起步文檔,用于描述 package。
- lib/hello.dart 文件
package 的 Dart 實現代碼。
- .idea/modules.xml、.idea/workspace.xml 文件
IntelliJ 的各自配置文件(包含在 .idea 隱藏文件夾下)。
- CHANGELOG.md 文件
又一個大概率為空的文檔,用于記錄 package 的版本變更。插件的native端實現
- android/
插件包API的Android實現
- iOS
插件包API的ios實現.
- example/:
? ?一個依賴于該插件的Flutter應用程序,來說明如何使用它
lib庫定義插件的主要功能。
2.3 實現插件
對于純 Dart 庫的 package,只要在 lib/<package name>.dart 文件中添加功能實現,或在 lib 目錄中的多個文件中添加功能實現。
如果要對 package 進行測試,在 test 目錄下添加 單元測試。
2.3.1 創建MethodChannel
項目默認生成了插件MethodChannel
1. 創建MethodChannel
flutter_log_plugin_method_channel.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';import 'flutter_log_plugin_platform_interface.dart';/// An implementation of [FlutterLogPluginPlatform] that uses method channels.
class MethodChannelFlutterLogPlugin extends FlutterLogPluginPlatform {/// The method channel used to interact with the native platform.@visibleForTestingfinal methodChannel = const MethodChannel('flutter_log_plugin');@overrideFuture<String?> getPlatformVersion() async {final version = await methodChannel.invokeMethod<String>('getPlatformVersion');return version;}
}
2.定義插件方法?
- 創建了一個MethodChannel,名稱為flutter_log_plugin
- 提供了一個方法訪問getPlatformVersion
我們看看其調用的方式,通過創建的methodChannel.invokeMethod來調用原生實現。
final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
2.3.2 插件方法Native實現
在項目 android 目錄下,增加對?MethodChannel 方法的實現。
默認的插件實現的功能:Dart通過插件,獲取 native端系統版本信息。
在android/src.main? 下,實現了Native方法
package com.example.flutter_log_pluginimport androidx.annotation.NonNullimport io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result/** FlutterLogPlugin */
class FlutterLogPlugin: FlutterPlugin, MethodCallHandler {/// The MethodChannel that will the communication between Flutter and native Android////// This local reference serves to register the plugin with the Flutter Engine and unregister it/// when the Flutter Engine is detached from the Activityprivate lateinit var channel : MethodChanneloverride fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_log_plugin")channel.setMethodCallHandler(this)}/*** 方法調用處理** @author zhouronghua* @time 2024/6/27 下午3:12*/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)}
}
MethodChannel提供Flutter與原生系統之間的通信。
1.綁定MethodChannel: Activity attach到Flutter引擎
/// This local reference serves to register the plugin with the Flutter Engine and unregister it/// when the Flutter Engine is detached from the Activityprivate lateinit var channel : MethodChanneloverride fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_log_plugin")channel.setMethodCallHandler(this)}
因為Flutter提供的引擎是“flutter_log_plugin”名稱,因此通過 命名找到對應的MethodChannel。
這樣Activity就可以綁定Flutter MethodChannel,可以建立通信通道。
設置MethodCallHandler,注冊插件到Flutter引擎。
channel.setMethodCallHandler(this)
2. Flutter調用Native方法
建立通道以后,Flutter調用Native端方法,方法名為getPlatformVersion,沒有參數。
/*** 方法調用處理** @author zhouronghua* @time 2024/6/27 下午3:12*/override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {if (call.method == "getPlatformVersion") {result.success("Android ${android.os.Build.VERSION.RELEASE}")} else {result.notImplemented()}}
Native端接收方法調用的入口是 onMethodCall
- 首先匹配方法名
- 根據call的參數進行處理
- 返回方法調用結果,通過result保存結果值。
- 如果對應名稱的方法未實現,則設置 result.notImplented()
當前獲取安卓系統版本,返回結果是
"Android ${android.os.Build.VERSION.RELEASE}"
3.Activity與Flutter引擎斷開時注銷插件
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {channel.setMethodCallHandler(null)}
?2.3.3 實現日志打印插件
1.聲明接口方法
在lib插件的接口文件flutter_log_plugin_platform_interface.dart
聲明接口方法 logI
/*** 聲明接口方法** @author zhouronghua* @time 2024/6/27 下午4:05*/void logI(String tag, String message) {throw UnimplementedError('logI() has not been implemented.');}
2. 插件端定義日志打印方法實現 logI
/*** 日志打印:I級別* 說明: 日志打印不需要接收結果,因此不需要異步回調** @author zhouronghua* @time 2024/6/27 下午3:45*/@overridevoid logI(String tag, String message) {/// 調用原生方法logI, 參數集為 {tag, message}/// 參數集按照鍵值對傳遞methodChannel.invokeMethod('logI', {"tag": tag, "message": message});}
調用的Native段方法名為 “logI”,對應的參數為:
{"tag": tag, "message": message}
參數一般使用鍵值對進行傳遞,參數之間采用逗號分隔。
注意,此處一定要使用注解?@override,否則調用logI編譯報錯
3.Native端實現方法接收處理
在android/src.main下,FlutterLogPlugin增加日志方法調用的實現。
/*** 方法調用處理** @author zhouronghua* @time 2024/6/27 下午3:12*/override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {if (call.method == "getPlatformVersion") {// 獲取系統版本信息result.success("Android ${android.os.Build.VERSION.RELEASE}")} else if (call.method == "logI") {// 日志打印處理 logI(參數要與插件的鍵保持一致)final String tag = call.argument("tag")final String message = call.argument("message")android.util.Log.i(tag, message)} else {result.notImplemented()}}
4.Flutter插件入口添加日志打印方法
FlutterLogPlugin中,增加日志打印入口調用
/*** Flutter插件入口* 門面模式** @author zhouronghua* @time 2024/6/27 下午4:11*/
class FlutterLogPlugin {Future<String?> getPlatformVersion() {return FlutterLogPluginPlatform.instance.getPlatformVersion();}/*** 日志打印調用** @author zhouronghua* @time 2024/6/27 下午4:11*/void logI(String tag, String message) {return FlutterLogPluginPlatform.instance.logI(tag, message);}}
這個是典型的門面模式,外部調用的使用不需要關注Flutter插件內部實現細節。
5.測試日志打印方法
在example/lib下,main.dart中,測試日志打印方法調用。
// Platform messages are asynchronous, so we initialize in an async method.Future<void> initPlatformState() async {// 調用日志打印_flutterLogPlugin.logI("MyApp", "開始初始化平臺");String platformVersion;// Platform messages may fail, so we use a try/catch PlatformException.// We also handle the message potentially returning null.try {platformVersion = await _flutterLogPlugin.getPlatformVersion() ??'Unknown platform version';} on PlatformException {platformVersion = 'Failed to get platform version.';}// 調用日志打印_flutterLogPlugin.logI("MyApp", "平臺信息是:$platformVersion");// If the widget was removed from the tree while the asynchronous platform// message was in flight, we want to discard the reply rather than calling// setState to update our non-existent appearance.if (!mounted) return;setState(() {_platformVersion = platformVersion;});}
問題一:編譯報錯logI未實現
還是報錯
Restarted application in 1,896ms.
E/flutter ( 2023): [ERROR:flutter/lib/ui/ui_dart_state.cc(198)] Unhandled Exception: MissingPluginException(No implementation found for method logI on channel flutter_log_plugin)
E/flutter ( 2023): #0 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:165:7)
E/flutter ( 2023): <asynchronous suspension>
E/flutter ( 2023):
E/flutter ( 2023): [ERROR:flutter/lib/ui/ui_dart_state.cc(198)] Unhandled Exception: MissingPluginException(No implementation found for method logI on channel flutter_log_plugin)
E/flutter ( 2023): #0 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:165:7)
E/flutter ( 2023): <asynchronous suspension>
E/flutter ( 2023):
test模塊,flutter_log_plugin_test.dart缺少對應的logI方法的實現,因此報錯。添加
class MockFlutterLogPluginPlatform with MockPlatformInterfaceMixinimplements FlutterLogPluginPlatform {@overrideFuture<String?> getPlatformVersion() => Future.value('42');@overridevoid logI(String tag, String message) {debugPrint("tag=$tag message=$message");}
}
6.打包apk
Terminal中,編譯安卓APK。
$ cd example/android$ gradlew clean$ flutter build apk
如果報錯,修正報錯信息,重新打包試試。
出現下面的信息則編譯成功。
安裝運行APK,看看是否打印日志信息
能夠輸出對應的日志信息。
三、插件原理
Plugin其實就是一個特殊的Package。Flutter Plugin提供Android或者iOS的底層封裝,在Flutter層提供組件功能,使Flutter可以較
方便的調取Native的模塊。很多平臺相關性或者對于Flutter實現起來比較復雜的部分,都可以封裝成Plugin。
3.1?Platform Channel
Platform Channel:
1. Flutter App (Client),通過MethodChannel類向Platform發送調用消息;
2. Android Platform (Host),通過MethodChannel類接收調用消息;
3. iOS Platform (Host),通過FlutterMethodChannel類接收調用消息。
- > PS:消息編解碼器,是JSON格式的二進制序列化,所以調用方法的參數類型必須是可JSON序列化的。
- > PS:方法調用,也可以反向發送調用消息。
3.2 安卓平臺
FlutterActivity,是Android的Plugin管理器,它記錄了所有的Plugin,并將Plugin綁定到FlutterView。?
3.3?理解Platform Channel工作原理
Flutter定義了三種不同類型的Channel,它們分別是
- BasicMessageChannel:用于傳遞字符串和半結構化的信息。
- MethodChannel:用于傳遞方法調用(method invocation)。
- EventChannel: 用于數據流(event streams)的通信。
三種Channel之間互相獨立,各有用途,但它們在設計上卻非常相近。每種Channel均有三個重要成員變量:
- name: ?String類型,代表Channel的名字,也是其唯一標識符。
- messager:BinaryMessenger類型,代表消息信使,是消息的發送與接收的工具。
- codec: MessageCodec類型或MethodCodec類型,代表消息的編解碼器。
- Channel name
? ? 一個Flutter應用中可能存在多個Channel,每個Channel在創建時必須指定一個獨一無二的name,Channel之間使用name來區分彼此。當有消息從Flutter端發送到Platform端時,會根據其傳遞過來的channel name找到該Channel對應的Handler(消息處理器)。
- 消息信使:BinaryMessenger
- 平臺通道數據類型支持和解碼器
- 標準平臺通道使用標準消息編解碼器,以支持簡單的類似JSON值的高效二進制序列化,例如 booleans,numbers, Strings, byte buffers, List, Maps(請參閱StandardMessageCodec了解詳細信息)。 當您發送和接收值時,這些值在消息中的序列化和反序列化會自動進行。
下表顯示了如何在宿主上接收Dart值,反之亦然:
?3.4 解碼器
?
消息解碼器主要將二進制格式的數據轉換為Handler能夠識別的數據,Flutter定義了兩種Codec:
MessageCodec和MethodCodec。
四、插件打包和發布
4.1 插件檢查
一旦完成了 package 的實現,你便可以將其提交到?pub.dev?上,以便其他開發者可以輕松地使用它。
發布你的 package 之前,確保檢查了這幾個文件:pubspec.yaml
、README.md
?和?CHANGELOG.md
,確保它們完整且正確,另外,為了提高 package 的可用性,可以考慮加入如下的內容:
-
代碼的示例用法
-
屏幕截圖,GIF 動畫或者視頻
-
代碼庫的正確指向鏈接
運行 dry-run 命令以檢驗是否所有內容都通過了分析:
$ flutter packages pub publish --dry-run
修正提示錯誤信息。?
pubspec.yaml 中anthor字段不需要了,直接刪除
修改后再次執行。?
Package validation found the following potential issue:
* Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version. If this package needs Dart version 2.17.0-239.0.dev, consider publishing the package as a pre-release instead.See https://dart.dev/tools/pub/publishing#publishing-prereleases For more information on pre-releases.Package has 1 warning.
pub finished with exit code 65
?
4.2 插件發布
最后一步是發布,請注意:發布是永久性?的,運行以下提交命令:
flutter pub publish
設置了中國鏡像的開發者們請注意:目前所存在的鏡像都不能(也不應該)進行 package 的上傳。如果你設置了鏡像,執行上述發布代碼可能會造成發布失敗。網絡設定好后,無需取消中文鏡像,執行下述代碼可直接上傳:
flutter pub publish --server=https://pub.dartlang.org
?
Dart 概覽 | Dart