Flutter桌面開(kāi)發(fā)windows插件開(kāi)發(fā)
前言
通過(guò)此篇文章,你將了解到:
Flutter插件的基本介紹;
windows插件開(kāi)發(fā)的真實(shí)踩坑經(jīng)驗(yàn)。
我們都知道,F(xiàn)lutter的定位更多是作為一個(gè)跨平臺(tái)的UI框架,對(duì)于原生平臺(tái)的功能,開(kāi)發(fā)過(guò)程中經(jīng)常需要插件來(lái)提供。不幸的是Windows的生態(tài)又極其不完整,插件開(kāi)發(fā)必不可少。但網(wǎng)上windows的文章少之又少,所以本篇文章,我們一起來(lái)聊聊插件開(kāi)發(fā)的一些技巧。
插件介紹
Flutter的插件主要分兩種:package和plugin。
- Package是純dart代碼的庫(kù),不涉及原生平臺(tái)的代碼;
- Plugin是原生插件庫(kù),是一種特殊的Package。Plugin需要開(kāi)發(fā)者分別在各原生平臺(tái)實(shí)現(xiàn)對(duì)應(yīng)的能力。
其中Plugin是我們要著重講的,既然是原生平臺(tái)實(shí)現(xiàn),那跟dart層就勢(shì)必需要通訊。Flutter Plugin的通訊主要有:methodChannel、eventChannel、basicMessageChannel。
- MethodChannel:同步調(diào)用的通道,調(diào)用后可以通過(guò)result返回結(jié)果??梢?Native 端主動(dòng)調(diào)用,也可以Flutter主動(dòng)調(diào)用,屬于雙向通信。這種通信方式是我們?nèi)粘i_(kāi)發(fā)中為最常用的方式, 關(guān)鍵點(diǎn)是Native 端的調(diào)用需要在主線程中執(zhí)行。
- EventChannel:異步事件通知的通道,一般是Native端主動(dòng)發(fā)出通知,F(xiàn)lutter接收通信信息。
- BasicMessageChannel:長(zhǎng)鏈接的通道,雙端可以隨時(shí)發(fā)出消息,對(duì)方收到消息后可以使用reply進(jìn)行回復(fù)。一般常用于需要雙向通信可不知道何時(shí)需要發(fā)送的場(chǎng)景。
windows插件編寫
Flutter Android的生態(tài)算是比較完整的,而且網(wǎng)上95%的插件文章,都是以移動(dòng)端為主,對(duì)于不熟悉Windows開(kāi)發(fā)的同學(xué)極度不友好。因此本篇文章我們不講Android端的實(shí)現(xiàn),重點(diǎn)講Windows端的實(shí)踐,不過(guò)我也不是C++技術(shù)棧的,只能淺淺分享我踩過(guò)的坑。
- 如何創(chuàng)建通信通道?
// MethodChannel void XXXPlugin::RegisterWithRegistrar( flutter::PluginRegistrarWindows* registrar) { // 創(chuàng)建一個(gè)MethodChannel auto channel = std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>( registrar->messenger(), "usb_tool", &flutter::StandardMethodCodec::GetInstance()); // 創(chuàng)建插件對(duì)象 auto plugin = std::make_unique<XXXPlugin>(); // 把通道設(shè)置給插件,同時(shí)傳入消息的處理入口 channel->SetMethodCallHandler( [plugin_pointer = plugin.get()](const auto& call, auto result) { plugin_pointer->HandleMethodCall(call, std::move(result)); }); }
// EventChannel // 創(chuàng)建事件流處理對(duì)象 auto eventHandler = std::make_unique< StreamHandlerFunctions<EncodableValue>>( [plugin_pointer = plugin.get()]( const EncodableValue* arguments, std::unique_ptr<EventSink<EncodableValue>>&& events) -> std::unique_ptr<StreamHandlerError<EncodableValue>> { return plugin_pointer->OnListen(arguments, std::move(events)); }, [plugin_pointer = plugin.get()](const EncodableValue* arguments) -> std::unique_ptr<StreamHandlerError<EncodableValue>> { return plugin_pointer->OnCancel(arguments); }); // 創(chuàng)建EventChannel對(duì)象 auto eventChannel = std::make_unique<flutter::EventChannel<flutter::EncodableValue>>( registrar->messenger(), eventChannelName, &flutter::StandardMethodCodec::GetInstance()); // 把通道設(shè)置給插件 eventChannel->SetStreamHandler(std::move(eventHandler));
最后我們還需要把插件注冊(cè)進(jìn)項(xiàng)目中
registrar->AddPlugin(std::move(plugin));
- 如何處理消息? 在上面創(chuàng)建的過(guò)程中,其實(shí)已經(jīng)把處理方法的傳遞給插件了。
// MethodChannel的處理 // result即通信的對(duì)象 void XXXPlugin::HandleMethodCall( const flutter::MethodCall<flutter::EncodableValue>& method_call, std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) { // 匹配通信的接口 if (method_call.method_name().compare("getPlatformVersion") == 0) { std::ostringstream version_stream; version_stream << "Windows "; if (IsWindows10OrGreater()) { version_stream << "10+"; } else if (IsWindows8OrGreater()) { version_stream << "8"; } else if (IsWindows7OrGreater()) { version_stream << "7"; } // 通過(guò)result->Succes回復(fù)消息 result->Success(flutter::EncodableValue(version_stream.str())); } else { result->NotImplemented(); } }
// 主動(dòng)向Flutter端發(fā)送消息 std::unique_ptr < flutter::StreamHandlerError<flutter::EncodableValue>> XXXPlugin::OnListen(const flutter::EncodableValue* arguments, std::unique_ptr<flutter::EventSink<flutter::EncodableValue>>&& events) { // 主動(dòng)發(fā)送 events_.reset(events.release()); return nullptr; } // Flutter取消監(jiān)聽(tīng)時(shí)觸發(fā) std::unique_ptr < flutter::StreamHandlerError<flutter::EncodableValue>> UsbToolPlugin::OnCancel(const flutter::EncodableValue* arguments) { return nullptr; }
BasicMessageChannel我暫時(shí)還沒(méi)有用過(guò),這里就不做記錄了。但是看C++的api,還是很簡(jiǎn)單就能找到的。至于Flutter端的,無(wú)需多言。只要通信層連通了,其他想怎么玩都可以。
Windows插件的一些坑
這是本篇文章的重點(diǎn)。我們都知道Flutter是單線程的機(jī)制,來(lái)到原生平臺(tái)也一樣,Platform是運(yùn)行在Flutter的主線程的,自然是不能做任何耗時(shí)的,不然會(huì)卡住主線程,系統(tǒng)會(huì)把我們認(rèn)為無(wú)響應(yīng)的應(yīng)用,從而殺死應(yīng)用。
我們經(jīng)常會(huì)在使用windows插件時(shí),感覺(jué)點(diǎn)擊卡頓,其實(shí)就是很多插件沒(méi)有做這個(gè)處理,導(dǎo)致事件隊(duì)列等待調(diào)度。這主要是因?yàn)樵趙indows的開(kāi)發(fā)習(xí)慣上,耗時(shí)操作會(huì)丟到子線程異步執(zhí)行,然后主線程如何等待執(zhí)行結(jié)果?使用while一直去查詢是否執(zhí)行完成,這在windows上成為掛起。
不過(guò)一個(gè)有趣的現(xiàn)象是:當(dāng)有耗時(shí)操作的時(shí)候,F(xiàn)lutter的動(dòng)畫是可以流程播放的,但是點(diǎn)擊事件卻卡住了,這時(shí)候C++的同學(xué)就會(huì)扯,你看動(dòng)畫都是流程的,問(wèn)題肯定出在Flutter上?其實(shí)是因?yàn)閯?dòng)畫在Flutter中屬于微任務(wù),它的優(yōu)先級(jí)是高于事件隊(duì)列的。而while也是分配到事件隊(duì)列中,所以動(dòng)畫優(yōu)先執(zhí)行,點(diǎn)擊卻需要一直等到while結(jié)束。
在Android中,為了避免這個(gè)問(wèn)題,我們一般會(huì)使用協(xié)程,把耗時(shí)操作丟給協(xié)程,讓系統(tǒng)幫我們進(jìn)行任務(wù)調(diào)度,通過(guò)await拿到執(zhí)行完之后的結(jié)果,再把結(jié)果返回給dart層。整個(gè)機(jī)制其實(shí)還是保留了flutter的單線程機(jī)制,從而避免了卡頓問(wèn)題。
在Windows端,其實(shí)也有協(xié)程這個(gè)概念,比如WinRT、C++都有提供協(xié)程的能力。但問(wèn)題在于協(xié)程這個(gè)東西,對(duì)于C++來(lái)說(shuō)太新了,同時(shí)C++的歷史包袱實(shí)在太重,到現(xiàn)在還是用著很老版本的庫(kù)。這就導(dǎo)致很多C++的庫(kù)沒(méi)辦法遷移到協(xié)程這種方式,至少在我現(xiàn)在的業(yè)務(wù)中,切換成本極高,幾乎沒(méi)辦法完成。
但問(wèn)題總得解決,目前我們主要使用異步通知的方式,來(lái)解決這個(gè)問(wèn)題。此異步是真異步,非flutter單線程任務(wù)調(diào)度的異步。我們會(huì)把耗時(shí)的操作丟給子線程,但是我們不再通過(guò)while進(jìn)行異步轉(zhuǎn)同步,而是在子線程中,主動(dòng)通過(guò)channel去通知會(huì)Dart層。
if (*method == "getAsync") { async_pipe_stream_->Get(request, std::bind(&XXXPlugin::OnResponse, this, std::placeholders::_1, *uuid)); // 直接返回true,但真正的執(zhí)行結(jié)果再OnResponse中主動(dòng)返回 result->Success(EncodableValue(true)); return; }
在插件的dart代碼中,我們需要主動(dòng)創(chuàng)建一個(gè)MethodChannel的接收器,異步接收到后,通過(guò)執(zhí)行業(yè)務(wù)端傳入的回調(diào)通知回去。
class NativePlugin { static const MethodChannel _channel = MethodChannel('com.open.flutter/xxx/xxx'); static NativePlugin? _instance; // 獲取實(shí)例,單例 static NativePlugin getInstance({String defaultToken = _token}) { _instance ??= NativePlugin._internal(defaultToken); return _instance!; } // 私有命名構(gòu)造函數(shù),做一次初始化 NativePlugin._internal(String defaultToken) { _defaultToken = defaultToken; _channel.setMethodCallHandler((MethodCall call) async { if (call.method == 'onResponse') { final arguments = Map<String, dynamic>.from(call.arguments); // 執(zhí)行業(yè)務(wù)端傳入的回調(diào) await _onResponse(arguments); } }); }
插件的Flutter層需要接收/維護(hù)回調(diào)列表,不過(guò)此方式有隱患,傳入的回調(diào)容易造成閉包問(wèn)題,增加一些內(nèi)存泄露的風(fēng)險(xiǎn);
但是對(duì)于沒(méi)辦法使用協(xié)程的C++插件來(lái)說(shuō),此方案確實(shí)可以解決不少問(wèn)題。
以上就是Flutter桌面開(kāi)發(fā)windows插件開(kāi)發(fā)的詳細(xì)內(nèi)容,更多關(guān)于Flutter windows插件開(kāi)發(fā)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android中View的炸裂特效實(shí)現(xiàn)方法詳解
這篇文章主要介紹了Android中View的炸裂特效實(shí)現(xiàn)方法,涉及Android組件ExplosionField的相關(guān)定義與使用技巧,需要的朋友可以參考下2016-07-07Android標(biāo)題欄最右邊添加按鈕的實(shí)例
這篇文章主要介紹了Android標(biāo)題欄最右邊添加按鈕的實(shí)例的相關(guān)資料,希望通過(guò)本文大家能掌握如何操作,需要的朋友可以參考下2017-09-09Android ScrollView實(shí)現(xiàn)反彈效果的實(shí)例
這篇文章主要介紹了 Android ScrollView實(shí)現(xiàn)反彈效果的實(shí)例的相關(guān)資料,這里自定義scrollview 并實(shí)現(xiàn)反彈效果,需要的朋友可以參考下2017-07-07RecyclerView+PagerSnapHelper實(shí)現(xiàn)抖音首頁(yè)翻頁(yè)的Viewpager效果
這篇文章主要為大家詳細(xì)介紹了RecyclerView+PagerSnapHelper實(shí)現(xiàn)抖音首頁(yè)翻頁(yè)的Viewpager效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-10-10Android SQLite事務(wù)處理結(jié)合Listview列表顯示功能示例
這篇文章主要介紹了Android SQLite事務(wù)處理結(jié)合Listview列表顯示功能,較為詳細(xì)的分析了Android使用sqlite數(shù)據(jù)庫(kù)進(jìn)行事務(wù)操作并結(jié)合Listview進(jìn)行列表顯示的相關(guān)操作技巧,需要的朋友可以參考下2017-07-07AndroidStudio升級(jí)4.1坑(無(wú)法啟動(dòng)、插件plugin不好用、代碼不高亮)
這篇文章主要介紹了AndroidStudio升級(jí)4.1坑(無(wú)法啟動(dòng)、插件plugin不好用、代碼不高亮),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10android實(shí)現(xiàn)session保持簡(jiǎn)要概述及實(shí)現(xiàn)
其實(shí)sesion在瀏覽器和web服務(wù)器直接是通過(guò)一個(gè)叫做name為sessionid的cookie來(lái)傳遞的,所以只要在每次數(shù)據(jù)請(qǐng)求時(shí)保持sessionid是同一個(gè)不變就可以用到web的session了,感興趣的你可以參考下本文或許對(duì)你有所幫助2013-03-03