淺嘗Flutter(一)

背景

公司新項(xiàng)目端三,公司想要更高效的人力開發(fā)方式锯茄,之前的app采用MVVM+Kotlin的android和Swift開發(fā)IOS原生單獨(dú)開發(fā)搔驼,優(yōu)勢(shì)和劣勢(shì)都很明顯谈火,開發(fā)效率低,但是apk的體驗(yàn)和大小都比較極致匙奴,起碼我負(fù)責(zé)的android是同行業(yè)競品里包體積最小的堆巧,甚至比平均水平小50%,這次B端項(xiàng)目,這種極致的體驗(yàn)要求不高谍肤,反而是效率更重要啦租。

通過對(duì)比RN,Uniapp荒揣,F(xiàn)lutter方式的優(yōu)劣勢(shì)篷角,以及團(tuán)隊(duì)人員的組成以移動(dòng)端為主情況,最后我的決策是使用Flutter進(jìn)行開發(fā)系任。

使用flutter的好處主要是恳蹲,主要是移動(dòng)端上手更快,本身是一個(gè)偏UI引擎的開發(fā)語言俩滥,而且本身有正規(guī)軍google維護(hù)嘉蕾,所以對(duì)生態(tài)還是比較放心。

開始

  • 開發(fā)工具支持:VScode霜旧,Android studio
  • 環(huán)境要求:Xcode , android SDK, Flutter SDK
    一開始也不清楚错忱,直接使用的Flutter doctor去檢測的環(huán)境,實(shí)際上Xcode和android SDK 應(yīng)該是可以2選一的挂据,我因?yàn)槭茿ndroid開發(fā)以清,后面因?yàn)榇疟P空間不夠,選擇把Xcode卸載了崎逃,實(shí)際開發(fā)中基本沒用過掷倔,因?yàn)槲覀僆OS打包有IOS開發(fā)負(fù)責(zé),如果你需要打包IOS个绍,Xcode還是需要的勒葱。

1、框架

實(shí)際這個(gè)過程也是從0到1障贸,沒有找參考項(xiàng)目错森。
但是框架搭建都大差不差,都是從一些基礎(chǔ)服務(wù)和基礎(chǔ)能力開始篮洁。
基礎(chǔ)服務(wù)提供埋點(diǎn)涩维,推送,網(wǎng)絡(luò)請(qǐng)求袁波,圖片加載瓦阐,數(shù)據(jù)庫,文件存儲(chǔ)篷牌,sp存儲(chǔ)等等睡蟋,
基礎(chǔ)UI能力下拉刷新,頁面的loading和重試框架枷颊,基類戳杀,通用彈窗等该面。
跨端信息獲取,統(tǒng)一的theme信卡,頁面跳轉(zhuǎn)路由等隔缀。

因?yàn)槭嵌嗳藚f(xié)作邊開發(fā)邊學(xué)習(xí),首先約定好文件和變量命名規(guī)則傍菇,然后項(xiàng)目搭建前面1-2天由我開始把最迫切的基礎(chǔ)能力網(wǎng)絡(luò)請(qǐng)求猾瘸,圖片加載,文件,sp存儲(chǔ)搭建好丢习,并且編寫入口啟動(dòng)頁和登錄頁牵触。
然后把各類基礎(chǔ)能力采用組合的方式把能力封裝出去,base_stateful和base_state_less咐低。

對(duì)于Flutter這種所有的頁面和view全部集成自Widget的然后在分成2個(gè)stateful和stateless控件的設(shè)計(jì)方式揽思,屬實(shí)適應(yīng)了一會(huì)兒,這讓我想起了android的Compose和單項(xiàng)數(shù)據(jù)流架構(gòu)的MVI见擦。

實(shí)際在使用Flutter之前我也沒用過Compose绰更,我們用Flutter寫這個(gè)項(xiàng)目的時(shí)候?qū)嶋HKMM還沒有成熟穩(wěn)定版本,現(xiàn)在已經(jīng)有了穩(wěn)定版本锡宋,接下來我可能會(huì)接觸下KMM。

對(duì)于基礎(chǔ)能力各家架構(gòu)各不相同特恬,我采用了組合的方式把主要的能力插拔式的采用組合的方式集成在基類中执俩。

abstract class BaseState<T extends BaseStatefulWidget> extends State<T> {
  StreamController<PageData> pageStatus = StreamController<PageData>();

  //頁面加載失敗顯示
  Widget? error;

  //頁面加載
  Widget? loading;

  //空頁面
  Widget? empty;

  //內(nèi)容
  PagerController? pageController;

  @override
  void initState() {
    super.initState();
  }

  @override
  void setState(VoidCallback fn) {
    if (mounted) {
      super.setState(fn);
    } else {
      print("Widget unmount. skip setState");
    }
  }

  DialogLoadingController? _dialogLoadingController;

  ///在頁面上方顯示一個(gè) loading widget
  ///共有兩種方法,showProgressDialog是其中一種
  ///具體參見 : loading_progress_widget.dart
  ///如果需要在 [dismissProgressDialog] 方法后跳轉(zhuǎn)到其他頁面或者執(zhí)行什么
  ///使用 參數(shù) [afterDismiss] 和 [dismissProgressDialog.afterPopTask] 不建議同時(shí)用使用癌刽。
  ///[loadingTimeOut] 超時(shí)時(shí)間役首,單位秒。
  /// * 抵達(dá)這個(gè)時(shí)間后显拜,將自動(dòng)關(guān)閉loading widget,默認(rèn)15秒
  showProgressDialog(
      {Widget? progress,
      Color? bgColor,
      int loadingTimeOut = 15,
      AfterLoadingCallback? afterDismiss}) {
    if (_dialogLoadingController == null) {
      _dialogLoadingController = DialogLoadingController();
      Navigator.of(context)
          .push(PageRouteBuilder(
              settings: const RouteSettings(name: loadingLayerRouteName),
              ///使用默認(rèn)值效果可能不好
              transitionDuration: const Duration(milliseconds: 0),
              reverseTransitionDuration: const Duration(milliseconds: 0),
              opaque: false,
              pageBuilder: (ctx, animation, secondAnimation) {
                return LoadingProgressState(
                        controller: _dialogLoadingController,
                        progress: progress,
                        bgColor: bgColor,
                        loadingTimeOut: loadingTimeOut)
                    .transformToWidget();
              }))
          .then((value) {
        _dialogLoadingController?.invokeAfterPopTask(value);
        _dialogLoadingController = null;
        if (afterDismiss != null) {
          afterDismiss(value);
        }
      });
    }
  }

  ///隱藏loading
  /// * 注意: [afterPopTask] 會(huì)在loading隱藏后被調(diào)用衡奥,但是如果用戶主動(dòng)取消的話,將不會(huì)被調(diào)用
  /// *       用戶主動(dòng)取消的將會(huì)調(diào)用[showLoading]的[afterDismiss]
  void dismissProgressDialog({AfterLoadingCallback? afterPopTask}) {
    if (afterPopTask != null) {
      _dialogLoadingController?.holdAfterPopTask(task: afterPopTask);
    }
    _dialogLoadingController?.dismissDialog();
  }

  @override
  void dispose() {
    _dialogLoadingController = null;
    pageController?.dispose();
    pageController = null;
    super.dispose();
  }

  //把任務(wù)拋到gpu繪制的下一幀執(zhí)行
  void postAtNextFrame(Function task) {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      task.call();
    });
  }
}

其中page_controller中實(shí)現(xiàn)了頁面的基礎(chǔ)狀態(tài)和UI邏輯远荠,并且實(shí)現(xiàn)基類的頁面可以自定義空頁面和loading等矮固。

而pageStatus表示頁面的狀態(tài)流,page_controller會(huì)負(fù)責(zé)監(jiān)聽這個(gè)狀態(tài)流進(jìn)行頁面狀態(tài)UI的實(shí)時(shí)更新譬淳。

網(wǎng)絡(luò)請(qǐng)求采用了通用的Dio庫并進(jìn)行了基礎(chǔ)封裝档址。

extension NetFunc on BaseState {
  ///get請(qǐng)求
  ///[showLoadingAuto] 是否自動(dòng)顯示和隱藏loading ,推薦在頁面加載完成之后的其他網(wǎng)絡(luò)請(qǐng)求中使用
  ///[toast] 是否自動(dòng)彈出失敗請(qǐng)求toast
  ///
  ///[antiContentFlow]只會(huì)走數(shù)據(jù)加載的狀態(tài)流
  ///主要應(yīng)用場景邻梆,在于頁面已經(jīng)加載出來之后守伸,如果繼續(xù)走flow會(huì)走完整的
  ///[loading -> content | error | empty 流]
  ///某些場景不需要loading
  ///
  ///[enableFlow]走完整的數(shù)據(jù)流loading , content , empty  failure
  Future<dynamic> get(String path, Map<String, dynamic> params,
      {Function? successCallBack,
      Function? errorCallBack,
      bool showLoadingAuto = false,
      bool enableFlow = true,
      bool antiContentFlow = false,
      bool toast = true,
      bool refresh = false,
      CancelToken? cancelToken}) async {
    if (showLoadingAuto) showProgressDialog();

    //模擬弱網(wǎng)
    //await Future<void>.delayed(Duration(seconds: 2));
    cancelToken?.cancel();

    if (enableFlow) pageStatus.add(PageData(PageStatus.LOADING));

    int currentTime = DateTime.now().millisecondsSinceEpoch;

    await NetManager.getInstance().get(path, params, (data) {
      if (showLoadingAuto) {
        dismissProgressDialog(afterPopTask: (e) {
          try {
            successCallBack?.call(data);
          } on Exception catch (error) {
            //做了一些日志處理
          }
        });
      } else {
        try {
          successCallBack?.call(data);
        } on Exception catch (error) {
          //做了一些日志處理
        }
      }

      if (enableFlow || antiContentFlow) {
        if (pageController?.emptyPrediction?.call(data) == true) {
          pageStatus.add(PageData(PageStatus.EMPTY));
        } else {
          // pageStatus.add(PageData(PageStatus.ERROR));
          pageStatus.add(PageData(PageStatus.CONTENT, data: data));
        }
      }
    }, (BaseBeanEntity error) {
      if (enableFlow) {
        pageStatus.add(PageData(error.code != ResultCode.NO_NETWORK
            ? PageStatus.ERROR
            : PageStatus.ERROR_NO_NET));
      }

      if (toast) {
        ToastUtils.showToast(
            context: context, msg: "Net Error Code:${error.code},${error.msg}");
      }
      if (showLoadingAuto) dismissProgressDialog();

      errorCallBack?.call(error);
    },startRequestTime : currentTime);
  }

  ///post請(qǐng)求
  ///[showLoadingAuto] 是否自動(dòng)顯示和隱藏loading
  ///[toast] 是否自動(dòng)彈出失敗請(qǐng)求toast
  ///
  ///[antiContentFlow]只會(huì)走數(shù)據(jù)加載的狀態(tài)流
  ///主要應(yīng)用場景浦妄,在于頁面已經(jīng)加載出來之后尼摹,如果繼續(xù)走flow會(huì)走完整的
  ///[loading -> content | error | empty 流]
  ///某些場景不需要loading
  ///
  ///[enableFlow]走完整的數(shù)據(jù)流loading , content , empty  failure
  Future<dynamic> post(String path, Map<String, dynamic> params,
      {Function? successCallBack,
      Function? errorCallBack,
      bool showLoadingAuto = false,
      bool enableFlow = true,
      bool antiContentFlow = false,
      bool toast = true,
      CancelToken? cancelToken}) async {
    if (showLoadingAuto) showProgressDialog();
    cancelToken?.cancel();

    //模擬弱網(wǎng)
    // await Future.delayed(const Duration(seconds: 3));

    if (enableFlow) pageStatus.add(PageData(PageStatus.LOADING));

    int currentTime = DateTime.now().millisecondsSinceEpoch;

    NetManager.getInstance().post(path, params, (data) async {
      if (showLoadingAuto) {
        dismissProgressDialog(afterPopTask: (e) {
          try {
            successCallBack?.call(data);
          } on Exception catch (error) {
            //做了一些日志處理
          }
        });
      } else {
        try {
          successCallBack?.call(data);
        } on Exception catch (error) {
           //做了一些日志處理
        }
      }

      if (enableFlow || antiContentFlow) {
        if (pageController?.emptyPrediction?.call(data) == true) {
          pageStatus.add(PageData(PageStatus.EMPTY));
        } else {
          pageStatus.add(PageData(PageStatus.CONTENT, data: data));
        }
      }

    }, (error) {
      if (enableFlow) {
        pageStatus.add(PageData(error.code != ResultCode.NO_NETWORK
            ? PageStatus.ERROR
            : PageStatus.ERROR_NO_NET));
      }

      if (toast) {
        ToastUtils.showToast(
            context: context, msg: "Net Error Code:${error.code}见芹,${error.msg}");
      }
      if (showLoadingAuto) dismissProgressDialog();

      errorCallBack?.call(error);
    },startRequestTime : currentTime);
  }
}

實(shí)際這個(gè)網(wǎng)絡(luò)擴(kuò)展類擴(kuò)展了base基類的能力,它主要的作用是根據(jù)網(wǎng)絡(luò)請(qǐng)求對(duì)UI流進(jìn)行分發(fā)蠢涝。底層的請(qǐng)求代碼就不貼了玄呛,非核心。

實(shí)際上頁面的狀態(tài)比這里定義的頁面狀態(tài)惠赫,更復(fù)雜把鉴。
比如:

  • 頁面頁面中多請(qǐng)求,誰該主導(dǎo)loading狀態(tài)還是儿咱,說要等所有請(qǐng)求完再顯示Content庭砍。
  • 頁面加載完之后,頁面條件變化混埠,需要重新請(qǐng)求部分內(nèi)容怠缸,這個(gè)時(shí)候覆蓋頁面的loading和蒙層的loading誰該顯示。
  • 還有Flutter的單項(xiàng)數(shù)據(jù)流钳宪,在setState中執(zhí)行的邏輯不能繼續(xù)觸發(fā)setState等揭北。
  • 還有復(fù)雜的路由跳轉(zhuǎn)維護(hù),電商業(yè)務(wù)的創(chuàng)建訂單結(jié)束復(fù)雜的頁面跳轉(zhuǎn)分發(fā)邏輯需要強(qiáng)大的路由基類支持等吏颖。

篇幅太長下章繼續(xù) To be continue....

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末搔体,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子半醉,更是在濱河造成了極大的恐慌疚俱,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缩多,死亡現(xiàn)場離奇詭異呆奕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)衬吆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門梁钾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人逊抡,你說我怎么就攤上這事姆泻。” “怎么了冒嫡?”我有些...
    開封第一講書人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵麦射,是天一觀的道長。 經(jīng)常有香客問我灯谣,道長潜秋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任胎许,我火速辦了婚禮峻呛,結(jié)果婚禮上罗售,老公的妹妹穿的比我還像新娘。我一直安慰自己钩述,他們只是感情好寨躁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著牙勘,像睡著了一般职恳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上方面,一...
    開封第一講書人閱讀 51,443評(píng)論 1 302
  • 那天放钦,我揣著相機(jī)與錄音,去河邊找鬼恭金。 笑死操禀,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的横腿。 我是一名探鬼主播颓屑,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼耿焊!你這毒婦竟也來了揪惦?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤罗侯,失蹤者是張志新(化名)和其女友劉穎丹擎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體歇父,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年再愈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了榜苫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡翎冲,死狀恐怖垂睬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情抗悍,我是刑警寧澤驹饺,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站缴渊,受9級(jí)特大地震影響赏壹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜衔沼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一蝌借、第九天 我趴在偏房一處隱蔽的房頂上張望昔瞧。 院中可真熱鬧,春花似錦菩佑、人聲如沸自晰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽酬荞。三九已至,卻和暖如春瞧哟,著一層夾襖步出監(jiān)牢的瞬間混巧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來泰國打工绢涡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留牲剃,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓雄可,卻偏偏與公主長得像凿傅,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子数苫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

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

  • 本篇教程聊一聊flutter中http的坑聪舒,閱讀本教程之前假設(shè)你已經(jīng)掌握了flutter的一些基礎(chǔ)知識(shí),比如環(huán)境搭...
    YuTao_閱讀 23,540評(píng)論 10 7
  • 假設(shè)你已經(jīng)掌握了flutter的一些基礎(chǔ)知識(shí)虐急,比如環(huán)境搭建箱残,簡單的dart語法,及flutter組件化思想止吁。那么你...
    YuTao_閱讀 22,183評(píng)論 0 21
  • 轉(zhuǎn)載 https://blog.csdn.net/weixin_44911775/article/details/...
    七月不下雨閱讀 11,229評(píng)論 0 0
  • 假設(shè)你已經(jīng)掌握了flutter的一些基礎(chǔ)知識(shí)被辑,比如環(huán)境搭建,簡單的dart語法敬惦,及flutter組件化思想盼理。那么你...
    代碼移動(dòng)工程師閱讀 218評(píng)論 0 0
  • 在互聯(lián)網(wǎng)飛速發(fā)展的今天,相信很多程序開發(fā)者對(duì) GitHub 很熟悉俄删,我們?cè)谶@里學(xué)習(xí)知識(shí)宏怔、分享自己的開源庫或者開源代...
    騰飛Tenfay閱讀 3,118評(píng)論 0 6