此控件的package我已經(jīng)托管到了 pub倉庫
創(chuàng)新互聯(lián),專注為中小企業(yè)提供官網(wǎng)建設(shè)、營銷型網(wǎng)站制作、響應(yīng)式網(wǎng)站開發(fā)、展示型網(wǎng)站設(shè)計制作、成都網(wǎng)站制作等服務(wù),幫助中小企業(yè)通過網(wǎng)站體現(xiàn)價值、有效益。幫助企業(yè)快速建站、解決網(wǎng)站建設(shè)與網(wǎng)站營銷推廣問題。
如果你被墻住了,也可以看 國內(nèi)鏡像
使用方式就是在你的flutter pubspec.yaml中添加依賴:
然后flutter packages get更新依賴即可
最近寫demo時發(fā)現(xiàn)Flutter自帶的ListView widget很簡陋,沒有分隔線,沒有section/row之分,也沒有sectionHeader,如果要實現(xiàn)一個有分割線,有section區(qū)分,有section header的ListView,耦合會非常嚴(yán)重:
在 上沒有找到封裝好的這種TableView,于是乎決定自己寫一個,命名為SectionTableView
本人是iOS開發(fā),所以習(xí)慣了iOS上的UITableView的調(diào)用風(fēng)格,所以在實現(xiàn)flutter的SectionTableView時,決定實現(xiàn)如下功能
為了實現(xiàn)這些功能,并且方便后期增加滾動功能,上下拉刷新功能,使用了StatefulWidget作為父類:
接著在對應(yīng)的_SectionTableViewState中的build方法中,返回ListView:
熟悉flutter ListView的同學(xué)知道,ListView的builder類方法,有一個itemBuilder回調(diào)函數(shù),參數(shù)是當(dāng)前的上下文,和將要渲染的行索引index,index對應(yīng)想要獲取的某一行控件(cell或者叫ListItem),返回非空的組件就證明這個index有值,返回null就表示列表到盡頭了。
我們需要做的就是對index進行映射,判斷當(dāng)前index對應(yīng)的控件,應(yīng)該是列表里的section header,還是分隔線devider,還是某一行的真正內(nèi)容cell。
出于性能的考慮,不可能每次調(diào)用 _buildCell的時候,都計算一遍index對應(yīng)的section和row的位置,所以定義了一個類成員變量indexPathSearch,是數(shù)組,數(shù)組長度就是ListView所有的行,當(dāng) _buildCell 的參數(shù)index大于等于indexPathSearch的長度的時候,就返回null,表示列表內(nèi)容到此為止了。
indexPathSearch里每一個元素,就是index對應(yīng)的section和row(稱為indexPath),index指向?qū)嶋H行(cell)的時候,section和row都是大于等于0的,當(dāng)section大于等于0,row==-1的時候,表示這里是一個section header,當(dāng)兩者都等于-1的時候,表示這里是一個分割線:
計算好了index到indexPath的映射,剩下的就好說了,在_buildCell中,提取indexPath并判斷indexPath的內(nèi)容,返回對應(yīng)的控件:
這是我的第一個flutter package,目前還很簡陋,flutter目前尚且如此,所以大家一起改善它,
下一步將優(yōu)化如下內(nèi)容:
如果大家喜歡,請多多star我的 項目GitHub
新建一個Flutter工程,android模塊。
1,只有一個Activity組件,它是Dart層繪制Widget的容器。
2,Application配置FlutterApplication。
應(yīng)用Application配置io.flutter.app.FlutterApplication類,App首次啟動時,初始化。
調(diào)用FlutterMain.startInitialization()方法。
initConfig方法,從AndroidManfest.xml配置的applicaion節(jié)點獲取meta-data數(shù)據(jù),初始化以下默認(rèn)值。
這些值都是使用中用到的name,例如,抽取apk中asset資源時,flutter_assets打包目錄,打包產(chǎn)物data名稱。
initResources方法, 初始化資源。
在Flutter打包apk的asset目錄下,包括fluttter_asset目錄/資源項,將資源從apk中抽取,保存在 Context.getDir("flutter", 0) 目錄下。
/data/user/0/包名/app_flutter目錄。
在目錄中創(chuàng)建一個時間戳文件,根據(jù)apk版本和包信息記錄的lastUpdateTime更新時間,第二次啟動時,若apk未更新,不需要再次抽取。
加載so庫,libflutter.so,System.loadLibrary()。
主頁面繼承FlutterActivity,配置啟動模式singleTop。
FlutterActivity類在io.flutter.app包, (區(qū)別io.flutter.embedding.android包), 組件生命周期委托給FlutterActivityDelegate類。
組件啟動,onCreate方法。
FlutterMain.ensureInitializationComplete方法,確保資源成功抽取完成,創(chuàng)建FlutterView視圖(io.flutter.view),繼承SurfaceView類,setContentView方法,設(shè)置組件主布局即FlutterView視圖。
最后,根據(jù)Bundle路徑,runBundle()加載運行,
調(diào)用FlutterView的runFromBundle方法,入口點在dart的main方法,
通過FlutterNativeView,調(diào)用FlutterJNI的native方法。
nativeRunBundleAndSnapshotFromLibrary方法。
任重而道遠
Flutter中Widget分為StatefulWidget和StatelessWidget,分別為動態(tài)視圖和靜態(tài)視圖,視圖的更新需要調(diào)用StatefulWidget的setState方法,這會遍歷調(diào)用子Widget的build方法。當(dāng)一個主頁面比較復(fù)雜時,會包含多個widget,如果直接調(diào)用setState,會遍歷所有子Widget的build,這是非常不必要的性能開銷,有沒有單獨刷新指定Widget的方式呢?這個時候就要用到GlobalKey了。
一個StatefulWidget包含一個Button,一個Text,通過點擊Button調(diào)用主Widget的setState方法,刷新Text,示例如下:
同樣一個StatefulWidget包含一個多個Text和Button,點擊Button我們只需要刷新指定的Text,通過GlobalKey的方式,實現(xiàn)如下:
主Widget,包含一個需要更新的TextWidget和一個不需要更新的Text
需要單獨更新的Widget
傳遞事件的Button
這樣點擊Button就只會更新指定的TextWidget了,效果如下:
這只是一個簡單的例子,在實際開發(fā)中為了頁面刷新的高效率,模塊化封裝非常重要。很多情況下都只需要局部刷新,而不是重構(gòu)整個視圖。所以Globalkey的運用在項目中需要熟練掌握
Flutter 里的 BuildContext 相信大家都不會陌生,雖然它叫 Context,但是它實際是 Element 的抽象對象,而在 Flutter 里,它主要來自于 ComponentElement 。
關(guān)于 ComponentElement 可以簡單介紹一下,在 Flutter 里根據(jù) Element 可以簡單地被歸納為兩類:
所以一般情況下,我們在 build 方法或者 State 里獲取到的 BuildContext 其實就是 ComponentElement 。
那使用 BuildContext 有什么需要注意的問題 ?
首先如下代碼所示,在該例子里當(dāng)用戶點擊 FloatingActionButton 的時候,代碼里做了一個 2秒的延遲,然后才調(diào)用 pop 退出當(dāng)前頁面。
正常情況下是不會有什么問題,但是當(dāng)用戶在點擊了 FloatingActionButton 之后,又馬上點擊了 AppBar 返回退出應(yīng)用,這時候就會出現(xiàn)以下的錯誤提示。
可以看到此時 log 說,Widget 對應(yīng)的 Element 已經(jīng)不在了,因為在 Navigator.of(context) 被調(diào)用時, context 對應(yīng)的 Element 已經(jīng)隨著我們的退出銷毀。
一般情況下處理這個問題也很簡單, 那就是增加 mounted 判斷,通過 mounted 判斷就可以避免上述的錯誤 。
上面代碼里的 mounted 標(biāo)識位來自于 State , 因為 State 是依附于 Element 創(chuàng)建,所以它可以感知 Element 的生命周期 ,例如 mounted 就是判斷 _element != null; 。
那么到這里我們收獲了一個小技巧: 使用 BuildContext 時,在必須時我們需要通過 mounted 來保證它的有效性 。
那么單純使用 mounted 就可以滿足 context 優(yōu)化的要求了嗎 ?
如下代碼所示,在這個例子里:
由于在 5 秒之內(nèi),Item 被劃出了屏幕,所以對應(yīng)的 Elment 其實是被釋放了,從而由于 mounted 判斷, SnackBar 不會被彈出。
那如果假設(shè)需要在開發(fā)時展示點擊數(shù)據(jù)上報的結(jié)果,也就是 Item 被釋放了還需要彈出,這時候需要如何處理 ?
我們知道不管是 ScaffoldMessenger.of(context) 還是 Navigator.of(context) ,它本質(zhì)還是通過 context 去往上查找對應(yīng)的 InheritedWidget 泛型,所以其實我們可以提前獲取。
所以,如下代碼所示,在 Future.delayed 之前我們就通過 ScaffoldMessenger.of(context); 獲取到 sm 對象,之后就算你直接退出當(dāng)前的列表頁面,5秒過后 SnackBar 也能正常彈出。
為什么頁面銷毀了,但是 SnackBar 還能正常彈出 ?
因為此時通過 of(context); 獲取到的 ScaffoldMessenger 是存在 MaterialApp 里,所以就算頁面銷毀了也不影響 SnackBar 的執(zhí)行。
但是如果我們修改例子,如下代碼所示,在 Scaffold 上面多嵌套一個 ScaffoldMessenger ,這時候在 Item 里通過 ScaffoldMessenger.of(context) 獲取到的就會是當(dāng)前頁面下的 ScaffoldMessenger 。
這種情況下我們只能保證Item 不可見的時候 SnackBar 還能正常彈出, 而如果這時候我們直接退出頁面,還是會出現(xiàn)以下的錯誤提示,因為 ScaffoldMessenger 也被銷毀了 。
所以到這里我們收獲第二個小技巧: 在異步操作里使用 of(context) ,可以提前獲取,之后再做異步操作,這樣可以盡量保證流程可以完整執(zhí)行 。
既然我們說到通過 of(context) 去獲取上層共享往下共享的 InheritedWidget ,那在哪里獲取就比較好 ?
還記得前面的 log 嗎?在第一個例子出錯時,log 里就提示了一個方法,也就是 State 的 didChangeDependencies 方法。
為什么是官方會建議在這個方法里去調(diào)用 of(context) ?
首先前面我們一直說,通過 of(context) 獲取到的是 InheritedWidget ,而 當(dāng) InheritedWidget 發(fā)生改變時,就是通過觸發(fā)綁定過的 Element 里 State 的 didChangeDependencies 來觸發(fā)更新, 所以在 didChangeDependencies 里調(diào)用 of(context) 有較好的因果關(guān)系 。
那我能在 initState 里提前調(diào)用嗎 ?
當(dāng)然不行,首先如果在 initState 直接調(diào)用如 ScaffoldMessenger.of(context).showSnackBar 方法,就會看到以下的錯誤提示。
這是因為 Element 里會判斷此時的 _StateLifecycle 狀態(tài),如果此時是 _StateLifecycle.created 或者 _StateLifecycle.defunct ,也就是在 initState 和 dispose ,是不允許執(zhí)行 of(context) 操作。
當(dāng)然,如果你硬是想在 initState 下調(diào)用也行,增加一個 Future 執(zhí)行就可以成功執(zhí)行
那我在 build 里直接調(diào)用不行嗎 ?
直接在 build 里調(diào)用肯定可以,雖然 build 會被比較頻繁執(zhí)行,但是 of(context) 操作其實就是在一個 map 里通過 key - value 獲取泛型對象,所以對性能不會有太大的影響。
真正對性能有影響的是 of(context) 的綁定數(shù)量和獲取到對象之后的自定義邏輯 ,例如你通過 MediaQuery.of(context).size 獲取到屏幕大小之后,通過一系列復(fù)雜計算來定位你的控件。
例如上面這段代碼,可能會導(dǎo)致鍵盤在彈出的時候,雖然當(dāng)前頁面并沒有完全展示,但是也會導(dǎo)致你的控件不斷重新計算從而出現(xiàn)卡頓。
所以到這里我們又收獲了一個小技巧: 對于 of(context) 的相關(guān)操作邏輯,可以盡量放到 didChangeDependencies 里去處理 。
下面這種情況下,為 InkWell 設(shè)置的 splashColor 不會生效:
需要用 Material 去除背景色,然后將顏色設(shè)置在 InkWell 外部:
在 Dialog builder 中使用 WillPopScope 禁用返回鍵返回:
注意:使用此方法同時也會禁用 iOS 上的手勢滑動返回功能,推薦判斷平臺后再使用。
修改對話框中的復(fù)選框狀態(tài),最簡便的方法是通過 Element 中的 markNeedsBuild 方法:
當(dāng)然,更推薦的做法是通過 StatefulBuilder ,然后就可以在 Dialog 中調(diào)用 setState 方法了,不過在調(diào)用 setState 時需要判斷 Dialog 是否已經(jīng)關(guān)閉,否則會造成 setState() called after dispose() 的錯誤,可以通過添加一個標(biāo)志位來解決,如下:
在 Web 中加載網(wǎng)絡(luò)圖片有時會失敗,遇到這樣的報錯: Exception caught by image resource service... ,造成該錯誤的原因通常是,圖片跨域了(見 跨域資源共享 )。最簡單的解決辦法是, 使用 HTML 渲染加載 ,而不是默認(rèn)的 CanvasKit。
Flutter 中所有的 list 默認(rèn)都是沒有 ScrollBar 的,必須使用 ScrollBar 組件。ScrollBar 組件通過監(jiān)聽 ScrollView 的 ScrollNotification 來刷新位置,所以 List 的長度必須是固定的。
當(dāng)使用 WebView 等高度不定的組件時會出現(xiàn)內(nèi)容被截斷的情況,通??梢允褂?NestedScrollView 來解決該問題,需要在 WebView 外部嵌套 SingleChildScrollView。
雖然使用了緩存,而且也是用 builder 加載圖片的,但是發(fā)現(xiàn)一個現(xiàn)象:滑動屏幕后圖片短暫消失并重新加載了。圖片高度很高時這種現(xiàn)象更加明顯,其原因是超出屏幕范圍一定距離的組件被重新渲染了。解決方法是在 ListView 上設(shè)置 cacheExtent 參數(shù):
該參數(shù)的作用是改變超出屏幕高度后繼續(xù)渲染的范圍(以像素為單位),比如設(shè)置成 9999 后意味著超出屏幕 10000 像素以內(nèi)的內(nèi)容都會被保留下來。
借助 IntrinsicHeight 組件:
另外,IntrinsicHeight 還可以用于 Dialog 或者 BottomSheet 中,使得其中的元素 顯示內(nèi)在元素的高度 ,從而避免元素因為約束的存在而不顯示或者高度太高(比如在使用了 Column 或者 Row 的時候)。
在通過 Uri 的 queryParameters 獲取 query 參數(shù)時,發(fā)現(xiàn)有些鏈接會拋出下面異常:
造成該異常的原因是 Uri 默認(rèn)使用 utf-8 解碼超鏈接字符串,如果鏈接中包含非 utf-8 字符,就會造成上面的錯誤,相關(guān) issue 見: issue #31621 。目前該 issue 處于 open 的狀態(tài),暫時的解決辦法是,在所有使用到 queryParameter 的地方用 try..catch 捕捉可能拋出的異常。
Flutter 開發(fā)非常依賴各種官方或第三方的插件,而在使用這些插件時多少都會遇到一些問題,大部分問題都可以通過搜索和查找 issue 來解決。這里記錄下一些我在使用部分插件時遇到的問題及其解決方法。
目前該庫沒有圖片加載完成的回調(diào)(見 issue #545 ),不過我們可以通過在 imageBuilder 中來添加回調(diào):
這是一個應(yīng)用內(nèi)更新插件,安卓 10 以上安裝時需要在 manifest 中添加以下內(nèi)容:
目前功能最強大的 WebView 插件,基本能滿足絕大部分移動端網(wǎng)頁加載的需求,而且可定制化程度高。
一般通過 CookieManager 修改 Cookie,攔截請求并修改請求對象的 Header 不會生效。
InAppWebViewOptions 的 userAgent 只在 iOS 上生效,而 applicationNameForUserAgent 只在 Android 上生效,所以最好的做法是分平臺設(shè)置 InAppWebViewOptions ,而且需要注意,由于設(shè)置 userAgent 后會覆蓋默認(rèn)的 UserAgent,所以如果需要在默認(rèn)的 UserAgent 上添加其它參數(shù),iOS 上需要通過 InAppWebViewController.getDefaultUserAgent() 獲取默認(rèn) UserAgent 參數(shù),而 Android 不需要添加。
如果圖片源或者請求是 http 的,為了在 Android 上正常加載請求,必須在 AndroidInAppWebViewOptions 中將 mixedContentMode 設(shè)置為 AndroidMixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW 。
當(dāng)我們想要設(shè)置全屏圖片的時候,由于默認(rèn)的 Constraint 會將圖片居中顯示,所以圖片四周會留有空隙。為了去除這個限制,我們需要 Xcode 中打開 LaunchScreen.storyboard,然后在 View Controller 的 View 和 LaunchImage 上的 Safe Area 去掉。
具體設(shè)置方法:右側(cè) Inspector 面板 Show the Size inspector 解選 Layout Margins 中的 Safe Area Relative Margins,拖動圖片占滿全屏,然后根據(jù) View Controller Scene 的 Warning,更新 Constraint 就可以了。
在集成某些三方庫之后,在使用命令行運行 iOS 模擬器的時候可能會遇到下面這個報錯:
這是因為 iOS 模擬器未來將會兼容 arm64 架構(gòu),但是目前還不支持,所以我們需要修改 Build Setting 使得能夠在 x86_64 的模擬器上運行,操作步驟見 這里 。