前言
這篇文章是我一直以來很想寫的一篇文章,終于下定決心動(dòng)筆了。
寫Flutter的小伙伴可能都感受到了:掘金的一些熱門的Flutter文章下,知乎的一些Flutter的話題下或者一些論壇里面,噴Flutter套娃地獄總是永不過時(shí)的一個(gè)話題。
如果你不服氣,上去辯駁倆下:“嵌套是你代碼習(xí)慣問題,你看我,抬手一個(gè)Row,反手一個(gè)Column,在children中把widget一提,層次分明,年輕人望你耗子尾汁,莫要瞎帶節(jié)奏”;然后你可能就被一群人噴成狗,大意了,這帖子沒同一陣營的小伙伴,噴不過,閃了閃了;一般被噴后,不是身經(jīng)百被噴,都需要一段時(shí)間來平復(fù)心情。。。
所以,終于我下定決心把這篇文章肝出來,如果你認(rèn)真看完,你可能會(huì)發(fā)現(xiàn):嵌套什么的都是浮云,從此你的頁面代碼將變的超級好維護(hù),交互邏輯入口,也變得層次分明。
全篇文章,絕無教大家做事之意,這是在項(xiàng)目中摸爬滾打,被坑出的不得不如此規(guī)范的一種行為。
準(zhǔn)備
改善
這篇文章能幫你改善什么問題?
-
頁面層的widget瘋狂套娃幾千行,后期維護(hù),心態(tài)崩了等問題
- 套娃不劃分頁面,后期需求大變,讓你大改頁面細(xì)節(jié)甚至結(jié)構(gòu),那將是非常難受的一件事
-
邏輯交互事件入口,混雜在widget,難以尋找問題
- 如果你在頁面層瘋狂套娃,你會(huì)發(fā)現(xiàn),就算用了provider,bloc中的cubit,getx之類,你想找到邏輯交互入口,也是一件很累的事情,改樣式那就更方了。。。
- 這里再嗶嗶一下,這些框架作者肯定是發(fā)現(xiàn)了這種情況,所以bloc才搞出了event層,redux搞出了action層,來統(tǒng)一管理事件及其事件入口。
頁面結(jié)構(gòu)充斥大量細(xì)節(jié),結(jié)構(gòu)調(diào)整起來困難
上面關(guān)于頁面層的這些問題,如果多人協(xié)同開發(fā)一個(gè)大型項(xiàng)目,代碼不規(guī)范的話,大概率都是會(huì)遇到的(改別人寫的模塊...);后期改需求 ,真的是一種折磨,有種碼海找針的感覺。
如果改你自己寫的模塊,那可能還會(huì)好點(diǎn),畢竟你還有點(diǎn)印象,整個(gè)模塊的大概思路,還知道怎么改。如果是改別人寫的模塊,你就需要在大量widget海中,去揣摩別人寫這些widget的意圖,結(jié)構(gòu)一下子也不能理清,十分痛苦,有可能邊改邊罵罵咧咧的。。。
Demo效果
在構(gòu)思文章的時(shí)候,就在想演示的Demo頁面必定不能過于簡單,一個(gè)簡單的Demo頁面,怎么能演示出套娃地獄的改善效果呢?思考了很久,想尋找一個(gè)合適demo頁面,周末時(shí)在聽喜馬拉雅里面的盜墓小說,看了看發(fā)現(xiàn)頁面,發(fā)現(xiàn)整體樣式不錯(cuò),咱就仿一個(gè)吧!而且整體的頁面復(fù)雜度,也足夠來演示了!
喜馬拉雅的這個(gè)PC頁面Demo,寫起來真的花費(fèi)了不少時(shí)間,希望能對大家有所幫助吧。
地址
- Web:仿喜馬拉雅頁面
- web無法強(qiáng)制設(shè)置窗口大小,可能需要你調(diào)整下web窗口的寬度,以達(dá)到最佳效果
- Windows:Windows平臺安裝包
- 如果你的電腦開啟了125%的
縮放與布局
,請打首頁的開啟縮放
按鈕
- 如果你的電腦開啟了125%的
- 項(xiàng)目地址:flutter_use
說明
代碼已經(jīng)發(fā)布到Github上,web端也已經(jīng)部署好了,因?yàn)槭褂玫腃anvasKit模式打包的,首次加載可能比較慢,多等一會(huì),因?yàn)閃eb端部署在Github上,訪問的話,要確保你的網(wǎng)絡(luò)能訪問Github。
-
關(guān)于Widows安裝包
- Window筆記本高分屏一般會(huì)開啟125%的縮放,這時(shí)候,存在一個(gè)坑比的問題,開啟縮放的時(shí)候,F(xiàn)lutter的布局都會(huì)相應(yīng)的縮放,但是坑比的是,整體的窗口并不會(huì)縮放,導(dǎo)致內(nèi)容會(huì)積壓整體的窗口,這個(gè)問題我也在幾臺電腦上,調(diào)了好久才發(fā)現(xiàn)的。
- 解決辦法,寫了個(gè)手動(dòng)開啟適配的功能。
- 關(guān)于
開啟縮放
的按鈕功能,只支持放大125%窗口功能,其它的也不用折騰了,我發(fā)現(xiàn)window_size初始化后,第一次設(shè)置完窗口尺寸后;然后,再設(shè)置窗口時(shí),往大了設(shè)置有效,往小了回調(diào)會(huì)無效,奇怪。。。
效果對比
來對比下仿制的效果吧,有個(gè)六七成相似,很多Icon和圖片實(shí)在找不到相似,,,這里demo只提供一個(gè)樣式演示,功能別想了,這不是一朝一夕,一個(gè)人能搞出的。。。
照片都是從喜馬拉雅web端上搞下來的,數(shù)據(jù)一直在變,相應(yīng)欄目的數(shù)據(jù)有對不上,但是整體樣式大致還是差不多。
其中Banner模塊是區(qū)別最大的一塊,用的三方庫只能支持搞成這樣,各位靚仔將就著看看吧。
- 原版的喜馬拉雅PC頁面
- 仿制的喜馬拉雅頁面
總結(jié)
上面?zhèn)z組圖片,細(xì)節(jié)方面對比基本慘不忍睹,但是整體架構(gòu)上還是比較相似。
建議各位彥祖,下載下window安裝包,安裝體驗(yàn)下;MacOS的于晏們,你們可以看看web展示效果。
咱們馬上來看看怎么搞規(guī)范代碼吧!復(fù)雜的模塊,讓你的代碼也能高度可維護(hù)!
開搞
分析
- Android的業(yè)務(wù)自定義View
- 在Android里面有個(gè)頁面分模塊的開發(fā)思想,將整個(gè)頁面劃分成幾個(gè)業(yè)務(wù)的自定義View,我們只需要關(guān)注傳入數(shù)據(jù)源,和對應(yīng)業(yè)務(wù)View交互的回調(diào)事件;數(shù)據(jù)源和交互事件是重點(diǎn)需要關(guān)注,其它的都不是我們需要關(guān)心的,不需要關(guān)注的細(xì)節(jié)封裝在內(nèi)部即可
- 然后主頁面里面,組合下這些業(yè)務(wù)view就OK了;徹底拋棄include坑比做法,include讓xml也耦合了,如果改動(dòng)了一個(gè)被多處引用的xml,可能會(huì)引發(fā)的一些影響,大家心里可以揣摩揣摩
- 上面的思想:明顯是外觀模式(門面模式)的思想。。。
- Flutter的Widget
- 然后再結(jié)合Flutter中那些眾多的系統(tǒng)widget,系統(tǒng)那些Widget基本都屬于功能性的Widget,需要定義巨量的字段傳值
- 這樣的好處,就是能夠非常顆粒的去控制需要的字段,再配合一些定義的回到函數(shù),就能起到:數(shù)據(jù)源和交互回調(diào)的完美組合。
結(jié)合上面的業(yè)務(wù)View和一切皆Widget的思路,我們可以得出一個(gè)結(jié)論:搞業(yè)務(wù)Widget,然后再進(jìn)行組合!
當(dāng)然,咱們在這里得出了一個(gè)不是結(jié)論的結(jié)論,一般來說,這種操作是咱們基本素養(yǎng),但是具體的操作細(xì)節(jié)上,還是有很多需要注意的:
- 業(yè)務(wù)Widget,也需要?jiǎng)澐帜K
- Column,Row之類有著天然結(jié)構(gòu),怎么去利用
- 旁枝末節(jié)的Widget細(xì)節(jié),怎么去封裝
主模塊封裝
上面咱們一通分析猛如虎后,得出一個(gè)結(jié)論:搞業(yè)務(wù)Widget!
關(guān)于業(yè)務(wù)Widget的封裝細(xì)節(jié),這里說明下:
-
數(shù)據(jù)源盡量只使用一個(gè),不要使用過多字段去劃分
- 解釋下,因?yàn)槲覀冞@是業(yè)務(wù)性widget,并不是功能性widget,過渡的細(xì)分字段輸入,會(huì)導(dǎo)致你封裝的widget過長,業(yè)務(wù)Widget很多時(shí)候,只會(huì)在你這個(gè)模塊,其它模塊一般都很少用的,沒必要去過度的細(xì)分字段,開發(fā)多了你就會(huì)發(fā)現(xiàn),你封裝的那些業(yè)務(wù)Widget,百分之95的概率,只會(huì)在你自己寫的那個(gè)頁面吃灰一輩紙。。。
- 如果是比較通用的widget,那就可以細(xì)分字段了或者使用中間實(shí)體都OK! 通用的模塊開發(fā),關(guān)于數(shù)據(jù)源輸入,就需要考慮一些比較通用的數(shù)據(jù)格式,例如只需要一個(gè)list數(shù)據(jù),就不要搞一個(gè)實(shí)體,只需要一個(gè)字段,就不需要搞一個(gè)list等等。。。
-
交互事件,必須使用回調(diào)函數(shù),暴露出來
- 關(guān)于交互事件,這里必須要暴露出來,給業(yè)務(wù)層或者邏輯層去處理
- 一般來說,用戶進(jìn)入該頁面,點(diǎn)擊或滑動(dòng)頁面,就是業(yè)務(wù)事件產(chǎn)生的時(shí)候了,這是必須暴露出來的,切記切記。
主模塊的結(jié)構(gòu)
這里使用了一點(diǎn)Getx知識,如果你不了解,可參考:Flutter GetX使用---簡潔的魅力!
- 主模塊代碼:按照下面的封裝,基本是把View層和Action層做了一個(gè)結(jié)合了
- 所有業(yè)務(wù)Widget的入口,可快速定位到需要修改的業(yè)務(wù)Widget
- 所有的事件交互入口,一眼可見,這樣能快速定位相應(yīng)的業(yè)務(wù)
class HimalayaPage extends StatelessWidget {
final logic = Get.put(HimalayaLogic());
final state = Get.find<HimalayaLogic>().state;
@override
Widget build(BuildContext context) {
return himalayaBuildBg(children: [
//頂部:左邊側(cè)邊導(dǎo)航欄 + 右邊信息流
himalayaBuildTopBg(children: [
//左邊導(dǎo)航欄
HimalayaLeftNavigation(
data: state,
//導(dǎo)航欄item回調(diào)
onTap: (HimalayaSubItemInfo item) => logic.navigationItem(item),
),
//右邊信息流
himalayaBuildInfoListBg(children: [
//頂部搜索框及其一些個(gè)人信息設(shè)置按鈕
HimalayaPersonalInfo(
//搜索框輸入監(jiān)聽
onChanged: (String msg) => logic.onSearch(msg),
//左箭頭
onLeftArrow: () => logic.dealLeftArrow(),
//右箭頭
onRightArrow: () => logic.dealRightArrow(),
//刷新按鈕
onRefresh: () => logic.onRefreshData(),
//皮膚按鈕
onSkin: () => logic.switchSkin(),
//設(shè)置按鈕
onSetting: () => logic.onSetting(),
),
//右側(cè)信息流 - 可滑動(dòng)部分
himalayaBuildScrollInfoListBg(children: [
//輪播圖
HimalayaBanner(
data: state.bannerList,
//具體banner的監(jiān)聽
onTap: (int index) => logic.clickBanner(index),
),
//猜你喜歡
HimalayaGuess(
data: state.guessList,
//換一批
onChange: () => logic.guessChange(),
//猜你喜歡具體卡片
onGuess: (HimalayaSubItemInfo item) => logic.guessDetail(item),
),
//最新精選
HimalayaNewest(
data: state,
//分類標(biāo)題
onSortTitle: (item) => logic.sortTitle(item),
//具體精選卡片
onNewest: (HimalayaSubItemInfo item) => logic.onNewest(item),
),
//熱門主播
HimalayaAnchor(
data: state.anchorList,
onAnchor: (HimalayaSubItemInfo item) => logic.hotAnchor(item),
),
//各類榜單
HimalayaRankList(
data: state.rankList,
//標(biāo)題
onTitle: (String title) => logic.rankTitle(title),
//榜單上具體item
onItem: (HimalayaSubItemInfo item) => logic.rankItem(item),
),
]),
]),
]),
//底部:音頻播放控制臺
HimalayaAudioConsole(
data: state.audioPlayInfo,
//左切換
onLeftArrow: () => logic.onLeftArrow(),
//播放
onPlay: () => logic.onPlay(),
//右切換
onRightArrow: () => logic.onRightArrow(),
//喜歡
onLove: () => logic.onLove(),
//播放模式
onPlayModel: () => logic.onPlayModel(),
//封面
onCover: () => logic.onCover(),
//進(jìn)度
onProgress: () => logic.onProgress(),
//音量
onVolume: () => logic.onVolume(),
//標(biāo)題
onSubtitle: () => logic.onSubtitle(),
//倍速
onSpeed: () => logic.onSpeed(),
//定時(shí)
onTiming: () => logic.onTiming(),
//目錄
onCatalog: () => logic.onCatalog(),
),
]);
}
}
經(jīng)過上面的一通封裝組合后,大家摸著良心說說:
- 還死亡嵌套嗎?
- 還俄羅斯套娃嗎?
- 看著還恐怖嗎?
別噴套娃了,外觀模式的思想稍稍這么一用,套娃直接GG
設(shè)計(jì)模式,yyds!
細(xì)節(jié)分析
一般來說,一個(gè)頁面整體基本上是橫向(Row)或者縱向(Column)的結(jié)構(gòu)
咱們仿造的喜馬拉雅模塊也是屬于縱向結(jié)構(gòu):上下倆大模塊
-
上模塊:導(dǎo)航欄 + 信息流 => 又分左右模塊
- 左模塊:左邊的側(cè)面導(dǎo)航欄 => 很明顯的縱向布局
- 右模塊:信息流 => 這就是簡單的縱向結(jié)構(gòu),從上到下了
下模塊:音頻播放欄 => 完全就是橫向布局了
通過上面的說明,很明顯,Row和Column中children屬性才是我們所關(guān)注的,其它的細(xì)節(jié)描述封裝起來即可
主體細(xì)節(jié)封裝
主模塊的很多主體細(xì)節(jié),是完全可以封裝起來的,新建一個(gè)(模塊名_function)文件
-
himalaya_function.dart:主體部分有很多無需關(guān)注的細(xì)節(jié),統(tǒng)一放在這個(gè)模塊
- 對外,只需要暴露一些必須的參數(shù)
- 請勿將這些無關(guān)的細(xì)節(jié)寫在主模塊中,會(huì)干擾到我們需要關(guān)注的信息
- 這些主體樣式寫完后,基本就很少去修改了
///喜馬拉雅整體外層布局設(shè)置
Widget himalayaBuildBg({required List<Widget> children}) {
return Scaffold(
backgroundColor: Colors.white,
body: Column(children: children),
);
}
///播放控制欄上面的外層布局設(shè)置
Widget himalayaBuildTopBg({required List<Widget> children}) {
return Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
);
}
///頂部右側(cè)信息流外層布局設(shè)置
Widget himalayaBuildInfoListBg({required List<Widget> children}) {
return Expanded(
child: Column(children: children),
);
}
///頂部右側(cè)信息流外層布局設(shè)置 - 可滑動(dòng)部分
Widget himalayaBuildScrollInfoListBg({required List<Widget> children}) {
return Expanded(
child: CustomSingleChildScrollView(
child: Container(
width: 860.dp,
child: Column(children: children),
),
),
);
}
業(yè)務(wù)Widget封裝
關(guān)于業(yè)務(wù)Widget封裝,是核心所在,這個(gè)非常重要?
幾個(gè)要點(diǎn)
- 盡量只暴露一個(gè)數(shù)據(jù)源(非通用業(yè)務(wù)Widget)
- 所有的事件交互必須暴露出來
- 主體細(xì)節(jié)封裝起來
- children中的widget全部提成方法
children中封裝
先來看看第一種情況,最常見的情況,children的widget,從上到下排列下來,非列表類數(shù)據(jù)
- 來看看這個(gè)頂部一些功能按鈕的布局,這塊涉及到很多事件交互,所以單獨(dú)提成了一個(gè)業(yè)務(wù)Widget
- 實(shí)現(xiàn)代碼:關(guān)于業(yè)務(wù)Widget,這是基石,規(guī)范寫好后,后期修改,異常簡單
- 結(jié)合上面的效果圖,再結(jié)合下面的代碼,大家應(yīng)該一眼看出來,就知道是哪個(gè)widget方法,對應(yīng)界面上的哪個(gè)控件;如果你想修改哪個(gè)控件樣式,直接點(diǎn)進(jìn)對應(yīng)的widget方法里修改即可
- children里面的每個(gè)widget方法上面,請一定一定記得寫上注釋,因?yàn)榇颂幉攀菢I(yè)務(wù)Widget最主要的入口,具體的widget方法寫不寫注釋無所謂了
///搜索框 個(gè)人信息 設(shè)置等按鈕
class HimalayaPersonalInfo extends StatelessWidget {
HimalayaPersonalInfo({
Key? key,
required this.onRefresh,
required this.onLeftArrow,
required this.onRightArrow,
required this.onSetting,
required this.onSkin,
required this.onChanged,
}) : super(key: key);
.............
@override
Widget build(BuildContext context) {
return _buildBg(children: [
//左圖標(biāo)
_buildLeftArrow(),
//右圖標(biāo)
_buildRightArrow(),
//刷新圖標(biāo)
_buildRefresh(),
//搜索框
_buildSearch(),
//頭像
_buildHeadImg(),
//皮膚
_buildSkin(),
//設(shè)置
_buildSetting(),
]);
}
..........
}
-
來看下其中的
_buildBg
方法- 可以發(fā)現(xiàn)
_buildBg
主體的這些細(xì)節(jié)描述,真的是無關(guān)緊要的代碼,這個(gè)寫完后,基本上,后面都很少去改,所以把它提取出來后,放在墻角吃灰就行了
- 可以發(fā)現(xiàn)
///搜索框 個(gè)人信息 設(shè)置等按鈕
class HimalayaPersonalInfo extends StatelessWidget {
........
Widget _buildBg({required List<Widget> children}) {
return Container(
margin: EdgeInsets.symmetric(vertical: 10.dp, horizontal: 18.dp),
width: 800.dp,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: children,
),
);
}
}
- 關(guān)于方法提取
- 選中你需要提取的Widget代碼
- 打開 Flutter Outline 選擇
右箭頭
圖片
- 填上方法名后,就能自動(dòng)生成一個(gè)widget方法
- 如果你提取的Widget塊中,還含有一些數(shù)據(jù),自動(dòng)生成的方法都會(huì)帶上相應(yīng)參數(shù),非常方便
單層列表樣式封裝
類列表樣式的封裝也是比較關(guān)鍵的,直接從頭莽尾式的提取是不行,這邊有一絲調(diào)整
這里就以猜你喜歡
模塊舉例
- 猜你喜歡模塊
- 代碼分析:總體是Column布局,分上下倆模塊
- 上模塊使用Row搞定即可
- 下模塊是四個(gè)卡片,這邊是直接用的寫死List數(shù)據(jù)源
///猜你喜歡
class HimalayaGuess extends StatelessWidget {
HimalayaGuess({
Key? key,
required this.data,
required this.onChange,
required this.onGuess,
}) : super(key: key);
..........
@override
Widget build(BuildContext context) {
return _buildBg(children: [
//標(biāo)題 + 換一批
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
//標(biāo)題
_buildTitle(),
//換一批
_buildGuessChange()
]),
//顯示具體信息流
_buildItemBg(itemBuilder: (item) {
return [
//圖片卡片
_buildPicCard(item),
//文字描述
Text(item.title, style: TextStyle(fontSize: 15.sp)),
//子標(biāo)題
_buildSubTitle(item),
];
})
]);
}
..........
}
- 上述children代碼,整體上還是比較清晰,有點(diǎn)迷糊的,可能就是
_buildItemBg
,來看看其中代碼- 此方法對面暴露了一個(gè)
itemBuilder
參數(shù),這其實(shí)是一個(gè)回調(diào)方法 - 因?yàn)榱斜眍悩邮剑仨氁闅v整個(gè)列表數(shù)據(jù),然后,需要把列表遍歷的具體數(shù)據(jù),反向傳給Widget
- 所以必須使用回調(diào)方法反傳數(shù)據(jù)
- 此方法對面暴露了一個(gè)
///猜你喜歡
class HimalayaGuess extends StatelessWidget {
...............
Widget _buildItemBg({
required List<Widget> Function(HimalayaSubItemInfo item) itemBuilder,
}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(data.length, (index) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: itemBuilder(data[index]),
);
}),
);
}
}
雙層列表樣式封裝
關(guān)于雙層列表數(shù)據(jù)源(List的每個(gè)具體數(shù)據(jù)源,又含有List)又該怎么封裝呢?
- 倆層List數(shù)據(jù)源封裝是比較麻煩,這邊以側(cè)邊欄舉例
- 整個(gè)布局是一個(gè)Column:標(biāo)題 + 欄目(List數(shù)據(jù)控制)
- 欄目
- 可劃分具體的Item
- Item:標(biāo)題 + 欄目(List數(shù)據(jù)控制)
- 代碼實(shí)現(xiàn)
- 上面的布局整體是由數(shù)據(jù)源驅(qū)動(dòng)頁面,數(shù)據(jù)能控制頁面item生成
///數(shù)據(jù)源:側(cè)邊導(dǎo)航欄目初始數(shù)據(jù),簡化了下,數(shù)據(jù)源太長了
///該數(shù)據(jù)源都放在state層維護(hù),此處放在這里,讓大家有個(gè)對比
leftItemList = [
HimalayaItemInfo(title: '推薦', subItemList: [
HimalayaSubItemInfo(
title: '發(fā)現(xiàn)',
icon: CupertinoIcons.compass,
tag: TagHimalayaConfig.find,
isSelected: true,
),
..............
]),
HimalayaItemInfo(title: '我聽', subItemList: [
HimalayaSubItemInfo(
title: '我的訂閱',
icon: Icons.star_border,
tag: TagHimalayaConfig.subscription,
),
.........
]),
HimalayaItemInfo(title: '我創(chuàng)建的聽單', subItemList: [
HimalayaSubItemInfo(
title: '我喜歡的聲音',
icon: Icons.favorite_border,
tag: TagHimalayaConfig.sound,
),
............
]),
];
///左邊導(dǎo)航欄
class HimalayaLeftNavigation extends StatelessWidget {
HimalayaLeftNavigation({
Key? key,
required this.data,
required this.onTap,
}) : super(key: key);
........
@override
Widget build(BuildContext context) {
return _buildBg(children: [
//喜馬拉雅logo圖標(biāo)
_buildLogo(),
//遍歷倆層循環(huán):不同item欄目 - 可點(diǎn)擊,可滑動(dòng)
//第一層:標(biāo)題 + 子item列表
//第二層:子item詳細(xì)布局
_buildItemListBg(itemBuilder: (item) {
return [
//最外層item - 大標(biāo)題
_buildTitle(item.title),
//子欄目 - 列表
_buildSubItemListBg(item, subBuilder: (subItem) {
return [
//選中紅色長方形條塊
_buildRedTag(subItem),
//圖標(biāo)
_buildItemIcon(subItem),
//描述
_buildItemDesc(subItem),
];
})
];
}),
]);
}
..........
}
- 第一層:來看下第一層
_buildItemListBg
方法- 這玩意不得不套了,需要的屬性太多了:滾動(dòng),滾動(dòng)條等
- 這玩意要是不提出來,從上往下套,那簡直就是毒瘤。。。
class HimalayaLeftNavigation extends StatelessWidget {
..........
Widget _buildItemListBg({
required List<Widget> Function(HimalayaItemInfo item) itemBuilder,
}) {
return Expanded(
child: Scrollbar(
child: CustomSingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(data.leftItemList.length, (index) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: itemBuilder(data.leftItemList[index]),
);
}),
),
),
),
);
}
}
- 第二層
- 這里面必須需要第一層遍歷的具體數(shù)據(jù)源,所以必須增加一個(gè)輸入?yún)?shù)
- 這里就是常規(guī)提取,需要注意的就是傳入的數(shù)據(jù)源
class HimalayaLeftNavigation extends StatelessWidget {
..........
Widget _buildSubItemListBg(
HimalayaItemInfo item, {
required List<Widget> Function(HimalayaSubItemInfo item) subBuilder,
}) {
return Column(
children: List.generate(item.subItemList.length, (index) {
return InkWell(
onTap: () => onTap(item.subItemList[index]),
child: Container(
padding: EdgeInsets.symmetric(vertical: 9.dp),
child: Row(children: subBuilder(item.subItemList[index])),
),
);
}),
);
}
}
總結(jié)
經(jīng)過上面的一通操作,業(yè)務(wù)Widget立馬變的清爽N倍
大家在寫Flutter的時(shí)候,應(yīng)該能明顯的感覺到,寫頁面擁有高度的自由,樣式、頁面結(jié)構(gòu)及其邏輯全都能耦合在一起。
既然我們還達(dá)不到,無招勝有招的水平;那么下筆之前還是要有點(diǎn)章法的好,所以在實(shí)際開發(fā)中,要注意自己代碼規(guī)范啊。。。
假設(shè)一種情況
- 你開發(fā)完一個(gè)模塊
- 過了幾月之后,需求調(diào)整,你要去改這個(gè)模塊
- 看到幾千行的套娃頁面代碼,然后一邊改一邊罵罵咧咧,開噴:這是哪個(gè)睿智的人寫的!!!
- 最后打開文件的git注釋(annotate)記錄,結(jié)束上面寫滿了你的名字
- 那豈不是很尷尬。。。
題外話
說一點(diǎn)題外話
實(shí)際上寫html也是無限套娃,不同的是,它從根本上做到的樣式結(jié)構(gòu)分離,控件的細(xì)節(jié)描述,全部交給了css去做,所以頁面整體看上去還是滿清爽的:
- 但是有一點(diǎn)讓我很蛋筒,寫小程序的時(shí)候,查看具體控件的描述樣式,需要跨文件去找
- uniapp則是直接把這些東西放在一個(gè)文件里(19年寫的時(shí)候是這樣的,不知道現(xiàn)在有沒有改),算是一種改善,查找起來方便,但是單個(gè)文件代碼量有點(diǎn)爆炸
- 樣式因?yàn)槭墙唤ocss去處理,層級描述也放在css中,有時(shí)候看代碼看的有點(diǎn)懵逼(是我太菜了)
Flutter直接從根本上樣式結(jié)構(gòu)不分離,結(jié)構(gòu)上直接從上往上下一套到底
- 優(yōu)點(diǎn):修改樣式簡單(方便定位);結(jié)構(gòu)清晰(從上往下看就行了)
- 缺點(diǎn):代碼閱讀,觀感爆炸;不做模塊劃分,后期代碼維護(hù)困難
所以,哪里有十全十美的框架,總是有舍有得。。。
新的事物發(fā)展,必然會(huì)迎來相應(yīng)的阻力
這里假設(shè)一種場景:
你已經(jīng)寫了倆三年Flutter了,各種控件,框架玩的牛的飛起
然后,你聽說:又來了一種神奇的,跨時(shí)代的前端框架,甚至能無縫調(diào)用所有平臺的底層硬件api,omg,反正就是各種6
然后你看到,關(guān)于這種跨時(shí)代框架的文章,在各個(gè)技術(shù)論壇中,瘋狂涌現(xiàn)
此時(shí),你心中會(huì)不會(huì)有絲絲異樣,心想:雜家,這幾年Flutter白寫了?又得去學(xué)這個(gè)新框架了?我踏馬豈不是又變成萌新了!又要天天去群里抱大佬大腿了!
然后你看到那一片片熱點(diǎn)文章,文章下滿是捧上天的評論,,,
此時(shí),你的心中會(huì)不會(huì)有絲波瀾,想當(dāng)一當(dāng)這技術(shù)界的清醒者,情不自禁吟誦:眾人皆醉我獨(dú)醒.....
然后,拿起鍵盤,化身一個(gè)大噴子,以一敵百,不落下風(fēng)
一瞬間,讓你覺得:這個(gè)論壇,現(xiàn)在叫l(wèi)bw論壇!我就是這論壇的王!
角色互換
其實(shí),對于很多言論,我們沒必要在意;角色互換,說不定,對方此刻的行為,就是我們自己以后可能會(huì)做的事。
其實(shí),我們都是打工人,又何必撕來撕去呢?
最后
文中DEMO地址:flutter_use
系列文章
通過上面一些代碼規(guī)范操作后,再配合上GetX的狀態(tài)管理,相信一般的項(xiàng)目,你都能hold的住了
加油,我們都是這條街,最靚的仔
狀態(tài)管理:Flutter GetX使用—簡潔的魅力!
一種優(yōu)雅dialog解決方案:這一次,解決Flutter Dialog的各種痛點(diǎn)!