對我這段時間Flutter plugin的學(xué)習(xí)開發(fā)做一個記錄色迂。
主要參考官方的文檔。
也可以看看微信的flutter plugin。Fluwx
就我個人理解,flutter即是一個橋梁,構(gòu)建一個可以同時溝通IOS系洛,Android等的通道。
而Plugin就是賦予我們能夠去開發(fā)完善這個橋梁的能力略步,構(gòu)建flutter中所沒有的描扯,但又需要的功能。
如何開始
成功安裝Flutter和dart插件后趟薄,可以new Flutter project绽诚,
AndroidX建議勾上,如果使用Kotlin或Swift開發(fā)的話可以勾上Kotlin和Swift竟趾,這樣默認(rèn)生成的Plugin就是使用的相應(yīng)的語言憔购。
項目結(jié)構(gòu)
這里就可以選擇構(gòu)建Plugin項目,F(xiàn)lutter會幫你自動構(gòu)建一個plugin岔帽,包括通道Channel的搭建玫鸟,flutter端,Native端的基礎(chǔ)配置犀勒。
(什么是Channel可以百度看看屎飘,因為都自動生成好了我就不說了)
打開后是下面這樣的項目結(jié)構(gòu)
android和ios即是Native的編寫部分,lib即是flutter的編寫部分贾费。還有example钦购,就是字面上的意思,用來測試你的功能的demo褂萧,在編寫完導(dǎo)入到其他項目后一般情況下這部分內(nèi)容不會影響項目押桃,需要注意的是example中就相當(dāng)于一個項目,同樣包括lib导犹,android唱凯,ios,不要寫錯地方谎痢。
如何編寫flutter端
lib下會有一個基礎(chǔ)文件磕昼,一般內(nèi)容如下:
static MethodChannel _channel =
const MethodChannel('location_service_check')..setMethodCallHandler(_handler);
static Future<String> get platformVersion async {
final String version = await _channel.invokeMethod('getPlatformVersion');
return version;
}
channel即是給橋梁構(gòu)建一個專門的key,對應(yīng)的Native中的节猿。也可以自定義票从,也可以建立新的。
初始的platformVersion方法即是官方給的例子,這里是用來獲取平臺版本峰鄙。其他的方法也可以按他的樣子照葫蘆畫瓢
假如要帶上參數(shù)浸间,可以像下面這樣填寫
static Future<String> testMessage(String testString, bool testBool) async {
Map map = await _channel.invokeMethod("testMessage", {
"testString" : testString,
"testBool" : testBool,
});
return map["success"];
}
Flutter主動與Native主要是通過channel.invokeMethod,
@optionalTypeArgs
Future<T> invokeMethod<T>(String method, [ dynamic arguments ]) {
return _invokeMethod<T>(method, missingOk: false, arguments: arguments);
}
method是與Native所對應(yīng)的名稱,唯一吟榴;arguments是傳一個map发框,其他類型容易導(dǎo)致類型不正確,沒有響應(yīng)等問題煤墙。
Flutter與Native對應(yīng)的類型可以由下圖:
主動給Native發(fā)信息是通過invokeMethod,那么Native主動給flutter發(fā)消息是通過什么接收呢宪拥?
通過setMethodCallHandler 仿野,_channel前的const去掉
static MethodChannel _channel =
const MethodChannel('location_service_check')..setMethodCallHandler(_handler);
static Future<dynamic> _handler(MethodCall methodCall) {
if ("testBack" == methodCall.method) {
// 監(jiān)聽
}
return Future.value(true);
}
方法跟傳過去差不多,也是對應(yīng)method名稱她君,就調(diào)用對應(yīng)方法脚作。
要注意的是,傳過去的時候(invokeMethod)可以接收返回值(需await)缔刹,可以看到我testMessage中也接收了一個Map球涛,Native中通過Resul返回,但僅能返回一次校镐,即原生中是持續(xù)返回的數(shù)據(jù)亿扁,第二次調(diào)用返回就會報錯。Handler中可以接收多次的數(shù)據(jù)鸟廓,即相當(dāng)于一個listener从祝。即可以簡單理解invokeMethod是函數(shù)方法,Handler是一個listener監(jiān)聽引谜。
如何獲取這個Handler獲取的數(shù)據(jù)牍陌,有多種方法,比如Fluwx中是使用Stream(事件流)的方法员咽,具體可以去看看Fluwx的源碼毒涧。
也可以使用自定義listener的方式
/// 監(jiān)聽
abstract class TestListener {
/// 收到Native傳遞的[message]
void onTestBack(String message);
}
/// 測試監(jiān)聽
static final testListeners = List<TestListener>();
static Future<dynamic> _handler(MethodCall methodCall) {
if ("testBack" == methodCall.method) {
// 監(jiān)聽
addTestBack(methodCall.arguments);
}
return Future.value(true);
}
/// [map]即Native傳回來的arguments
static void addTestBack(Map map) {
var message = map["message"];
for (var listener in testListeners) {
listener.onTestBack(message);
}
}
/// 添加該監(jiān)聽[testListener]
static void addTestBackListener(TestListener testListener) {
testListeners.add(testListener);
}
使用該Listener:
在需要使用的頁面add該listener
這種方式可以由自己決定在哪里進(jìn)行監(jiān)聽,什么時候監(jiān)聽贝室,也可以在需要的時候remove監(jiān)聽契讲,而且也不難理解,不需要去使用Stream這種相對比較復(fù)雜的東西档玻。
監(jiān)聽可以實現(xiàn)怀泊,回調(diào)也是可以實現(xiàn)的,只需要稍微改一下方法:
static void testMessage(String testString, Function onSuccess ,Function onError) async {
Future res = _channel.invokeMethod("testMessage",{
"testString" : testString,
});
res.then((args) {
if (args["status"] == "success") {
onSuccess(args["message"]);
} else {
onError(args["errorCode"]);
}
});
}
如何編寫Android原生
項目右鍵有個Flutter-Open Android Model可以進(jìn)入到Android原生中误趴,Android中的項目結(jié)構(gòu)如下
要注意的是霹琼,在location_service_check(flutter項目名稱)下編寫,app是example的原生模塊。假如添加了其他pub插件枣申,也會有對應(yīng)的模塊售葡。
在其下的xxxPlugin文件中進(jìn)行相應(yīng)的編寫,假如不想擠在一個頁面中忠藤,也可以建立新的文件挟伙,但凡是想要使用相關(guān)的registrar,最好在該頁面中進(jìn)行注冊模孩,或者建立新的channel尖阔。
之前的Flutter插件 plugin中注冊使用的是
public static void registerWith(Registrar registrar) {
final MethodChannel channel = new MethodChannel(registrar.messenger(), "location_service_check");
channel.setMethodCallHandler(new LocationServiceCheckPlugin());
}
最新的Flutter插件中默認(rèn)使用的是下面這個,但依舊保留了registerWith方法榨咐,方便之前版本的項目使用介却,所以最好兩塊都寫上一樣的操作
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "location_service_check");
channel.setMethodCallHandler(this);
}
在Registrar或FlutterPluginBinding都能拿到對應(yīng)的上下文Context,
registrar.context()
or
flutterPluginBinding.getApplicationContext()
只有通過這個Context块茁,才能進(jìn)行如startActivity齿坷,getSystemService等,這個context也就是MainActivity的context数焊,對應(yīng)要進(jìn)行原生操作永淌,特別是在原生中進(jìn)行頁面跳轉(zhuǎn)等,這個Context是必不可少的佩耳。
同時遂蛀,獲取Flutter中的信息,也是通過Registrar或FlutterPluginBinding蚕愤,比如獲取Asset中的文件:
registrar.lookupKeyForAsset("name");
以及
flutterPluginBinding.getFlutterAssets().getAssetFilePathByName("name");
除了注冊答恶,就是接收到Flutter的信息進(jìn)行處理,即onMethodCall
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
if ("getPlatformVersion".equals(call.method)) {
result.success("Android " + android.os.Build.VERSION.RELEASE);
} else if ("testMessage".equals(call.method)) {
testMessage(call.arguments, result);
} else {
result.notImplemented();
}
}
call.method即與Flutter中的invokeMethod中的method對應(yīng)萍诱,然后調(diào)用對應(yīng)的方法悬嗓。
private void testMessage(Object args, Result result) {
JSONObject map = (JSONObject) args;
try {
String testMessage = map.getString("testString");
Map<String, Object> resMap = new HashMap<>();
resMap.put("status", "success");
resMap.put("message", "我收到了:" + testMessage);
// resMap.put("status", "error");
// resMap.put("errorCode", "出現(xiàn)錯誤了");
// 主動返回數(shù)據(jù)給Flutter
channel.invokeMethod("testBack", resMap);
// 返回數(shù)據(jù),也有error方法裕坊,但一般只是用success進(jìn)行處理也夠了
result.success(resMap);
} catch (JSONException e) {
e.printStackTrace();
}
}
返回數(shù)據(jù)使用result.succes() 包竹,主動給Flutter傳數(shù)據(jù)是使用channel.invokeMethod(),注意區(qū)別籍凝。
result是該方法下的周瞎,F(xiàn)lutter調(diào)用哪個方法,就返回數(shù)據(jù)到該方法下饵蒂,僅能返回一次声诸,但不是必須的。
invokeMethod即跟Flutter中的類似退盯,是基于通道channel的彼乌,可以在任何時間地點使用泻肯,沒有次數(shù)限制,只要channel正確慰照。即監(jiān)聽的主要獲取參數(shù)方法(也有EventMethod方法灶挟,但感覺沒那么方便)。
至此毒租,一個簡單的Flutter與Android的Plugin就完成了稚铣。其他原生操作與以上功能結(jié)合即可能實現(xiàn)相應(yīng)的原生功能。
如何編寫iOS原生
要編寫ios墅垮,首先需要macOS系統(tǒng)并配置好相應(yīng)的環(huán)境惕医。
經(jīng)過Pods下對應(yīng)文件夾后一大串的目錄即可找到相應(yīng)的文件剃允。
根據(jù)創(chuàng)建是選項生成OC或Swift文件及相關(guān)內(nèi)容,同理可以建立其他文件來構(gòu)建想要的結(jié)構(gòu)齐鲤,但不要忘了相應(yīng)的注冊斥废。
主要.m文件:
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
FlutterMethodChannel* channel = [FlutterMethodChannel
methodChannelWithName:@"location_service_check"
binaryMessenger:[registrar messenger]];
LocationServiceCheckPlugin* instance = [[LocationServiceCheckPlugin alloc] init];
[registrar addMethodCallDelegate:instance channel:channel];
}
與Android同理,也是通過registrar進(jìn)行相關(guān)原生的操作或者獲取Flutter中的信息给郊。
在handleMethodCall接收Flutter傳送的數(shù)據(jù)
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([@"getPlatformVersion" isEqualToString:call.method]) {
result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
} else if ([@"testMessage" isEqualToString:call.method]) {
[self checkLocationIsOpen:result];
} else {
result(FlutterMethodNotImplemented);
}
}
invokeMethod與result與Android端同理(channel要改一下以獲取到)
- (void)testMessage:(NSDictionary*)args result:(FlutterResult)result {
NSString *testString = args[@"testString"];
NSMutableDictionary *resArgs = [NSMutableDictionary dictionary];
resArgs[@"status"] = @"success";
resArgs[@"message"] = [@"我收到了:" stringByAppendingString:testString];
//resArgs[@"status"] = @"error";
//resArgs[@"errorCode"] = @"出現(xiàn)錯誤了";
//主動返回數(shù)據(jù)給Flutter
[channel invokeMethod:@"testBack" arguments:resArgs];
//返回數(shù)據(jù)
result(resArgs);
}
static FlutterMethodChannel *channel = nil;
channel = [FlutterMethodChannel
methodChannelWithName:@"location_service_check"
binaryMessenger:[registrar messenger]];
至此牡肉,一個基本的Plugin項目就完成了∠牛可以在example的lib編寫個demo調(diào)用測試统锤。
最后
Plugin的搭建并不困難,主要的Native的統(tǒng)一炭庙,即Android與iOS端的數(shù)據(jù)類型的統(tǒng)一饲窿,盡量保持一致,假如無法一致焕蹄,或者說一端可以實現(xiàn)逾雄,另一端無法實現(xiàn),該以怎樣的方式處理腻脏。
導(dǎo)入項目方法鸦泳,可以git或者本地:
location_service_check:
git:
url: https://github.com/...
location_service_check:
path: ../location_service_check
(../是與項目同一路徑下,./是項目下)
以上主要是個人的實踐理解永品,所以不講述原理這一塊做鹰,想了解更多的可以去看看官方文檔,或者搜一下鼎姐。
補(bǔ)充 - 2020.10.12
iOS端相關(guān)
FlutterPluginRegistrar跟Android端一樣同樣重要钾麸,也有l(wèi)ookupKeyForAsset方法更振。
若是想要在plugin中使用UIApplication相關(guān)的系統(tǒng)方法,可以調(diào)用
addApplicationDelegate方法
[registrar addApplicationDelegate:instance];
podspecs Plugin 常用說明
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = 'sharesdk_plugin'
s.version = '1.1.2'
s.summary = 'Flutter plugin for ShareSDK.'
s.description = <<-DESC
ShareSDK is the most comprehensive Social SDK in the world,which share easily with 40+ platforms.
DESC
s.homepage = 'http://www.mob.com/mobService/sharesdk'
s.license = { :file => '../LICENSE' }
s.author = { 'Mob' => 'mobproduct@mob.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.public_header_files = 'Classes/**/*.h'
s.dependency 'Flutter'
s.dependency 'mob_sharesdk', '4.3.8'
s.dependency 'mob_sharesdk/ShareSDKExtension'
s.dependency 'mob_sharesdk/ShareSDKUI'
s.dependency 'mob_sharesdk/ShareSDKPlatforms/WeChat_Lite'
s.dependency 'mob_sharesdk/ShareSDKPlatforms/Facebook_Lite'
s.dependency 'mob_sharesdk/ShareSDKPlatforms/WhatsApp'
#分享閉環(huán)
s.dependency 'mob_sharesdk/ShareSDKRestoreScene'
s.static_framework = true
s.ios.deployment_target = '8.0'
end
以上是項目創(chuàng)建后自動生成的podspec
podspec是對pod的配置喂走,每個plugin在編寫完后都是以第三方庫pods的形式提供項目使用殃饿。
常用的配置:
s.platform: 指定平臺,以及版本
s.dependency: 指定依賴庫(API芋肠、SDK)使用乎芳,常見三種方式
- s.dependency 'Flutter'
- s.dependency 'Flutter', '~> 1.20.2'
- s.dependency ‘Flutter‘, :git => ‘https://github.com/flutter/flutter.git‘, :tag => ‘master‘
s.static_framework: 指定Pods為靜態(tài)庫模式
動態(tài)庫相比靜態(tài)庫,減少了app可執(zhí)行文件的大小.并且可以只在使用時,按需加載而不是在啟動時加載.這個特性減低了啟動時間,并且更優(yōu)秀的利用了內(nèi)存.
動態(tài)庫不能依賴靜態(tài)庫
靜態(tài)庫更加穩(wěn)定,但占用內(nèi)存空間帖池;動態(tài)庫共享代碼節(jié)約空間奈惑,不過很容易導(dǎo)致編譯錯誤,較難定位與修復(fù)睡汹。
當(dāng)podfile中使用了uses_framework! 時肴甸,可以使用s.static_framework = true支持靜態(tài)框架。
s.resource_bundles: 指定資源路徑
s.resource_bundles= {`
'TestBundle' => ['Classes/Resources/Assets/*']
}
'TestBundle' 為顯示的資源的bundle名字囚巴,可自定原在,后面為指定文件目錄。
設(shè)置成功即可在pods中通過Bundle調(diào)用Pods中的資源
NSString* key = [_registrar lookupKeyForAsset:@"icons/test.png"];
NSString* path = [[NSBundle mainBundle] pathForResource:key ofType:nil];
NSLog(@"%@", path);
或
NSString *imgName = [NSString stringWithFormat:@"%@/%@", @"TestBundle.bundle",@"test"];
imgView.image = [UIImage imageNamed:imgName];