Flutter 在铭师堂的实践
簡介
Flutter 是 Google 的一套跨平臺(tái) UI 框架。目前已經(jīng)是 1.7 的 Release 版本。在移動(dòng)端雙端投入人力較大,短期緊急需求的背景下。跨端技術(shù)會(huì)成為越來越多的移動(dòng)端技術(shù)棧選擇。銘師堂移動(dòng)端團(tuán)隊(duì)在過去幾個(gè)月,對(duì) Flutter 技術(shù)做了一些嘗試和工作。這篇文章將會(huì)對(duì) Flutter 的基本原理和我們?cè)?升學(xué)e網(wǎng)通 APP 的工程實(shí)踐做一個(gè)簡單的分享。
Flutter 的架構(gòu)和原理
Flutter framework 層的架構(gòu)圖如下:
Foundation: foundation 提供了 framework 經(jīng)常使用的一些基礎(chǔ)類,包括但不限于:
-
BindBase: 提供了提供單例服務(wù)的對(duì)象基類,提供了 Widgets、Render、Gestures等能力
-
Key: 提供了 Flutter 常用的 Key 的基類
-
AbstractNode:表示了控件樹的節(jié)點(diǎn)
在 foundation 之上,Flutter 提供了 動(dòng)畫、繪圖、手勢(shì)、渲染和部件,其中部件就包括我們比較熟悉的 Material 和 Cupertino 風(fēng)格
我們從 dart 的入口處關(guān)注 Flutter 的渲染原理
void runApp(Widget app) {WidgetsFlutterBinding.ensureInitialized()..attachRootWidget(app)..scheduleWarmUpFrame(); } 復(fù)制代碼我們直接使用了 Widgets 層的能力
widgets
負(fù)責(zé)根據(jù)我們 dart 代碼提供的 Widget 樹,來構(gòu)造實(shí)際的虛擬節(jié)點(diǎn)樹
在 FLutter 的渲染機(jī)制中,有 3 個(gè)比較關(guān)鍵的概念:
- Widget: 我們?cè)?dart 中直接編寫的 Widget,表示控件
- Element:實(shí)際構(gòu)建的虛擬節(jié)點(diǎn),所有的節(jié)點(diǎn)構(gòu)造出實(shí)際的控件樹,概念是類似前端經(jīng)常提到的 vitrual dom
- RenderObject: 實(shí)際負(fù)責(zé)控件的視圖工作。包括布局、渲染和圖層合成
根據(jù) attachRootWidget 的流程,我們可以了解到布局樹的構(gòu)造流程
到這里,整顆 tree 的 root 節(jié)點(diǎn)就構(gòu)造出來了,在 mount 中,會(huì)通過 BuildOwner#buildScope 執(zhí)行子節(jié)點(diǎn)的創(chuàng)建和掛載, 這里需要注意的是 child 的 RenderObject 也會(huì)被 attach 到 parent 的 RenderObejct 上去
整個(gè)過程我們可以通過下圖表示
感興趣可以參考 Element、RenderObjectElement、RenderObject 的源碼
渲染
負(fù)責(zé)實(shí)際整個(gè)控件樹 RenderObject 的布局和繪制
runApp 后會(huì)執(zhí)行 scheduleWarmUpFrame 方法,這里就會(huì)開始調(diào)度渲染任務(wù),進(jìn)行每一幀的渲染
從 handleBeginFrame 和 handleDrawFrame 會(huì)走到 binding 的 drawFrame 函數(shù),依次會(huì)調(diào)用 WidgetsBinding 和 RendererBinding 的 drawFrame。
這里會(huì)通過 Element 的 BuildOwner,去重新塑造我們的控件樹。
大致原理如圖
在構(gòu)造或者刷新一顆控件樹的時(shí)候,我們會(huì)把有改動(dòng)部分的 Widget 標(biāo)記為 dirty,并針對(duì)這部分執(zhí)行 rebuild,但是 Flutter 會(huì)有判斷來保證盡量復(fù)用 Element,從而避免了反復(fù)創(chuàng)建 Element 對(duì)象帶來的性能問題。
在對(duì) dirty elements 進(jìn)行處理的時(shí)候,會(huì)對(duì)它進(jìn)行一次排序,排序規(guī)則參考了 element 的深度:
static int _sort(Element a, Element b) {if (a.depth < b.depth)return -1;if (b.depth < a.depth)return 1;if (b.dirty && !a.dirty)return -1;if (a.dirty && !b.dirty)return 1;return 0;} 復(fù)制代碼根據(jù) depth 排序的目的,則是為了保證子控件一定排在父控件的左側(cè), 這樣在 build 的時(shí)候,可以避免對(duì)子 widget 進(jìn)行重復(fù)的 build。
在實(shí)際渲染過程中,Flutter 會(huì)利用 Relayout Boundary機(jī)制
void markNeedsLayout() {// ...if (_relayoutBoundary != this) {markParentNeedsLayout();} else {_needsLayout = true;if (owner != null) {owner._nodesNeedingLayout.add(this);owner.requestVisualUpdate();}}//...} 復(fù)制代碼在設(shè)置了 relayout boundary 的控件中,只有子控件會(huì)被標(biāo)記為 needsLayout,可以保證,刷新子控件的狀態(tài)后,控件樹的處理范圍都在子樹,不會(huì)去重新創(chuàng)建父控件,完全隔離開。
在每一個(gè) RendererBinding 中,存在一個(gè) PipelineOwner 對(duì)象,類似 WidgetsBinding 中的 BuildOwner. BuilderOwner 負(fù)責(zé)控件的build 流程,PipelineOwner 負(fù)責(zé) render tree 的渲染。
void drawFrame() {assert(renderView != null);pipelineOwner.flushLayout();pipelineOwner.flushCompositingBits();pipelineOwner.flushPaint();renderView.compositeFrame(); // this sends the bits to the GPUpipelineOwner.flushSemantics(); // this also sends the semantics to the OS.} 復(fù)制代碼RenderBinding 的 drawFrame 實(shí)際闡明了 render obejct 的渲染流程。即 布局(layout)、繪制(paint)、合成(compositeFrame)
調(diào)度(scheduler和線程模型)
在布局和渲染中,我們會(huì)觀察到 Flutter 擁有一個(gè) SchedulerBinding,在 frame 變化的時(shí)候,提供 callback 進(jìn)行處理。不僅提供了幀變化的調(diào)度,在 SchedulerBinding 中,也提供了 task 的調(diào)度函數(shù)。這里我們就需要了解一下 dart 的異步任務(wù)和線程模型。
dart 的單線程模型,所以在 dart 中,沒有所謂的主線程和子線程說法。dart 的異步操作采取了 event-looper 模型。
dart 沒有線程的概念,但是有一個(gè)概念,叫做 isolate, 每個(gè) isolate 是互相隔離的,不會(huì)進(jìn)行內(nèi)存的共享。在 main isolate 的 main 函數(shù)結(jié)束之后,會(huì)開始一個(gè)個(gè)處理 event queue 中的 event。也就是,dart 是先執(zhí)行完同步代碼后,再進(jìn)行異步代碼的執(zhí)行。所以如果存在非常耗時(shí)的任務(wù),我們可以創(chuàng)建自己的 isolate 去執(zhí)行。
每一個(gè) isolate 中,存在 2 個(gè) event queue
- Event Queue
- Microtask Queue
event-looper 執(zhí)行任務(wù)的順序是
flutter 的異步模型如下圖
Gesture
每一個(gè) GUI 都離不開手勢(shì)/指針的相關(guān)事件處理。
在 GestureBiding 中,在 _handlePointerEvent 函數(shù)中,PointerDownEvent 事件每處理一次,就會(huì)創(chuàng)建一個(gè) HintTest 對(duì)象。在 HintTest 中,會(huì)存有每次經(jīng)過的控件節(jié)點(diǎn)的 path。
最終我們也會(huì)看到一個(gè) dispatchEvent 函數(shù),進(jìn)行事件的分發(fā)以及 handleEvent,對(duì)事件進(jìn)行處理。
在根節(jié)點(diǎn)的 renderview 中,事件會(huì)開始從 hitTest 處理,因?yàn)槲覀兲砑恿耸录膫鬟f路徑,所以,時(shí)間在經(jīng)過每個(gè)節(jié)點(diǎn)的時(shí)候,都會(huì)被”處理“。
// from HitTestDispatchervoid dispatchEvent(PointerEvent event, HitTestResult hitTestResult) {if (hitTestResult == null) {assert(event is PointerHoverEvent || event is PointerAddedEvent || event is PointerRemovedEvent);try {pointerRouter.route(event);} catch (exception, stack) {}return;}for (HitTestEntry entry in hitTestResult.path) {try {entry.target.handleEvent(event, entry);} catch (exception, stack) {}}} 復(fù)制代碼這里我們就可以看出來 Flutter 的時(shí)間順序,從根節(jié)點(diǎn)開始分發(fā),一直到子節(jié)點(diǎn)。同理,時(shí)間處理完后,會(huì)沿著子節(jié)點(diǎn)傳到父節(jié)點(diǎn),最終回到 GestureBinding。 這個(gè)順序其實(shí)和 Android 的 View 事件分發(fā) 和 瀏覽器的事件冒泡 是一樣的。
通過 GestureDector 這個(gè) Widget, 我們可以觸發(fā)和處理各種這樣的事件和手勢(shì)。具體的可以參考 Flutter 文檔。
Material、Cupertino
Flutter 在 Widgets 之上,實(shí)現(xiàn)了兼容 Andorid/iOS 風(fēng)格的設(shè)計(jì)。讓APP 在 ui/ue 上有類原生的體驗(yàn)。
Flutter 的工程實(shí)踐
根據(jù)我們自己的實(shí)踐,我從 混合開發(fā)、基礎(chǔ)庫建設(shè)和日常的采坑的角度,分享一些我們的心得體會(huì)。
混合工程
我們的 APP 主題大部分是 native 開發(fā)完成的。為了實(shí)踐 Flutter,我們就需要把 Flutter 接入到原生的 APP 里面去。并且能滿足如下需求:
- 對(duì)不參與 Flutter 實(shí)踐的原生開發(fā)同學(xué)不產(chǎn)生影響。不需要他們?nèi)グ惭b Flutter 開發(fā)環(huán)境
- 對(duì)于參與 FLutter 的同學(xué)來說,我們要共享一份dart 代碼,即共享一個(gè)代碼倉庫
我們的原生架構(gòu)是多 module 組件化,每個(gè) module 是一個(gè) git 倉庫,使用 google git repo 進(jìn)行管理。以 Android 工程為例,為了對(duì)原生開發(fā)沒有影響。最順理成章的思路就是,提供一個(gè) aar 包。對(duì)于 Android 的視角來說,flutter 其實(shí)只是一個(gè) flutterview,那么我們按照 flutter 的工程結(jié)構(gòu)自己創(chuàng)建一個(gè)相應(yīng)的 module 就好了。
我們查看 flutter create 創(chuàng)建的flutter project的Andorid的 build.gradle,可以找到幾個(gè)關(guān)鍵的地方
app的build.gradle
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"flutter {source '../..' } 復(fù)制代碼這里制定了 flutter 的gradle,并且制定了 flutter 的source 文件目錄。
我們可以猜測(cè)出來,flutter相關(guān)的構(gòu)建和依賴,都是 flutter 的gradle 文件里面幫我們做的。那么在我們自己創(chuàng)建的原生 module 內(nèi)部,也用同樣的方式去組織。就可以了。
同時(shí),我們可以根據(jù)自己的實(shí)際去制定 flutter 的 source 路徑。也通過 repo 將原生的module 和 dart 的lib目錄,分成2個(gè)git倉庫。就完美實(shí)現(xiàn)了代碼的隔離。對(duì)于原生開發(fā)來說,后面的構(gòu)建打包等持續(xù)集成都不會(huì)收到 flutter 的影響。
混合工程的架構(gòu)如下:
混合工程啟動(dòng)和調(diào)試
在一個(gè) flutter 工程中,我們一般是使用 flutter run 命令啟動(dòng)一個(gè) flutter 應(yīng)用。這時(shí)候我們就會(huì)有關(guān)注到:混合工程中,我們進(jìn)入app會(huì)先進(jìn)入原生頁面,如何再進(jìn)入 flutter 頁面。那么我們?nèi)绾问褂脽嶂剌d和調(diào)試功能呢。
熱重載
以 Andorid 為例,我們可以先給 app 進(jìn)行 ./gradlew assembleDebug 打出一個(gè) apk 包。
然后使用
flutter run --use-application-binary {debug apk path} 復(fù)制代碼命令。會(huì)啟動(dòng)我們的原生 app, 進(jìn)入特定的 flutter 入口頁面,命令行會(huì)自動(dòng)出現(xiàn) flutter 的 hot reload。
混合工程調(diào)試
那么我們?nèi)绾芜M(jìn)行 flutter 工程的調(diào)試呢?我們可以通過給原生的端口和移動(dòng)設(shè)備的 Observatory 端口進(jìn)行映射。其實(shí)這個(gè)方法也同樣適用于我們運(yùn)行了一個(gè)純 flutter 應(yīng)用,想通過類似 attach 原生進(jìn)程的方式里面開始斷點(diǎn)。
命令行啟動(dòng)app, 出現(xiàn)flutter 的hotreload 后,我們可以看到
An Observatory debugger and profiler on Android SDK built for x86 is available at: http://127.0.0.1:54946/ 復(fù)制代碼這端。這個(gè)地址,我們可以打開一個(gè)關(guān)于 dart 的性能和運(yùn)行情況的展示頁面。
我們記錄下這個(gè)端口 xxxx
然后通過 adb logcat | grep Observatory 查看手機(jī)的端口,可以看到如下輸出
我們把最后一個(gè)地址輸入到手機(jī)的瀏覽器,可以發(fā)現(xiàn)手機(jī)上也可以打開這個(gè)頁面
我們可以理解成這里是做了一次端口映射,設(shè)備上的端口記錄為 yyyy
在 Android Studio 中,我們?cè)?run -> Edit Configurations 里面,新建一個(gè) dart remote debug, 填寫 xxxx 端口。
如果不成功,可以手動(dòng) forward 一下
adb forward tcp:xxxx tcp:yyyy 復(fù)制代碼然后啟動(dòng)這個(gè)調(diào)試器,就可以進(jìn)行 dart 的斷點(diǎn)調(diào)試了。
原生能力和插件開發(fā)
在 flutter 開發(fā)中,我們需要經(jīng)常使用原生的功能,具體的可以參考 官方文檔, native 和 flutter 通過傳遞消息,來實(shí)現(xiàn)互相調(diào)用。
架構(gòu)圖如下
查看源碼,可以看到 flutter 包括 4 中 Channel 類型。
- BasicMessageChannel 是發(fā)送基本的信息內(nèi)容的通道
- MethodChannel和 OptionalMethodChannel是發(fā)送方法調(diào)用的通道
- EventChannel 是發(fā)送事件流 stream 的通道。
在 Flutter 的封裝中,官方對(duì)純 Flutter 的 library 定義為 Package, 對(duì)調(diào)用了原生能力的 libraray 定義為 Plugin。
官方同時(shí)也提供了 Plugin 工程的腳手架。通過 flutter create --org {pkgname} --template=plugin xx 創(chuàng)建一個(gè) Plugin 工程。內(nèi)部包括三端的 library 代碼,也包括了一個(gè) example 目錄。里面是一個(gè)依賴了此插件的 flutter 應(yīng)用工程。具體可以參考插件文檔
在實(shí)踐中,我們可以發(fā)現(xiàn) Plugin 的依賴關(guān)系如下。 例如我們的 Flutter 應(yīng)用叫 MyApp, 里面依賴了一個(gè) Plugin 叫做 MyPlugin。那么,在 Andorid APP 中,庫依關(guān)系如下圖
但是如果我們?cè)趧?chuàng)建插件工程的時(shí)候,原生部分代碼,不能依賴到插件的原生 aar。這樣每次編譯的時(shí)候就會(huì)在 GeneratedPluginRegistrant 這個(gè)類中報(bào)錯(cuò),依賴關(guān)系就變成了下圖
我們會(huì)發(fā)現(xiàn)紅色虛線部分的依賴在插件工程中是不存在的。
仔細(xì)思考一下會(huì)發(fā)現(xiàn),其實(shí)我們?cè)?Flutter 應(yīng)用工程中使用 Plugin 的時(shí)候,只是在 pubspec.yaml 中添加了插件的依賴。原生部分是怎么依賴到插件的呢?
通過比較 flutter create xx(應(yīng)用工程) 和 flutter create --template=plugin (插件工程) ,我們會(huì)發(fā)現(xiàn)在settings.gradle 中有一些不一樣。應(yīng)用工程中,有如下一段自動(dòng)生成的 gradle 代碼
gradle 會(huì)去讀取一個(gè) .flutter-plugins 文件。從這里面讀取到插件的原生工程地址,include 進(jìn)來并制定了 path。
我們查看一個(gè) .flutter-plugins 文件:
path_provider=/Users/chenglei/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider-1.1.0/復(fù)制代碼我們也可以大致猜測(cè)到,flutter的 gradle 腳本里面會(huì)把自己include進(jìn)來的插件工程全部依賴一遍。
從這個(gè)角度,我們發(fā)現(xiàn)插件工程開發(fā)還是有一些規(guī)則上的限制的。 從開發(fā)的角度看,必須遵循腳手架的規(guī)范編寫代碼。如果依賴其他的插件,必須自己寫腳本解決上面的依賴問題。 從維護(hù)的角度看,插件工程仍然需要至少一個(gè)android 同學(xué) 加一個(gè) iOS 同學(xué)進(jìn)行維護(hù)。
所以我們?cè)谏婕霸?Flutter 基礎(chǔ)庫開發(fā)中,沒有采用原生工程的方式。而是通過獨(dú)立的 fluter package、獨(dú)立的android ios module打二進(jìn)制包的形式。
flutter基礎(chǔ)設(shè)施之路
基于上一小節(jié)的結(jié)論,我們開發(fā)了自己的一套 flutter 基礎(chǔ)設(shè)置。我們的基建大致從下面幾個(gè)角度出發(fā)
- 利用現(xiàn)有能力:基于 Channel 調(diào)用原生的能力,例如網(wǎng)絡(luò)、日志上報(bào)。可以收攏 APP 中這些基礎(chǔ)操作
- 質(zhì)量和穩(wěn)定性:Flutter 是新技術(shù),我們?nèi)绾卧谒暇€的時(shí)候做到心中有底
- 開發(fā)規(guī)范:從早期就定下第一版的代碼結(jié)構(gòu)、技術(shù)棧選擇,對(duì)于后面的演進(jìn)益大于弊
利用現(xiàn)有能力
我們封裝了 Channel,開發(fā)了一個(gè) DartBridge 框架。負(fù)責(zé)原生和 Dart 的互相調(diào)用。在此之上,我們開發(fā)了網(wǎng)絡(luò)庫、統(tǒng)一跳轉(zhuǎn)庫等基礎(chǔ)設(shè)施
DartBridge
反觀 e網(wǎng)通 APP 在 webview 的通信,是在消息到達(dá)另一端后,通過統(tǒng)一的路由調(diào)用格式進(jìn)行路由調(diào)用。對(duì)于路由提供方來說,只識(shí)別路由協(xié)議,不關(guān)心調(diào)用端是哪一段。在一定程度上,我們也可以把統(tǒng)一的路由協(xié)議理解為“跨平臺(tái)”。我們內(nèi)部協(xié)議的格式是如下形式:
scheme://{"domain":"", "action":"", "params":""}
所以在 Flutter 和原生的通信中,結(jié)合實(shí)際業(yè)務(wù)場(chǎng)景,我們沒有使用 MethodChannel,而是使用了 BasicMessageChannel, 通過這一個(gè) channel,發(fā)送最基本的路由協(xié)議。被調(diào)用方收到后,調(diào)用各自的路由庫,返回調(diào)用結(jié)果給通道。我們封裝了一套 DartBridge 來進(jìn)行消息的傳遞。
通過閱讀源碼我們可以發(fā)現(xiàn),Channel 的設(shè)計(jì)非常的完美。它解耦了消息的編解碼方式,在 Codec 對(duì)象中,我們可以進(jìn)行我們的自定義編碼,例如序列化為 json 對(duì)象的 JsonMessageCodec。
var _dartBridgeChannel = BasicMessageChannel(DART_BRIDGE_CHANNEL,JSONMessageCodec()); 復(fù)制代碼在實(shí)際開發(fā)中,我們可能想要查詢消息內(nèi)容。如果消息的內(nèi)容是獲取原生的內(nèi)容,例如一個(gè)學(xué)生的作業(yè)總數(shù),我們希望在原生提供服務(wù)前,不阻塞自己的開發(fā)。并且在不修改業(yè)務(wù)代碼的情況下獲取到路由的mock數(shù)據(jù)。所以我們?cè)诼酚傻膬?nèi)部增加了攔截器和mock服務(wù)的功能。在sdk初始化的時(shí)候,我們可以通過對(duì)象配置的方式,配置一些對(duì)應(yīng) domain、action的mock數(shù)據(jù)。
整個(gè) DartBridge 的架構(gòu)如下
基于這個(gè)架構(gòu)模型,我們收到消息后,通過原生路由(例如 ARouter)方案,去進(jìn)行相應(yīng)的跳轉(zhuǎn)或者服務(wù)調(diào)用。
網(wǎng)絡(luò)庫 EIO
Flutter 提供了自己的http 包。但是集成到原生app的時(shí)候,我們?nèi)匀幌MW(wǎng)絡(luò)這個(gè)基礎(chǔ)操作的口子可以被統(tǒng)一管理。包括統(tǒng)一的https支持,統(tǒng)一的網(wǎng)絡(luò)攔截操作,以及可能進(jìn)行的統(tǒng)一網(wǎng)絡(luò)監(jiān)控和調(diào)優(yōu)。所以在Android中,網(wǎng)絡(luò)庫我們選擇調(diào)用 OKHttp。
但是考慮到如果有新的業(yè)務(wù)需求,我們開發(fā)了一個(gè)全新的flutter app,也希望在不更改框架層的代碼,就可以直接移植過去,并且脫離原生的請(qǐng)求。
這就意味著網(wǎng)絡(luò)架構(gòu)需要把 網(wǎng)絡(luò)配置 和 網(wǎng)絡(luò)引擎 解耦開。本著不重復(fù)造輪子的原則,我們發(fā)現(xiàn)了一個(gè)非常優(yōu)秀的框架:DIO
DIO 留下了一個(gè) HttpClientAdapter 類,進(jìn)行網(wǎng)絡(luò)請(qǐng)求的自定義。
我們實(shí)現(xiàn)了這個(gè)類,在 fetch() 函數(shù)中,通過 DartBridge,對(duì)原生的網(wǎng)絡(luò)請(qǐng)求模塊進(jìn)行調(diào)用。返回的數(shù)據(jù)是一個(gè)包括:
- nativeBytes List 網(wǎng)絡(luò)數(shù)據(jù)的字節(jié)流
- statusCode 網(wǎng)絡(luò)請(qǐng)求的 http code
- headers Map<String, dynamic> 網(wǎng)絡(luò)的 response headers
這些數(shù)據(jù),通過 Okhttp 請(qǐng)求可以獲取。這里有一個(gè)細(xì)節(jié)問題。在 OkHttp 中,請(qǐng)求到的 bytes是一個(gè) byte[], 直接給到dart 這邊,被我強(qiáng)轉(zhuǎn)成了一個(gè)List, 因?yàn)閖ava 中 byte的范圍是 -126 - 127 ,所以這時(shí)候,就出現(xiàn)了亂碼。
通過對(duì)比實(shí)際的dart dio請(qǐng)求到的相同的字節(jié)流,我發(fā)現(xiàn),byte中的一些數(shù)據(jù)轉(zhuǎn)換成int的時(shí)候發(fā)生了溢出,變成了負(fù)數(shù),產(chǎn)生了亂碼。正好是做一次補(bǔ)碼運(yùn)算,就成了正確的。所以。我在 dart 端,對(duì)數(shù)據(jù)做了一次統(tǒng)一的轉(zhuǎn)化:
nativeBytes = nativeBytes.map((it) {if (it < 0) {return it + 256;} else {return it;}}).toList(); 復(fù)制代碼關(guān)于 utf8 和 byte 具體的編解碼過程,我們不做贅述。感興趣的同學(xué)可以參考一下這篇文章
統(tǒng)一路由跳轉(zhuǎn)
在 DartBridge 框架的基礎(chǔ)上,我們對(duì)接原生的路由框架封裝了我們自己的統(tǒng)一跳轉(zhuǎn)。目前我們的架構(gòu)還比較簡單,采用了還是多容器的架構(gòu),在業(yè)務(wù)上去規(guī)避這點(diǎn)。我們的容器頁面其實(shí)就是一個(gè) FlutterActivity,我們給容器也設(shè)置了一個(gè) path,原生在跳轉(zhuǎn)flutter的時(shí)候,其實(shí)是跳轉(zhuǎn)到了這個(gè)容器頁。在容器頁中,拿到我們實(shí)際的 Flutter path 和 參數(shù)。偽代碼如下:
val extra = intent?.extrasextra?.let {val path = it.getString("flutterPath") ?: ""val params = HashMap<String, String>()extra.keySet().forEach { key ->extra[key]?.let { value ->params[key] = value.toString()}}path.isNotEmpty().let {// 參數(shù)通過 bridge 告訴flutter的第一個(gè) widget// 在flutter頁面內(nèi)實(shí)現(xiàn)真正的跳轉(zhuǎn)DartBridge.sendMessage<Boolean>("app", "gotoFlutter",HashMap<String,String>().apply {put("path", path)put("params", params)}, {success->Log.e("native跳轉(zhuǎn)flutter成功", success.toString())}, { code, msg->Log.e("native跳轉(zhuǎn)flutter出錯(cuò)", "code:$code;msg:$msg")})}} 復(fù)制代碼那么,業(yè)務(wù)在原生跳往 Flutter 頁面的時(shí)候,我們每次都需要知道容器頁面的path嗎,很明顯是不能這樣的。 所以我們?cè)谏厦鏀⑹龅幕A(chǔ)上,抽象了一個(gè) flutter 子路由表。進(jìn)行單獨(dú)維護(hù)。 業(yè)務(wù)只需要跳往自己的子路由表內(nèi)的 path,在 SDK內(nèi)部,會(huì)把實(shí)際的path 替換成容器的 path,把路由表 path 和跳轉(zhuǎn)參數(shù)整體作為實(shí)際的參數(shù)。
在 Andorid 中,我提供了一個(gè) pretreatment 函數(shù),在 ARouter 的 PretreatmentService 中調(diào)用進(jìn)行處理。返回最終的路由 path 和 參數(shù)。
質(zhì)量和穩(wěn)定性
線上開關(guān)
為了保證新技術(shù)的穩(wěn)定,在 Flutter 基礎(chǔ) SDK 中,我們提供了一個(gè)全局開關(guān)的配置。這個(gè)開關(guān)目前還是高粒度的,控制在進(jìn)入 Flutter 頁面的時(shí)候是否跳轉(zhuǎn)容器頁。 在開關(guān)處理的初始化中,需要提供 2 個(gè)參數(shù)
- 是否允許線上打開 Flutter 頁面
- 在不能打開 Flutter 頁面的時(shí)候,提供一個(gè) Flutter 和 native 頁面的路由映射表。跳轉(zhuǎn)到對(duì)應(yīng)的原生頁面或者報(bào)錯(cuò)頁。
線上開關(guān)可以和 APP 現(xiàn)有的無線配置中心對(duì)接。如果線上出現(xiàn) Flutter 的質(zhì)量問題。我們可以下發(fā)配置來控制頁面跳轉(zhuǎn)實(shí)現(xiàn)降級(jí)。
異常收集
在原生開發(fā)中,我們會(huì)使用例如 bugly 之類的工具查看線上收集的 crash 異常堆棧。Flutter 我們應(yīng)該怎么做呢?在開發(fā)階段,我們經(jīng)常會(huì)發(fā)現(xiàn) Flutter 出現(xiàn)一個(gè)報(bào)錯(cuò)頁面。 閱讀源碼,我們可以發(fā)現(xiàn)其實(shí)這個(gè)錯(cuò)誤的顯示是一個(gè) Widget:
在 ComponentElement 的 performRebuild 函數(shù)中有如下調(diào)用
在調(diào)用 build 方法 ctach 到異常的時(shí)候,會(huì)返回顯示一個(gè) ErrorWidget。進(jìn)一步查看會(huì)發(fā)現(xiàn),它的 builder 是一個(gè) static 的函數(shù)表達(dá)式。
(FlutterErrorDetails details) => ErrorWidget(details.exception)
它的參數(shù)最終也返回了一個(gè)私有的函數(shù)表達(dá)式 _debugReportException
最終這里會(huì)調(diào)用 onError 函數(shù),可以發(fā)現(xiàn)它也是一個(gè) static 的函數(shù)表達(dá)式
那么對(duì)于異常捕獲,我們只需要重寫下面 2 個(gè)函數(shù)就可以進(jìn)行 build 方法中的視圖報(bào)錯(cuò)
- ErrorWidget.builder
- FlutterError.onError
到這一步,我們進(jìn)行了視圖的異常捕獲。在 dart 的異步操作中拋出的異常又該如何捕獲呢。查詢資料我們得到如下結(jié)論:
在 Flutter 中有一個(gè) Zone 的概念,它代表了當(dāng)前代碼的異步操作的一個(gè)獨(dú)立的環(huán)境。Zone 是可以捕獲、攔截或修改一些代碼行為的
最終,我們的異常收集代碼如下
void main() {runMyApp(); }runMyApp() {ErrorHandler.flutterErrorInit(); // 設(shè)置同步的異常處理需要的內(nèi)容runZoned(() => runApp(MyApp()), // 在 zone 中執(zhí)行 MyAppzoneSpecification: null,onError: (Object obj, StackTrace stack) {// Zone 中的統(tǒng)一異常捕獲ErrorHandler.reportError(obj, stack);}); } 復(fù)制代碼開發(fā)規(guī)范
在開發(fā)初期,我們就內(nèi)部商議定下了我們的 Flutter 開發(fā)規(guī)范。重點(diǎn)在代碼的組織結(jié)構(gòu)和狀態(tài)管理庫。 開發(fā)結(jié)構(gòu)我們考慮到未來有新增多數(shù) Flutter 代碼的可能,我們選擇按照業(yè)務(wù)分模塊管理各自的目錄。
. +-- lib | +-- main.dart | +-- README.md | +-- business | +-- business1 | +-- module1 | +-- business1.dart | +-- store | +-- models | +-- pages | +-- widgets | +-- repositories | +-- common | +-- ui | +-- utils | +--comlib | +-- router | +-- network 復(fù)制代碼在每個(gè)業(yè)務(wù)中,根據(jù)頁面和具體的視圖模塊,分為了 page 和 widgets 的概念。store 中,我們會(huì)存放相關(guān)的狀態(tài)管理。repositories 中我們要求業(yè)務(wù)把各自的邏輯和純異步操作抽象為獨(dú)立的一層。每個(gè)業(yè)務(wù)早期可以維護(hù)一個(gè)自己的 common, 可以在迭代中不停的抽象自己的 pakcage,并沉淀到最終面向每個(gè)人的 comlib。這樣,基本可以保證在迭代中避免大家重復(fù)造輪子導(dǎo)致的代碼冗余混亂。
在狀態(tài)管理的技術(shù)選型上,我們調(diào)研了包括 Bloc、'redux和mobx`。我們的結(jié)論是
- flutter-redux 的概念和設(shè)計(jì)非常的優(yōu)秀,但是適合統(tǒng)一的全局狀態(tài)管理,其實(shí)和組件的分割又有很大的矛盾。在開源方案中,我們發(fā)現(xiàn) fish-redux 很好的解決了這個(gè)問題。
- Bloc 的大致思路其實(shí)和 redux 有很高的相似度。但是功能還是不如 redux 多。
- mobx,代碼簡單,上手快。基本上搞清楚 Observables、Actions和Reactions幾個(gè)概念就可以愉快的開發(fā)。
最終處于上手成本和代碼復(fù)雜度的考慮,我們選擇了 mobx 作為我們的狀態(tài)管理組件。
總結(jié)
到這里,我分享了一些 Flutter 的原理和我們的一些實(shí)踐。希望能和一些正在研究 Flutter 的同學(xué)進(jìn)行交流和學(xué)習(xí)。我們的 Flutter 在基礎(chǔ)設(shè)施開發(fā)的同時(shí),還剝離編寫了一些 升學(xué)e網(wǎng)通 APP 上的頁面和一些基礎(chǔ)的 ui 組件庫。在未來我們會(huì)嘗試在一些老的頁面中,上線 Flutter 版本。并且研究更好的基礎(chǔ)庫、異常收集平臺(tái)、工具鏈優(yōu)化和單容器相關(guān)的內(nèi)容。
轉(zhuǎn)載于:https://juejin.im/post/5d3c341c6fb9a07ecf726ced
總結(jié)
以上是生活随笔為你收集整理的Flutter 在铭师堂的实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 沉思录---Windows Phone软
- 下一篇: 接口批量测试