Flutter Plugin 開發(fā)

對我這段時間Flutter plugin的學(xué)習(xí)開發(fā)做一個記錄色迂。

主要參考官方的文檔。

也可以看看微信的flutter plugin。Fluwx

就我個人理解,flutter即是一個橋梁,構(gòu)建一個可以同時溝通IOS系洛,Android等的通道。

而Plugin就是賦予我們能夠去開發(fā)完善這個橋梁的能力略步,構(gòu)建flutter中所沒有的描扯,但又需要的功能。

如何開始

成功安裝Flutter和dart插件后趟薄,可以new Flutter project绽诚,

New Plugin

選項

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)

image

androidios即是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)的類型可以由下圖:

參數(shù)對照

主動給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:

onTestBack

在需要使用的頁面add該listener

初始化添加該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)如下

image

要注意的是霹琼,在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);
  }

RegistrarFlutterPluginBinding都能拿到對應(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)境惕医。

同樣的方法Flutter-open iOS module進(jìn)入XCode中,需要先成功pod install算色,不然看不到Pods曹锨,或者不能編輯相應(yīng)文件。
ios 項目結(jié)構(gòu)

經(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.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];
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末彤叉,一起剝皮案震驚了整個濱河市庶柿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌秽浇,老刑警劉巖浮庐,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異柬焕,居然都是意外死亡审残,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進(jìn)店門斑举,熙熙樓的掌柜王于貴愁眉苦臉地迎上來搅轿,“玉大人,你說我怎么就攤上這事富玷〗槭保” “怎么了?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵凌彬,是天一觀的道長沸柔。 經(jīng)常有香客問我,道長铲敛,這世上最難降的妖魔是什么褐澎? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮伐蒋,結(jié)果婚禮上工三,老公的妹妹穿的比我還像新娘迁酸。我一直安慰自己,他們只是感情好俭正,可當(dāng)我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布奸鬓。 她就那樣靜靜地躺著,像睡著了一般掸读。 火紅的嫁衣襯著肌膚如雪串远。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天儿惫,我揣著相機(jī)與錄音澡罚,去河邊找鬼。 笑死肾请,一個胖子當(dāng)著我的面吹牛留搔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播铛铁,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼隔显,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了饵逐?” 一聲冷哼從身側(cè)響起荣月,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎梳毙,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捐下,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡账锹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了坷襟。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奸柬。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖婴程,靈堂內(nèi)的尸體忽然破棺而出廓奕,到底是詐尸還是另有隱情,我是刑警寧澤档叔,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布桌粉,位于F島的核電站,受9級特大地震影響衙四,放射性物質(zhì)發(fā)生泄漏铃肯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一传蹈、第九天 我趴在偏房一處隱蔽的房頂上張望押逼。 院中可真熱鬧步藕,春花似錦、人聲如沸挑格。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽漂彤。三九已至雾消,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間显歧,已是汗流浹背仪或。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留士骤,地道東北人范删。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像拷肌,于是被迫代替她去往敵國和親到旦。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容