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

前言

這篇文章是我一直以來很想寫的一篇文章,終於下定決心動筆了。

寫Flutter的小夥伴可能都感受到了:掘金的一些熱門的Flutter文章下,知乎的一些Flutter的話題下或者一些論壇裏面,噴Flutter套娃地獄總是永不過時的一個話題。

如果你不服氣,上去辯駁倆下:「嵌套是你代碼習慣問題,你看我,直接一個Row,反手一個Column,在children中把widget一提,層次分明,年輕人望你耗子尾汁,莫要瞎帶節奏」;然後你可能就被一群人噴成狗,大意了,這帖子沒同一陣營的小夥伴,噴不過,閃了閃了;一般被噴後,不是身經百被噴,都需要一段時間來平復心情。。。

所以,終於我下定決心把這篇文章肝出來,如果你認真看完,你可能會發現:嵌套什麼的都是浮雲,從此你的頁面代碼將變的超級好維護,交互邏輯入口,也變得層次分明。

全篇文章,絕無教大家做事之意,這是在項目中摸爬滾打,被坑出的不得不如此規範的一種行為。

準備

改善

先說說這篇文章能幫你改善什麼問題:

  • 頁面層的widget瘋狂套娃幾千行,後期維護,心態崩了等問題

    • 套娃不劃分頁面,後期需求大變,讓你大改頁面細節甚至結構,那將是非常難受的一件事
  • 邏輯交互事件入口,混雜在widget,難以尋找問題

    • 如果你在頁面層瘋狂套娃,你會發現,就算用了provider,bloc中的cubit,getx之類,你想找到邏輯交互入口,也是一件很累的事情,改樣式那就更方了。。。
    • 這裡再嗶嗶一下,這些框架作者肯定是發現了這種情況,所以bloc才搞出了event層,fish_redux搞出了action層,來統一管理事件及其事件入口。
  • 頁面結構充斥大量細節,結構調整起來困難

上面關於頁面層的這些問題,如果多人協同開發一個大型項目,代碼不規範的話,大概率都是會遇到的(改別人寫的模塊…);後期改需求 ,真的是一種折磨,有種碼海找針的感覺;如果改你自己寫的模塊,那可能還會好點,畢竟你還有點印象,整個模塊的大概思路,還知道怎麼改。如果是改別人寫的模塊,你就需要在大量widget海中,去揣摩別人寫這些widget的意圖,結構一下子也不能理清,十分痛苦,有可能邊改邊罵罵咧咧的。。。

Demo效果

在構思文章的時候,就在想演示的Demo頁面必定不能過於簡單,一個簡單的Demo頁面,怎麼能演示出套娃地獄的改善效果呢?思考了很久,想尋找一個合適demo頁面,周末時在聽喜馬拉雅裏面的盜墓小說,看了看發現頁面,發現整體樣式不錯,咱就仿一個吧!而且整體的頁面複雜度,也足夠來演示了!

喜馬拉雅的這個PC頁面Demo,寫起來真的花費了不少時間,肝痛。

地址

  • Web:仿喜馬拉雅頁面
    • web無法強制設置窗口大小,可能需要你調整下web窗口的寬度,以達到最佳效果
  • Windows:Windows平台安裝包
    • 如果你的電腦開啟了125%的縮放與布局,請打首頁的開啟縮放按鈕
  • 項目地址:flutter_use

說明

代碼已經發佈到github上,web端也已經部署好了,因為使用的CanvasKit模式打包的,首次加載可能比較慢,多等一會,因為發佈在github上,所以可能需要你知道的上網訪問;CanvasKit模式打包的web,在手機上訪問效果也不錯,咱在這絕對不是和前端那些牛逼轟轟的框架比,只是讓自己多了一些可能,也能搞成一些小玩意

  • 關於Widows安裝包
    • Window筆記本高分屏一般會開啟125%的縮放,這時候,存在一個坑比的問題,開啟縮放的時候,Flutter的布局都會相應的縮放,但是坑比的是,整體的窗口並不會縮放,導致內容會積壓整體的窗口,這個問題我也在幾台電腦上,調了好久才發現的。
    • 解決辦法,寫了個手動開啟適配的功能。
    • 關於開啟縮放的按鈕功能,只支持放大125%窗口功能,其它的也不用折騰了,我發現window_size初始化後,第一次設置完窗口尺寸後;然後,再設置窗口時,往大了設置有效,往小了回調會無效,奇怪。。。

效果對比

來對比下仿製的效果吧,有個六七成相似,很多Icon和圖片實在找不到相似,,,這裡demo只提供一個樣式演示,功能別想了,這不是一朝一夕,一個人能搞出的。。。

照片都是從喜馬拉雅web端上搞下來的,數據一直在變,相應欄目的數據有對不上,但是整體樣式大致還是差不多。

其中Banner模塊是區別最大的一塊,用的三方庫只能支持搞成這樣,各位靚仔將就着看看吧。

  • 原版的喜馬拉雅PC頁面

image-20210314165954339

  • 仿製的喜馬拉雅頁面

總結

上面倆組圖片,細節方面對比基本慘不忍睹,但是整體架構上還是比較相似。

圖片因為尺寸限制,沒能展示全部內容,右邊的信息流模塊還有一些信息沒展示出來:最新精選、熱門主播、經典免費榜、有聲書免費榜、相聲評書免費榜。

建議各位彥祖,下載下window安裝包,安裝體驗下;MacOS的於晏們,你們可以看看web展示效果,沒有蘋果那一套東西,又不想折騰黑蘋果,實在打包不了。

咱們馬上來看看怎麼搞規範代碼吧!複雜的模塊,讓你的代碼也能高度可維護!

開搞

分析

  • Android的業務自定義View
    • 在Android裏面有個頁面分模塊的開發思路,將整個頁面劃分成幾個業務的自定義View,我們只需要關注傳入數據源,和對應業務View交互事件回傳信息,這明顯是外觀模式的思想。。。關注了:數據源和交互事件即可,其它的一切都不是我們所關心的,然後主頁面裏面,組合下這些業務view就OK了,徹底拋棄include坑比做法,include讓xml也耦合了,如果改動了一個被多處引用的xml,可能會引發的一些影響,大家心裏可以揣摩揣摩。
  • Flutter的Widget
    • 然後再結合Flutter中那些眾多的系統widget,系統那些Widget基本都屬於功能性的Widget,需要定義巨量的字段傳值,這樣的好處,就是能夠非常顆粒的去控制需要的字段,再配合一些定義的回到函數,就能起到:數據源和交互回調的完美組合。

結合上面的業務View和一切皆Widget的思路,我們可以得出一個結論:搞業務Widget,然後再進行組合!

當然,咱們在這裡得出了一個不是結論的結論,一般來說,這種操作是咱們基本素養,但是具體的操作細節上,還是有很多需要注意的:

  • 業務Widget,也需要劃分模塊
  • Column,Row之類有着天然結構,怎麼去利用
  • 旁枝末節的Widget細節,怎麼去封裝

主模塊封裝

上面咱們一通分析猛如虎後,得出一個結論:搞業務Widget!

關於業務Widget的封裝細節,這裡說明下:

  • 數據源盡量只使用一個,不要使用過多字段去劃分
    • 解釋下,因為我們這是業務性widget,並不是功能性widget,過渡的細分字段輸入,會導致你封裝的widget過長,業務Widget很多時候,只會在你這個模塊,其它模塊一般都很少用的,沒必要去過度的細分字段,開發多了你就會發現,你封裝的那些業務Widget,百分之95的概率,只會在你自己寫的那個頁面吃灰一輩紙。。。
    • 如果是比較通用的widget,那就可以細分字段了或者使用中間實體都OK!通用的模塊開發,關於數據源輸入,就需要考慮一些比較通用的數據格式,例如只需要一個list數據,就不要搞一個實體,只需要一個字段,就不需要搞一個list等等。。。
  • 交互事件,必須使用回調函數,保留出來
    • 關於交互事件,這裡必須要保留出來,給業務層或者邏輯層去處理;一般來說,用戶進入該頁面,點擊或滑動頁面,就是業務事件產生的時候了,這是必須暴露出來的,切記切記。

仿造的喜馬拉雅主模塊

  • 來看看仿造的喜馬拉雅PC頁面主模塊的代碼

    • 這裡使用了一點Getx知識,如果你不了解,可查詢此文章:Flutter GetX使用—簡潔的魅力!
    • 組裝對應的業務Widget的時候:請記住,對應的業務Widget一定要寫上注釋
  • 下面就是主模塊的所有代碼,大家摸着良心說說:

    • 這還死亡嵌套嗎?
    • 這還俄羅斯套娃嗎?
    • 這看着還恐怖嗎?
  • 其實按照下面的封裝,基本是把View和Event做了一個結合了

    • 所有業務Widget的入口,可快速定位到需要修改的業務Widget
    • 所有的事件交互觸發入口,一眼可見,能快速的定義相應業務
class HimalayaPage extends StatelessWidget {
  final HimalayaLogic logic = Get.put(HimalayaLogic());
  final HimalayaState state = Get.find<HimalayaLogic>().state;

  @override
  Widget build(BuildContext context) {
    return himalayaBuildBg(children: [
      //頂部:左邊側邊導航欄 + 右邊信息流
      himalayaBuildTopBg(children: [
        //左邊導航欄
        HimalayaLeftNavigation(
          data: state,
          //導航欄item回調
          onTap: (Rx<HimalayaSubItemInfo> item) => logic.navigationItem(item),
        ),

        //右邊信息流
        himalayaBuildInfoListBg(children: [
          //頂部搜索框及其一些個人信息設置按鈕
          HimalayaPersonalInfo(
            //搜索框輸入監聽
            onChanged: (String msg) => logic.onSearch(msg),
            //左箭頭
            onLeftArrow: () => logic.dealLeftArrow(),
            //右箭頭
            onRightArrow: () => logic.dealRightArrow(),
            //刷新按鈕
            onRefresh: () => logic.onRefreshData(),
            //皮膚按鈕
            onSkin: () => logic.switchSkin(),
            //設置按鈕
            onSetting: () => logic.onSetting(),
          ),

          //右側信息流 - 可滑動部分
          himalayaBuildScrollInfoListBg(children: [
            //輪播圖
            HimalayaBanner(
              data: state.bannerList,
              //具體banner的監聽
              onTap: (int index) => logic.clickBanner(index),
            ),

            //猜你喜歡
            HimalayaGuess(
              data: state.guessList,
              //換一批
              onChange: () => logic.guessChange(),
              //猜你喜歡具體卡片
              onGuess: (HimalayaSubItemInfo item) => logic.guessDetail(item),
            ),

            //最新精選
            HimalayaNewest(
              data: state,
              //分類標題
              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,
              //標題
              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(),
        //進度
        onProgress: () => logic.onProgress(),
        //音量
        onVolume: () => logic.onVolume(),
        //標題
        onSubtitle: () => logic.onSubtitle(),
        //倍速
        onSpeed: () => logic.onSpeed(),
        //定時
        onTiming: () => logic.onTiming(),
        //目錄
        onCatalog: () => logic.onCatalog(),
      ),
    ]);
  }
}

主體細節封裝

一般來說,一個頁面整體基本上是橫向(Row)或者縱向(Column)的結構

咱們仿造的喜馬拉雅模塊也是屬於縱向結構:上下倆大模塊

  • 上模塊:導航欄 + 信息流 => 又分左右模塊

    • 左模塊:左邊的側面導航欄 => 很明顯的縱向布局
    • 右模塊:信息流 => 這就是簡單的縱向結構,從上到下了
  • 下模塊:音頻播放欄 => 完全就是橫向布局了

通過上面的說明,很明顯,Row和Column中children屬性才是我們所關注的,其它的細節描述封裝起來即可

主模塊的很多主體細節,是完全可以封裝起來,新建一個(模塊名_function)文件:

  • himalaya_function.dart:主體部分有很多無需關注的細節,統一放在這個模塊,此處,只需要暴露一個children屬性即可
    • 請勿將這些無關的細節寫在主模塊中,會干擾到我們需要關注的信息,這些主體樣式寫完後,基本就很少去修改了
import 'package:flutter/material.dart';
import 'package:flutter_use/app/base/base_scaffold.dart';
import 'package:flutter_use/app/utils/ui/auto_ui.dart';

///喜馬拉雅整體外層布局設置
Widget himalayaBuildBg({List<Widget> children}) {
  return BaseScaffold(
    backgroundColor: Colors.white,
    body: Column(children: children),
  );
}

///播放控制欄上面的外層布局設置
Widget himalayaBuildTopBg({List<Widget> children}) {
  return Expanded(
    child: Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: children,
    ),
  );
}

///頂部右側信息流外層布局設置
Widget himalayaBuildInfoListBg({List<Widget> children}) {
  return Expanded(
    child: Column(children: children),
  );
}

///頂部右側信息流外層布局設置 - 可滑動部分
Widget himalayaBuildScrollInfoListBg({List<Widget> children}) {
  return Expanded(
    child: Scrollbar(
      child: SingleChildScrollView(
        child: Container(
          width: 860.dp,
          child: Column(children: children),
        ),
      ),
    ),
  );
}

業務Widget封裝

主模塊的封裝還是比較簡單的,只需將主體模塊的細節封裝起來,暴露children屬性即可,然後進行組裝即可

接下來業務Widget封裝,這就是核心所在了

幾個要點

  • 盡量只暴露一個數據源(非通用業務Widget)
  • 所有的事件交互必須暴露出來
  • 主體細節封裝起來
  • children中的widget全部提成方法

children中封裝

先來看看第一種情況,最常見的情況,children的widget,從上到下排列下來,非列表類數據

  • 來看看這個頂部一些功能按鈕的布局,這塊涉及到很多事件交互,所以單獨提成了一個業務Widget

image-20210314212412718

  • 實現代碼:關於業務Widget,這是基石,規範寫好後,後期修改,異常簡單
    • 結合上面的效果圖,再結合下面的代碼,大家應該一眼看出來,就知道是哪個widget方法,對應界面上的哪個控件;如果你想修改哪個控件樣式,直接點進對應的widget方法里修改即可
    • children裏面的每個widget方法上面,請一定一定記得寫上注釋,因為此處才是業務Widget最主要的入口,具體的widget方法寫不寫注釋無所謂了
///搜索框 個人信息 設置等按鈕
class HimalayaPersonalInfo extends StatelessWidget {
  HimalayaPersonalInfo({
    Key key,
    this.onRefresh,
    this.onLeftArrow,
    this.onRightArrow,
    this.onSetting,
    this.onSkin,
    this.onChanged,
  }) : super(key: key);

  .............

  @override
  Widget build(BuildContext context) {
    return _buildBg(children: [
      //左圖標
      _buildLeftArrow(),

      //右圖標
      _buildRightArrow(),

      //刷新圖標
      _buildRefresh(),

      //搜索框
      _buildSearch(),

      //頭像
      _buildHeadImg(),

      //皮膚
      _buildSkin(),

      //設置
      _buildSetting(),
    ]);
  }

  ..........
}
  • 來看下其中的_buildBg方法
    • 可以發現_buildBg主體的這些細節描述,真的是無關緊要的代碼,這個寫完後,基本上,後面都很少去改,所以把它提取出來後,放在牆角吃灰就行了
///搜索框 個人信息 設置等按鈕
class HimalayaPersonalInfo extends StatelessWidget {
  ........

  Widget _buildBg({List<Widget> children}) {
    return Container(
      margin: EdgeInsets.symmetric(vertical: 10.dp, horizontal: 18.dp),
      width: 800.dp,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: children,
      ),
    );
  }
}
  • 關於方法提取
    • 選中你需要提取的Widget代碼
    • 打開 Flutter Outline 選擇左箭頭圖片

image-20210314214406466

  • 填上方法命後,就能自動生成一個widget方法
  • 如果你提取的Widget塊中,還含有一些數據,自動生成的方法都會帶上相應參數,非常方便

image-20210314214520198

列表類樣式封裝

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

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

  • 猜你喜歡模塊

image-20210314220037075

  • 代碼分析:總體是Column布局,分上下倆模塊
    • 上模塊使用Row搞定即可
    • 下模塊是四個卡片,這邊是直接用的寫死List數據源
///猜你喜歡
class HimalayaGuess extends StatelessWidget {
  HimalayaGuess({
    Key key,
    this.onChange,
    this.data,
    this.onGuess,
  }) : super(key: key);

  ..........

  @override
  Widget build(BuildContext context) {
    return _buildBg(children: [
      //標題 + 換一批
      Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
        //標題
        _buildTitle(),

        //換一批
        _buildGuessChange()
      ]),

      //顯示具體信息流
      _buildItemBg(itemBuilder: (item) {
        return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
          //圖片卡片
          _buildPicCard(item),

          //文字描述
          Text(item.title, style: TextStyle(fontSize: 15.sp)),

          //作者
          Text(item.subTitle, style: TextStyle(fontSize: 13.sp, color: Colors.grey)),
        ]);
      })
    ]);
  }

  ..........
}
  • 上述children代碼,整體上還是比較清晰,有點迷糊的,可能就是_buildItemBg,來看看其中代碼
    • 此方法對面暴露了一個itemBuilder參數,這其實是一個回調方法
    • 因為列表類樣式,必須要遍歷整個列表數據,然後,需要把列表遍歷的具體數據,反向傳給Widget,所以必須使用此類的回調方法
///猜你喜歡
typedef HimalayaSubBuilder = Widget Function(HimalayaSubItemInfo item);
class HimalayaGuess extends StatelessWidget {
  
  ...............

  Widget _buildItemBg({HimalayaSubBuilder itemBuilder}) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: data.map((e) {
        return itemBuilder(e);
      }).toList(),
    );
  }
}

關於雙層列表數據源(List的每個具體數據源,又含有List)又該怎麼封裝呢?

  • 倆層List數據源封裝是比較麻煩,這邊以側邊欄舉例
    • 整個布局是一個Column:標題 + 欄目(List數據控制)
    • 欄目:可劃分具體的Item
      • Item:標題 + 欄目(List數據控制)

image-20210314221811228

  • 代碼實現
    • 上面的布局整體是由數據源驅動頁面,數據能控制頁面item生成
///數據源:側邊導航欄目初始數據,簡化了下,數據源太長了
///該數據源都放在state層維護,此處放在這裡,讓大家有個對比
leftItemList = [
    HimalayaItemInfo(title: '推薦', subItemList: [
        HimalayaSubItemInfo(
            title: '發現',
            icon: CupertinoIcons.compass,
            tag: TagHimalayaConfig.find,
            isSelected: true,
        ).obs,
        ..............
    ]),
    HimalayaItemInfo(title: '我聽', subItemList: [
        HimalayaSubItemInfo(
            title: '我的訂閱',
            icon: Icons.star_border,
            tag: TagHimalayaConfig.subscription,
        ).obs,
        .........
    ]),
    HimalayaItemInfo(title: '我創建的聽單', subItemList: [
        HimalayaSubItemInfo(
            title: '我喜歡的聲音',
            icon: Icons.favorite_border,
            tag: TagHimalayaConfig.sound,
        ).obs,
        ............
    ]),
];

///左邊導航欄
class HimalayaLeftNavigation extends StatelessWidget {
  HimalayaLeftNavigation({
    Key key,
    this.data,
    this.onTap,
  }) : super(key: key);

  ........

  @override
  Widget build(BuildContext context) {
    return _buildBg(children: [
      //喜馬拉雅logo圖標
      _buildLogo(),

      //遍歷倆層循環:不同item欄目 - 可點擊,可滑動
      //第一層:標題 + 子item列表
      //第二層:子item詳細布局
      _buildItemListBg(itemBuilder: (item) {
        return [
          //最外層item - 大標題
          _buildTitle(item.title),

          //子欄目 - 列表
          _buildSubItemListBg(
            data: item,
            subBuilder: (subItem) => _buildSubItemBg(data: subItem, children: [
              //選中紅色長方形條塊
              _buildRedTag(subItem),

              //圖標
              _buildItemIcon(subItem),

              //描述
              _buildItemDesc(subItem),
            ]),
          ),
        ];
      }),
    ]);
  }
    
  ..........
}
  • 第一層:來看下第一層_buildItemListBg方法
    • 這玩意不得不套了,需要的屬性太多(滾動,滾動條等),這玩意要是不提出來,放在children,那簡直就是毒瘤了
typedef HimalayaItemBuilder = List<Widget> Function(HimalayaItemInfo item);
class HimalayaLeftNavigation extends StatelessWidget {
  ..........

  Widget _buildItemListBg({HimalayaItemBuilder itemBuilder}) {
    return Expanded(
      child: Scrollbar(
        child: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: data.leftItemList.map((e) {
              return Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: itemBuilder(e),
              );
            }).toList(),
          ),
        ),
      ),
    );
  }
}
  • 第二層
    • 這裏面必須需要第一層遍歷的具體數據源,所以必須增加一個輸入參數
    • 這裡就是常規提取,需要注意的就是傳入的數據源
typedef HimalayaRxSubBuilder = Widget Function(Rx<HimalayaSubItemInfo> item);
class HimalayaLeftNavigation extends StatelessWidget {
    ..........

    Widget _buildSubItemListBg({HimalayaItemInfo data, HimalayaRxSubBuilder subBuilder}){
        return Column(
            children: data.subItemList.map((e) {
                return subBuilder(e);
            }).toList(),
        );
    }
}

總結

經過上面的一通操作,業務Widget立馬變的清爽N倍

大家在寫Flutter的時候,應該能明顯的感覺到,寫頁面擁有高度的自由,樣式、頁面結構及其邏輯全都能耦合在一起;所以在實際開發中,更要注意自己代碼規範。

假設一種情況:你開發完一個模塊,過了幾月之後,需求調整,你要去改這個模塊,看到幾千行的套娃頁面代碼,然後一邊改一邊罵罵咧咧,然後噴是哪個睿智的人寫的,最後打開文件的git注釋(annotate)記錄,如果上面寫滿了你的名字,那豈不是很尷尬。。。

題外話

說一點題外話

實際上寫html也是無限套娃,不同的是,它從根本上做到的樣式結構分離,控件的細節描述,全部交給了css去做,所以頁面整體看上去還是滿清爽的:

  • 但是有一點讓我很蛋筒,寫小程序的時候,查看具體控件的描述樣式,需要跨文件去找
    • uniapp則是直接把這些東西放在一個文件里(19年寫的時候是這樣的,不知道現在有沒有改),算是一種改善,查找起來方便,但是單個文件代碼量有點爆炸
  • 樣式因為是交給css去處理,層級描述也放在css中,有時候看代碼看的有點懵逼(是我太菜了)

Flutter直接從根本上樣式結構不分離,結構上直接從上往上下一套到底

  • 優點:修改樣式簡單(方便定位);結構清晰(從上往下看就行了)
  • 缺點:代碼閱讀,觀感爆炸;不做模塊劃分,後期代碼維護困難

所以,哪裡有十全十美的框架,總是有舍有得。。。

新的事物發展,必然會迎來相應的阻力

這裡假設一種場景:

  • 你已經寫了倆三年Flutter了,各種控件,框架玩的牛的飛起

  • 然後,你聽說:又來了一種神奇的,跨時代的前端框架,甚至能無縫調用所有平台的底層硬件api,omg,反正就是各種6

  • 然後你看到,關於這種跨時代框架的文章,在各個技術論壇中,瘋狂湧現

  • 此時,你心中會不會有絲絲異樣,心想:雜家,這幾年Flutter白寫了?又得去學這個新框架了?我踏馬豈不是又變成萌新了!又要天天去群里抱大佬大腿了!

  • 然後你看到那一片片熱點文章,文章下滿是捧上天的評論,,,

  • 此時,你的心中會不會有絲波瀾,想當一當這技術界的清醒者,情不自禁吟誦:眾人皆醉我獨醒…..

  • 然後,拿起鍵盤,化身一個大噴子,以一敵百,不落下風

  • 一瞬間,讓你覺得:這個論壇,現在叫lbw論壇!我就是這論壇的王!

角色互換

其中,對於很多言論,我們沒必要在意,角色互換,說不定,對方此刻的行為,就是我們自己以後可能會做的事。

小丑竟是我自己是什麼梗小丑竟是我自己是什麼意思出處在哪-站長之家

其實,我們都是打工人,又何必撕來撕去呢?

最後

文中DEMO地址:flutter_use

系列文章

通過上面一些代碼規範操作後,再配合上GetX的狀態管理,相信一般的項目,你都能hold的住了

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

Tags: