Flutter 改善套娃地獄問題(仿喜馬拉雅PC頁面舉例)

前言

這篇文章是我一直以來很想寫的一篇文章,終于下定決心動(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%的縮放與布局,請打首頁的開啟縮放按鈕
  • 項(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頁面
image-20210314165954339
  • 仿制的喜馬拉雅頁面
image

總結(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
image-20210314212412718
  • 實(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è)寫完后,基本上,后面都很少去改,所以把它提取出來后,放在墻角吃灰就行了
///搜索框 個(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 選擇右箭頭圖片
image-20210314214406466
  • 填上方法名后,就能自動(dòng)生成一個(gè)widget方法
  • 如果你提取的Widget塊中,還含有一些數(shù)據(jù),自動(dòng)生成的方法都會(huì)帶上相應(yīng)參數(shù),非常方便
image-20210314214520198

單層列表樣式封裝

類列表樣式的封裝也是比較關(guān)鍵的,直接從頭莽尾式的提取是不行,這邊有一絲調(diào)整

這里就以猜你喜歡模塊舉例

  • 猜你喜歡模塊
image-20210314220037075
  • 代碼分析:總體是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ù)
///猜你喜歡
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ù)控制)
image-20210314221811228
  • 代碼實(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的住了

加油,我們都是這條街,最靚的仔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容