乾貨 | 攜程火車票Flutter最佳實踐

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"背景"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在競爭激烈的移動時代,各大互聯網公司都在爭相搶奪市場,如何提高研發效率,快速迭代產品成爲非常重要的因素。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"跨平臺方案能夠節約一定開發、測試、運維成本。Flutter是由谷歌開源的跨平臺框架,可以快速在 iOS 和 Android 上構建高質量的原生用戶界面。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一、 爲什麼選擇Flutter"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"攜程在已經引入了 React Native 的情況下,爲什麼還會選擇 Flutter?更多是對性能的考慮。開發效率與性能體驗就像天平兩端,需要找到一個平衡點。RN 能夠滿足我們絕大部分的業務,並且熱更、版本控制都很靈活。但是在複雜頁面上,特別是在長列表的渲染上,還是存在一定的問題,促使我們去嘗試一些新的解決方案。Flutter官宣自繪UI引擎,採用原生方式做渲染,媲美原生體驗。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Native 、React Native、Flutter 對比如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":"br"}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/2b\/2b9e123a731b6cd390d637c44dfc83dd.jpeg","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.1 研發效率"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter具有跨平臺性,可以在多端上運行。同時Dart語言作爲開發語言,本身的優勢就在於它既支持JIT,又支持AOT,在 JIT(Just In Time)即時編譯功能下,能提供 Hot Reload 功能。在開發過程中,實時地看到界面改動。生產包AOT編譯,將代碼編譯成 ARM 二進制,從而既可以享受運行時又具有原生語言相近的運行效率。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/f3\/f3883975596ca69122ea0bc437580162.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.2 擴展性好"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter提供了多種不同的Channel,用於 Dart 和平臺之間相互通信。通過這些橋方法,使Flutter具有很好地與 Native 和 React Native 進行混合編程的能力。賦予 Flutter 一些 Native 的能力,同時也能很好地讓我們在現有 Native 項目混合Flutter開發。 "}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"二、 Provider對MVVM架構的實踐"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Flutter的開發過程中,特別是一些業務複雜的頁面,爲了代碼結構清晰,模塊邏輯解耦,我們一般採用的是模塊化的編程思想。隨之而來的問題就是,組件之間怎麼相互通訊,比如變更了登錄態,如何通知其他模塊刷新?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"推薦使用Provider來管理各個組件的狀態,我們實踐下來 ,主體佈局採用MVVM模式是比較方便做模塊化編程的。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.1 爲什麼需要使用Provider"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果狀態是該組件私有的,則應該由組件自己管理;但是如果狀態要跨組件共享,則該狀態應該由各個組件共同的父元素來管理。對於組件私有的狀態很好理解,當需要刷新當前widget的時候,只需要通過setState()的方法來實現組件重繪的效果;對於跨組件共享的狀態,可以使用EventBus來實現。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可是當事件多了的時候,難以正確管理,其次訂閱者必須要顯式註冊狀態改變回調,也必須在組件銷燬的時候手動解綁以避免內存泄漏。而Provider就可以通過自身的原理,簡單地去實現狀態共享,不需要麻煩的操作。且Provider是官方推薦的狀態管理方式,具有良好的生態環境及維護團隊。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.2 Provider的實現原理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"1)  InheritedWidget簡單介紹"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Provider是基於InheritedWidget的再次封裝,InheritedWidget提供了一種數據在Widget樹中自上而下傳遞,共享的方式。我們在根Widget繼承了InheritedWidget,然後在該組件中存放一個數據data,那麼可以在任意子Widget中來獲取該組件的數據並使用。當在任一組件中改變了共享數據data,InheritedWidget組件會自上而下通知所有使用過共享數據的組件並刷新組件,同時會回調didChangeDependencies() 方法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"2)  Provider的原理和流程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/d8\/d818c50ec76dbcff1af934cccb737eba.jpeg","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"共享數據的Model變化後,會自動通知ChangeNotifierProvider,ChangeNotifierProvider內部會重新構建InheritedWidget,而依賴該InheritedWidget的子Widget就會更新。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.3 Provider的使用方式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"架構模式圖如下:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/d0\/d0538c2607b0099cc2fb5cab00287fd6.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)創建業務ViewModel,在ViewModel內部存放需要共享的數據。ViewModel 繼承Flutter SDK中提供的ChangeNotifier類,它繼承Listenable,也實現了一個Flutter風格的訂閱者模式,其內部實現了addListener(),removeListener()等方法,實現對訂閱者的處理。同時最好複寫dispose()和notifyListeners()方法,防止用戶在調用數據時銷燬界面,而等到數據獲取到以後通知界面刷新導致Crash。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)註冊狀態管理類,使用ChangeNotifierProvider或者MutiProvider將需要共享數據的Widget包起來,單個NotifierProvider時使用ChangeNotifierProvider,多個NotifierProvider時使用MutiProvider包裝,如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n\/\/\/多個NotifierProvider的時候\nreturn MultiProvider(providers: [\n ChangeNotifierProvider(create: (context) => dataViewModel(mCommonAdvancedFilterRoot,query)),\n ChangeNotifierProvider(create: (context) => UserPreferentialViewModel(query)),\n ChangeNotifierProvider(create: (context) => UserPromotionViewModel())\n\/\/\/需要調用共享數據的子Widget\n], child: ListResearchPageful(query));"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3)在被包起來的Widget中的任一子組件中獲取共享數據ViewModel,可以在StatefulWidget中的builder()方法中獲取,也可以使用Builder組件進行獲取,如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n\/\/\/在StatefulWidget中的build()方法中獲取ViewModel\nclass ListResearchPageState extends TripState {\n@override\n Widget build(BuildContext context) {\n\/\/\/使用Provider包裝以後,可以在widget的任一一個子widget獲取共享數據並操作數據,在這裏就是可以在HotelListView方法下的唯一位置獲取ViewModel\n var listViewModel = Provider.of(context);\n var userPromotionViewModel = Provider.of(context);\n return MediaQuery(\n child: QueryListPage(widget.query, \n ListDataViewModel, userPromotionViewModel));\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n\/\/\/借用Builder組件進行獲取ViewModel\n@override\nWidget build(BuildContext context) {\n\/\/\/使用Provider包裝以後,可以在widget的任一一個子widget獲取共享數據並操作數據,在這裏就是可以在ListView方法下的唯一位置獲取ListDataViewModel\n var userPromotionViewModel = Provider.of(context);\n return MediaQuery(\n child: Builder(builder: (context) {\nvar listDataViewModel = Provider.of(context);\n return queryListPage(\nwidget.query, listDataViewModel, userPromotionViewModel);\n },));\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4)獲取到ViewModel後,可以在子組件中直接使用viewmodel中的共享數據,如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n\/\/領券監聽\n\/\/\/此處可以直接使用viewModel調用viewmodel中的方法\nEvent.addEventListener( \n\"UPDATE_QUERY_RESULT_LIST\",(eventName, eventData) {\n if (isOnPause) {\n listViewModel.isNeedRefresh = true;\n listViewModel.refreshListData(listViewModel.query);\n } else {\n listViewModel.refreshListData(listViewModel.query);\n }\n});"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.4 Provider的優勢"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)我們的業務代碼更專注數據,只要更新Model,UI就會自動更新,不用在狀態改變後再去手動調用setState()來顯示更新頁面。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)數據改變的消息傳遞被屏蔽時,我們無需手動去處理狀態改變事件的發佈和訂閱,provider自行處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3)在大型複雜應用中,尤其是需要全局共享的狀態非常多時,使用Provider將會大大簡化代碼邏輯,降低出錯的概率,提高開發效率。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"三、Flutter 性能調優"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個新技術改造完成,我們最關注的當然是性能體驗有沒有達到預期。那Flutter頁面性能評判標準是什麼,如何去度量,有沒有可視化工具,幫我們去做一些性能調優。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.1 Flutter渲染原理簡介"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在做性能優化之前,先讓我們瞭解一下渲染的原理。Flutter的一切皆爲Widget。爲了性能又區分了 "},{"type":"link","attrs":{"href":"https:\/\/api.flutter-io.cn\/flutter\/widgets\/StatefulWidget-class.html","title":null,"type":null},"content":[{"type":"text","text":"StatefulWidget"}]},{"type":"text","text":","},{"type":"link","attrs":{"href":"https:\/\/api.flutter-io.cn\/flutter\/widgets\/StatelessWidget-class.html","title":null,"type":null},"content":[{"type":"text","text":"StatelessWidget"}]},{"type":"text","text":"。StatefulWidget 能通過"},{"type":"link","attrs":{"href":"https:\/\/api.flutter-io.cn\/flutter\/widgets\/State\/setState.html","title":null,"type":null},"content":[{"type":"text","text":"setState()"}]},{"type":"text","text":"來實現刷新。這樣的設計方便我們去控制局部刷新,從而提高性能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter 中的控件會歷 Widget -> Element -> RenderObject -> Layer 這樣的變化過程,而其中 Layer 的組成由 RenderObject 中的 isRepaintBoundary 標誌位決定。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當調用 setState() 時,RenderObject 就會往上的父節點去查找,根據 isRepaintBoundary是否爲 true,會決定是否從這裏開始往下去觸發重繪,來確定要更新哪些區域。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.2 構建運行Profile模式 "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter 支持三種模式編譯 app,Debug模式、Release模式和Profile模式。Debug 模式 採用JIT編譯,支持HotReload,所以在Debug模式下會放大性能問題。性能分析需要確保使用真機並在profile模式下運行,這樣拿到的數據是最接近真實性能的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)Debug 模式對應 Dart 的 JIT 模式,可以在真機和模擬器上運行。該模式會打開所有的斷言,以及所有的調試信息、服務擴展和調試輔助。此外,該模式支持有狀態的 Hot reload。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)Release 模式對應 Dart 的 AOT 模式,只能在真機上運行,不能在模擬器上運行,其編譯目標爲最終的線上發佈。該模式會關閉所有的斷言,以及儘可能多的調試信息、服務擴展和調試輔助。此外,該模式優化了應用快速啓動、代碼快速執行,以及二級制包大小。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3)Profile 模式,基本與 Release 模式一致,只是多了對 Profile 模式的服務擴展的支持,包括支持跟蹤,以及一些爲了最低限度支持所需要的依賴。該模式用於分析真實設備實際運行性能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"純 Flutter 項目構建 Profile 模式"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"flutter run —profile 命令是使用 Profile 模式來編譯的。IDE 也是支持這個模式的,例如 Android Studio 提供了 Run > Profile… 菜單選項。"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter 與 Native 混合項目構建 Profile 模式"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"a. 打包Flutter工程Profile產物"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n\/\/ 進入flutter項目,執行build-release,並指定輸出目錄 tripflutter\nbuild-release -o \/projects\/ctrip_flutter\/release -i info"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"b. 配置Native項目"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"打包好flutter產物之後,需要導入到native項目並打包。修改Native項目根目錄的gradle.properties文件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n### 開啓Profile模式 \nTRIP_FLUTTER_PROFILE=true \n### 設置profile模式下js使用的產物目錄(過程1構建的 .\/profile 目錄)\nTRIP_FLUTTER_LOCAL_OUTPUTS_PATH=\/projects\/ctrip_flutter\/profile"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"c. 構建Native工程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"直接通過IDE運行到手機上。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.3 性能分析工具及方法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)performance overlay "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"平時常用的性能分析工具有performance overlay,通過它可以直觀看到當前幀的耗時。在Profile模式下,通過Android Studio 看頁面的FPS,注意需要在HotReload 連接的情況下查看。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"選中 View > Tool Windows > Flutter Performance。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/46\/469d3740d2918a51280f8da4b133499e.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"點擊上面圖中的箭頭所指的按鈕,就會在手機或模擬器中打開(如下圖所示)。FPS是一個動態過程,頁面滑動這個值是一直變化的,最右邊的是當前幀。出現紅色則表示耗時超過16.6ms,也就是發生丟幀現象,也是我們常說的頁面閃動問題。performance overlay的主要功能如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"獲取FPS數值來衡量頁面性能,方便對比Flutter、Native頁面幀率;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"直觀統計頁面在各個機型上面的表現;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"定位頁面的具體哪個模塊有問題;"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/17\/1725071872a0df58e6da7782d2f2c6b4.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)Dart DevTool"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另一個工具是Dart DevTool ,在Android studio右側,還可以從Flutter inspector裏面的more action,以及Flutter Performance底部的入口進入。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前DevTools支持的功能有如下一些:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"檢查和分析應用程序的UI佈局和狀態。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"診斷應用的UI 性能問題。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"檢測和分析應用程序的CPU使用情況。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"分析應用程序的網絡使用情況。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter或Dart應用程序的源代碼級調試。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"調試Flutter或Dart應用程序的內存使用情況和分析內存問題。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"查看運行的Flutter或Dart應用程序的一般日誌和診斷信息。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.4 實戰性能技巧"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)懶加載ListView"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"推薦使用ListView.builder()構建List,這樣當Item滾入屏幕時才創建Item,而不是ListView-children,這樣會立刻創建所有的Item。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n\/\/\/Bad code 不推薦使用children 構建List\nListView(children: getItems(mList))\nList getItems(List mList){\n List items=new List();\n if(null!=mList){\nfor(Node node in mList){\nitems.add(Text(\"不推薦寫法\"));\n}\n }\n return items;\n}\n\n\/\/\/推薦寫法\nListView.builder(\n\/\/ physics: NeverScrollableScrollPhysics(),\n\/\/shrinkWrap: true,\nitemCount:mList.length,\nitemBuilder: (BuildContext context, int index) {\nreturn Text(\"推薦使用ListView.builder()\");\n})\n)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意,無論是ListView還是GridView,只要是設置了shrinkWrap: true屬性,都沒有了懶加載的效果了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)控制刷新範圍與次數"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"儘量避免在滑動監聽中觸發setStat()刷新視圖。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如上圖所示,需要滑動的過程中,顯示、隱藏標題欄,並且是一個漸變的過程,遇到這種情況,一定要儘量的控制刷新的範圍和頻次。控制在只在頭圖可見的情況下面觸發setStat(),避免不必要的頁面滑動觸發刷新。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nscrollController.addListener(() {\nif (scrollController.offset > scrollHeight && titleAlpha != 255) {\n setState(() {\ntitleAlpha = 255;\n });\n }\n\nif (scrollController.offset <= 0 && titleAlpha != 0) {\n setState(() {\ntitleAlpha = 0;\n });\n }\n\nif (scrollController.offset > 0 && scrollController.offset < scrollHeight) {\n setState(() {\ntitleAlpha = scrollController.offset * 255 ~\/ scrollHeight;\n });\n }\n});"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"儘量將setStat()放在放置於視圖樹的低層級,好處是build時影響範圍極小,簡稱局部刷新。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如上圖所示在列表中 Item 中存在大量的倒計時。一定要控制刷新倒計時隻影響控件本身,並且只有可視的區域視圖是在刷新的,不可見的情況下及時銷燬計時器。一直刷整個列表,性能開銷是恐怖的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nWidget build(BuildContext context) {\nreturn Text(timeRemaining,\n style: TextStyle(\n color: HotelColors.hotel_list_reduction_sale_color,\n fontSize: 10,\n fontWeight: FontUtil.mediumWeight));\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3)避免組件重複創建"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"能複用的組件儘量複用,特別是在組件化編程,頁面級的情況下面,每次刷新頁面把所有的子組件都重新渲染一遍,性能開銷也是很大的。儘量複用,避免不必要的視圖創建。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n\/\/\/存放界面所有的widgets,用以緩存\nList widgets = new List();\n\/\/\/因爲頭部佈局是靜態的不刷新,使用變量控制是否複用以前的widgets\nvar refreshPage = true;\n\/\/\/獲取界面佈局所有的widgets\nList getPageWidgets(ScriptDataEntity data) {\nif(widgets.isNotEmpty && !refreshPage) {\n return widgets;\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"四、Flutter 佈局技巧"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.1 Flutter 不可見組件預加載"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter 一些組件基本都是有懶加載的,不可見的組件是沒有渲染視圖的,這樣滑動過去,有用到網絡圖片的地方,經常會先白一下。針對這種情況我們對將要加載的圖片進行預加載處理,比如列表頁在分頁請求數據回來的時候做圖片預加載。還有,下一個頁面的圖片,需要一進去就有圖片直接顯示,就可以在當前頁面做圖片預加載。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代碼如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n\/\/\/對每一頁加載的數據進行做圖片預加載\n(hotelListViewModel.currentPageHotels ?? []).forEach((element) {\nvar logo = element?.logo ?? \"\";\n if (StringUtil.isNotEmpty(logo)) {\n precacheImage(NetworkImage(logo), context);\n }\n});"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當數據出來後使用PreChcheImage()預加載處理圖片鏈接,以保證當用戶滑動圖片以後不會看到圖片加載白屏這種問題。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.2 Flutter 數據預加載"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了縮短用戶的加載等待時長,我們經常需要一些預加載方法。比如在前一個頁面預加載下一個頁面的數據,或者在長列表的分頁請求時候,可以做分頁預加載。比如當你滑動到第五個可見的時候,就提前把下一頁的數據加載好。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"列表頁通過橋方法獲取上一個頁面預加載的數據,這樣就能有一個直出體驗,這裏要考慮數據已經加載好、加載中、加載失敗的情況。同時還要考慮,緩存數據的時效性,什麼情況下需要刪除緩存。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n\/\/\/請求列表數據數據\nvoid loadListData(HotelQuery query) {\n\/\/\/在首頁提前獲取列表頁的數據並緩存到本地,當用戶進入列表頁時可以直接展示數據\n if (resultModel != null) {\n \/\/\/判斷是否需要再次請求數據\n _dealWithResult(resultModel);\n return;\n } else if (isPreloading) {\n \/\/\/通過橋方法獲取首頁已經緩存的數據 HotelBridge.getListCache({'queryModel':query.toJson()})\n .then((resp) {\n final newResultModel = \n QueryResultModel.fromJson(resp);\n \/\/\/有緩存數據直接處理使用\n _dealWithResult(newResultModel);\n }).catchError((error) {\n \/\/\/沒有數據採取請求列表頁的數據\n getHotelList();\n });\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.3 佈局自適應高度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果需要根據內容填充的高度來自適應左邊圖片的高度,目前Flutter並不支持該功能,我們可以藉助IntrinsicHeight組件來完美地解決該問題。InstrinsicHeight可以讓同一行的子widget都是相同的高度。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以將需要自適應高度的Widget使用ConstrainedBox進行包裹,並設置最低高度;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將圖片作爲Container的背景圖片,使用DecorationImage進行修飾當前的Container;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將圖片的填充方式設置爲BoxFit.Cover或者fillHeight即可;"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"五、Flutter 中常見問題分析及解決方案"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.1 設置State引起的問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)錯誤展示信息:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"NoSuchMethodError: The method  markNeedsBuild  was called on null。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)錯誤分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個錯誤一般情況下出現在異步任務,比如一些界面請求網絡數據,異步獲取本地數據等,需要根據數據的狀態來改變刷新Widget State。異步任務結束在頁面被銷燬之後,沒有檢查State是否還是mounted狀態,繼續setState()就會出現這個錯誤。錯誤代碼如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n\/\/\/從服務器端獲取當前活動終止時間,當服務器返回以後,會通知刷新這裏\n\/\/\/如果用戶在數據返回之前銷燬該界面,等數據回來以後刷新界面就會報錯\nfinal endTime = roomDetailItemEntity?.tonightEndTime ?? '';\nint endTimeOfNum = 0;\nif (endTime.isNotEmpty) {\n try {\n endTimeOfNum = int.parse(endTime) ?? 0;\n if(endTimeOfNum - Util.currentTimeMillis() > 0) {\n this.setState(() {\n _showCountDown = true;\n });\n }\n } catch (e) {}\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3)處理辦法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在調用setState()方法之前檢查是否mounted,mounted是一個標示當前Widget樹是否已經被渲染的狀態值。所以mounted檢查很重要,只要涉及到異步還有各種回調的時候,都不能忘記檢查該值。如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nfinal endTime = roomDetailItemEntity?.tonightEndTime ?? '';\nint endTimeOfNum = 0;\nif (endTime.isNotEmpty) {\n try {\n endTimeOfNum = int.parse(endTime) ?? 0;\n if(endTimeOfNum - Util.currentTimeMillis() > 0) {\n if(mounted) {\n this.setState(() {\n _showCountDown = true;\n });}}} catch (e) {}\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.2 使用MediaQuery.of()動態獲取屏幕屬性的問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)錯誤展示信息"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"BoxConstraints has a negative minimum width;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":"br"}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)錯誤分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種情況一般出現在需要獲取屏幕寬度,根據屏幕寬度減去另外一個組件的寬度,用來設置另外一個組件的寬度導致,在一些計算速度比較低的手機,可能獲取到的屏幕寬度爲0,這樣就會導致你的組件的寬度爲負數,報出錯誤異常。如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nWidget hotelListDesContent(BuildContext context) {\nreturn Container(\n\/\/\/此處想實現左邊是圖片,右邊是相關信息的佈局,如果MediaQuery.on(context).size.width獲取爲0時,就會報出異常\n width: MediaQuery.of(context).size.width - Dimens.image_width80,\n \/\/\/右邊內容\n child: Stack(children: [\n Container(child: Column(\n mainAxisSize: MainAxisSize.min,\n mainAxisAlignment: MainAxisAlignment.start,\n children: [\n hotelListDesName(),\n englishName(),\n hotelListRemarkContent(),],),),\n \/\/\/左邊圖片\n Positioned(child: fullRoomItem()),\n ],\n));\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3)處理方式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"儘量使用Expand,Flexible,Flex,Wrap,Stack等組件配合Column,Row進行動態佈局設置組件的寬高等。如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nWidget hotelListDesContent(BuildContext context) {\nreturn Expanded(\n flex: 1,child: Stack(\n children: [Container(\n child: Column(\n mainAxisSize: MainAxisSize.min,\n mainAxisAlignment: MainAxisAlignment.start,\n children: [\n hotelListDesName(),\n englishName(),\n hotelListRemarkContent(),],),),\n Positioned(child: fullRoomItem()),\n ],\n ));\n}"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.3 使用Provider時,未判斷界面狀態通知界面刷新的問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)錯誤信息展示"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Null check operator used on a null value;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)錯誤分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般情況下出現這種問題是由於界面銷燬後,繼續調用notifyListeners()方法通知界面刷新引起的bug。當用戶打開一個界面,我們發送了API請求,此時用戶銷燬了界面,我們並未監聽,等到數據返回以後,強行通知界面刷新,導致Crash。如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nHotelServices.getTyHotelRoomPrice(params, ApiCallBack(onSuccess: (Object obj) {\nthis.roomPriceEntity = HotelRoomPriceEntity.fromJson(obj);\n this.resultCode = 1;\n \/\/\/如果在數據返回是,用戶已經關閉當前界面,此處通知刷新界面會導致crash\n notifyListeners();\n}, onError: (int code, String message) {}\n notifyListeners()\n}));"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3)處理方式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"正常情況下,我們會寫一個基類繼承ChangeNotifier,在內部重新複寫dispose()方法,同時重新封裝方法通知刷新界面,在每次需要通知刷新界面的時候判斷當前界面是否已經被銷燬。如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nimport 'package:flutter\/cupertino.dart';\n\/\/\/ ViewModel基類\nclass HotelViewModel extends ChangeNotifier{\n bool _disposed = false;\n @override\n void dispose() {\n _disposed = true;\n super.dispose();\n }\n void hotelNotifyListeners() {\n if(!_disposed){\n notifyListeners();\n }\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.4 使用Text.rich時導致的問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)錯誤信息展示:UnimplementedError"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)錯誤分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"出現這個問題的原因在於使用Text.rich來展示多個Span組件時,如果設置了最大行數,當組件超過最大行數,有別的組件未成功展示時,再次點擊當前widget,使它接受時間,就會導致crash,用戶的感知爲操作無響應,其實已經crash。如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n\/\/\/母房型名稱, 當前我們Text最大顯示兩行,當大於兩行是,出現...,可是此時第二個組件無處顯示,當用戶點擊就會crash\nRow(children: [\nExpanded(child: Text.rich(TextSpan(\n children: [TextSpan(\n text: itemRoomEntity.baseName ??\"\"),\n WidgetSpan(\n child: Container(\n padding: EdgeInsets.only(bottom: Dimens.gap_dp3),\n child: Icon(HotelIcons.show_more),\n ),\n ),\n ]), maxLines: 2, overflow: TextOverflow.ellipsis),\n ),\n], crossAxisAlignment: CrossAxisAlignment.center,),"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3)解決辦法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用Flexible代替Expanded,直接使用Text即可,區別在於Flexible不會自動填充整個剩餘寬度,如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n\/\/\/母房型名稱\nRow( mainAxisAlignment: MainAxisAlignment.start,\n children: [\nFlexible(child: Text((childCount > 1)?itemRoomEntity.baseName ?? \"\":\"\",\n maxLines: 2,\n overflow: TextOverflow.ellipsis,),),\nContainer(child: Icon(childCount ==1?HotelIcons.show_more:null),\n margin: EdgeInsets.only(top: Dimens.gap_dp2),),\n ], crossAxisAlignment: CrossAxisAlignment.center,)"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"六、總結與展望"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"總結一下,本文我們介紹了選擇Flutter的初衷,Provider 狀態管理的實際使用,建議Flutter主體的構架採用MVVM模式,還介紹了一些Flutter性能檢測、量化工具和一些性能優化點供大家參考。收集了Flutter開發過程中常見並且大量發生的問題,並提供了相應的解決方案。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在複雜業務和長列表上面體驗,確實 Flutter 優於 React Native。但是React Native 也有它的優勢,比如靈活的版本迭代。沒有最好的跨平臺方案,只有最合適業務的。目前來說,Flutter還處於早期階段,隨着Flutter2.0的重大升級,其跨平臺能力、性能、生態系統將會蓬勃發展,還是很值得嘗試的。後續我們也將有更多的業務接入Flutter。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"【參考文檔】"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[1] Flutter開發文檔"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/flutter.cn\/docs\/perf\/metrics","title":null,"type":null},"content":[{"type":"text","text":"https:\/\/flutter.cn\/docs\/perf\/metrics"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[2] Tripflutter開發文檔"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"http:\/\/pages.release.ctripcorp.com\/trip-flutter\/docs\/"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[3] 鹹魚技術"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/developer.aliyun.com\/group\/idlefish?spm=a2c6h.12873639.0.0.2c9618dd4mdBAQ#\/?_k=khoksz","title":null,"type":null},"content":[{"type":"text","text":"https:\/\/developer.aliyun.com\/group\/idlefish?spm=a2c6h.12873639.0.0.2c9618dd4mdBAQ#\/?_k=khoksz"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[4] Flutter實戰"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/flutter.cn\/docs\/perf\/metrics","title":null,"type":null},"content":[{"type":"text","text":"https:\/\/flutter.cn\/docs\/perf\/metrics"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[5] 美團技術"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/tech.meituan.com\/","title":null,"type":null},"content":[{"type":"text","text":"https:\/\/tech.meituan.com\/"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"作者簡介:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文爲聯合撰稿,作者爲攜程火車票Flutter團隊,致力於跨端快速、高性能開發。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文轉載自:攜程技術中心(ID:ctriptech)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/VP6WEQkEel3W4tdo3ThYDw","title":"xxx","type":null},"content":[{"type":"text","text":"乾貨 | 攜程火車票Flutter最佳實踐"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章