Flutter 基于getX搭建通用項目架構(gòu)

從事flutter開發(fā)工作兩年多了,做了幾個項目,從剛開始的Provider到現(xiàn)在的GetX,Bloc都在項目中使用過,本篇文章結(jié)合GetX特性和項目中的實際運用整理了一套基于GetX搭建通用項目架構(gòu),代碼會上傳gitee.特此分享,歡迎探討交流。

老規(guī)矩,先上效果泌绣。
首頁
商品分類
資訊
購物車
我的
一. 網(wǎng)絡(luò)封裝

網(wǎng)絡(luò)主要使用的是dioretrofit來實現(xiàn)的,關(guān)于dio的封裝,思路基本上和網(wǎng)上的教程大同小異.大致思路就是創(chuàng)建options對象,設(shè)置超時時間,域名,responseType,超時時間等等.然后在創(chuàng)建dio單例對象,給dio對象添加攔截器,我基本上添加三個攔截器分別為

* MiddleInterceptor (獲取請求路徑,請求參數(shù),打印日志)
* ErrorInterceptor (獲取錯誤原因,打印日志)
* ApiResultInterceptor(獲取后端返回結(jié)果,處理邏輯,數(shù)據(jù)返回,比如多設(shè)備登錄同一賬號異常處理等等)

在這里代碼就不貼出來了,有興趣的可以下載demo查看.
為什么要使用retrofit?
retrofit是一個非常好用強大的代碼生成器,支持POST GET PATCH PUT等請求.定義好請求方法,返回類型執(zhí)行一行命令就會自動生成代碼,在Controller中直接使用自己使用好的model即可.
常用命令:

* flutter packages pub run build_runner build
* flutter pub run build_runner build --delete-conflicting-outputs

代碼定義如下所示

/// 列表接口
  @GET("searchV5")
  Future<Result<InfoWorkModel>> getInfoListData(
    @Query('pn') int page,
    @Query('ps') int pageSize,
    @Query('q') String name,
    @Query('t') String t,
  );

  /// 詳情接口
  @GET("pc/items/info")
  Future<Result<InfoWorkModel>> getInfoDetailData(
    @Query('entityId') int entityId,
  );

  /// 點贊接口
  @POST("thumbsUpOrDown")
  Future<Result<InfoWorkModel>> likeThumbsUpOrDown(
      @Body() Map<String, String> param);

Result 類里面就是返回的最外層數(shù)據(jù)包括 code,msg,data其中data是一個 泛型T可以接受任何類型,然后再定義data里面對象,也就是InfoWorkModel,這樣層層定義保障每個對象都轉(zhuǎn)化成modle即可.

yaml如下代碼所示:

dependencies:
  retrofit: '>=4.0.0 <5.0.0'
  dio: ^4.0.6
dev_dependencies:
  retrofit_generator: '>=5.0.0 <6.0.0'
  build_runner: '>=2.3.0 <4.0.0' 
  json_serializable: ^4.4.0
二. 路由設(shè)計

將真心話使用了GetX之后基本就不需要路由設(shè)計了,GetX都封裝好了,在這里就說幾個Api吧.

1.跳轉(zhuǎn)界面
Get.toNamed('/test');
2.跳轉(zhuǎn)界面?zhèn)髦?br> Get.toNamed('/test', arguments: {'id': '0'});
3.跳轉(zhuǎn)界面并執(zhí)行頁面返回事件(比如刷新數(shù)據(jù))
Get.toNamed("/test",)?.then((value) { print('執(zhí)行事件'); });
4.返回上一界面
Get.back();
5.返回上一界面并回調(diào)傳值
Get.back(result: { 'id':1 });
6.返回上一界面并關(guān)閉當前頁面
Get.offNamed("/test");
7.返回上一界面并關(guān)閉之前所有頁面
Get.offAll("/test");
8.返回指定頁面
Get.until((route) => Get.currentRoute == '/test');

三.狀態(tài)管理

1.使用GetBuilder:
buildContent中直接返回GetBuilder,然后在Controller中數(shù)據(jù)發(fā)生變化后調(diào)用update()方法整個頁面都會發(fā)生變化碉碉,適合列表頁面的開發(fā)阁危。

GetBuilder<HomeController>(
     builder: (_) {
       return Container();
     },
   );

2.使用RxX..Obx()配合使用進行小部件的狀態(tài)刷新。

關(guān)于狀態(tài)管理的設(shè)計原理:參考這篇文章
四.Base類設(shè)計
  1. BaseView 設(shè)計
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get_state_manager/get_state_manager.dart';

abstract class BaseView<T> extends GetView<T> {
  BaseView({Key? key}) : super(key: key);

  /// 狀態(tài)欄高度
  double statusBarH = ScreenUtil().statusBarHeight;

  /// 導航欄高度
  double navBarH = AppBar().preferredSize.height;

  /// 安全區(qū)域高度
  double safeBarH = ScreenUtil().bottomBarHeight;

  /// 設(shè)置背景顏色
  Color? contentColor;

  /// 設(shè)置標題文字
  String? navTitle;

  /// 設(shè)置導航欄顏色
  Color? navColor;

  /// 設(shè)置左邊按鈕
  Widget? leftButton;

  /// 設(shè)置左邊寬度
  double? leftWidth;

  /// 設(shè)置右邊按鈕數(shù)組
  List<Widget>? rightActionList;

  /// 是否隱藏導航欄
  bool? isHiddenNav;

  /// 設(shè)置主主視圖內(nèi)容(子類不實現(xiàn)會報錯)
  Widget buildContent();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: contentColor ?? Colors.white,
        appBar: isHiddenNav == true
            ? null
            : AppBar(
                backgroundColor: navColor ?? Colors.white70,
                title: Text(
                  navTitle ?? '',
                ),
                leading: leftButton ?? const SizedBox(),
                leadingWidth: leftWidth ?? 0,
                actions: rightActionList ?? [],
              ),
        body: buildContent());
  }
}

新創(chuàng)建的widget都繼承自BaseView 這樣在集成于BaseView 的widget中可以方便設(shè)置和獲取一些常用的頁面屬性以及完成頁面基本布局。
  1. BaseController 設(shè)計
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';

enum NetState {
  /// 初始狀態(tài)
  initializeState,
  /// 加載狀態(tài)
  loadingState,

  /// 錯誤狀態(tài),顯示失敗界面
  errorState,

  /// 錯誤狀態(tài),只彈錯誤信息
  erroronlyTotal,

  /// 錯誤狀態(tài),顯示刷新按鈕
  errorshowRelesh,

  /// 沒有更多數(shù)據(jù)
  noMoreDataState,

  /// 是否還有更多數(shù)據(jù)
  hasMoreDataState,

  /// 空數(shù)據(jù)狀態(tài)
  emptyDataState,

  /// 數(shù)據(jù)獲取成功狀態(tài)
  dataSussessState,
}

abstract class BaseController extends SuperController {
  /// 定義網(wǎng)絡(luò)狀態(tài)方便子控制器使用
  NetState netState = NetState.initializeState;

  @override
  void onReady() {
    super.onReady();
    initData();
  }

  @override
  void onDetached() {
    debugPrint("a11111");
  }

  @override
  void onInit() {
    // TODO: implement onInit
    super.onInit();
  }

  @override
  void onInactive() {
    debugPrint("a11112");
  }

  @override
  void onPaused() {
    //徹底離開回調(diào)
    debugPrint("a11113");
  }

  @override
  void onResumed() {
    //徹底恢復回調(diào)
    debugPrint("a11114");
  }

  void initData();
}

這個類不需要多說就是重寫了一下Getx的生命周期方法,以及添加了一個網(wǎng)絡(luò)狀態(tài)的屬性,為什么把網(wǎng)絡(luò)狀態(tài)加在BaseController中呢奶镶?稍后再講。

分析:正常來說到了這一步就可以利用這兩個base來做項目了陪拘,無非就是在Controller里面請求數(shù)據(jù)厂镇,然后根據(jù)接口的返回在Controller里面更新netState網(wǎng)絡(luò)狀態(tài),然后在widget里面根據(jù)netState的狀態(tài)左刽,判斷返回LoadingWidget捺信,或者EmptyWidget,或者ErrorWidget悠反,數(shù)據(jù)請求成功返回SussessWidget残黑,總之需要在widget里面寫大量的判斷邏輯,并且凡是涉及到根據(jù)網(wǎng)絡(luò)請求接口返回來展示頁面(實際上90%都是這樣)的都要把這些判斷復制一遍斋否,非常麻煩梨水,那么重點來了:
  1. BaseCommonView 設(shè)計
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get_demo/base/base_view.dart';
import 'package:get_demo/widget/loading_widget.dart';
import '../widget/empty_status.dart';
import 'base_common_controller.dart';
import 'base_controller.dart';

abstract class BaseCommonView<T> extends BaseView<T> {
  BaseCommonView({Key? key}) : super(key: key);

  /// 創(chuàng)建空視圖 (子視圖實現(xiàn)的話 Widget就是子視圖實現(xiàn)的)
  Widget creatEmptyWidget() {
    return const EmptyStatusWidget(
      emptyType: EmptyStatusType.noMessage,
    );
  }

  /// 創(chuàng)建錯誤視圖 (子視圖實現(xiàn)的話 Widget就是子視圖實現(xiàn)的)
  Widget creatFailWidget(BaseCommonController controller) {
    return EmptyStatusWidget(
      emptyType: EmptyStatusType.fail,
      refreshTitle: '重新加載',
      width: 1.sw,
      height: 1.sh,
      onTap: () {
        /// 重新請求數(shù)據(jù)
        controller.getnetworkdata(controller.configNetWorkParmas());
      },
    );
  }

  /// 創(chuàng)建頁面主視圖
  Widget creatCommonView(BaseCommonController controller, Widget commonView) {
    return _refresherListView(controller, commonView);
  }

  Widget _refresherListView(
      BaseCommonController controller, Widget commonView) {
    if (controller.netState == NetState.loadingState) {
      /// loading 不會有這個狀態(tài),只是寫一個這樣的判斷吧(控制器里面已經(jīng)封裝好了單例了,防止在網(wǎng)絡(luò)層直接操作控制不了loading的場景)
      return const LoadingWidget();
    } else if (controller.netState == NetState.emptyDataState) {
      /// 返回站位視圖
      return creatEmptyWidget();
    } else if (controller.netState == NetState.errorshowRelesh) {
      /// 返回站位刷新視圖
      return creatFailWidget(controller);
    } else if (controller.netState == NetState.dataSussessState) {
      return commonView;
    } else if (controller.netState == NetState.initializeState) {
      return const SizedBox();
    } else {
      return const Center(child: Text('未知情況,待排查'));
    }
  }
}

繼承自BaseCommonViewwidget(view)只需要調(diào)用creatCommonView方法,
然后將自己綁定的controller(繼承BaseCommonController稍后會講)和自己頁面(view)將要實現(xiàn)的頁面widget傳遞BaseCommonView就可以了茵臭,在BaseCommonView中會根據(jù)controller中的網(wǎng)絡(luò)狀態(tài)(這就是把網(wǎng)絡(luò)狀態(tài)定義在BaseController中的原因了疫诽,子類都要使用不需要每次都創(chuàng)建了)展示不同的widget,這樣就不需要在每個頁面中進行重復的判斷了旦委,然后如果想自定義EmptyStatusWidget那么只需要在子頁面重寫creatEmptyWidget方法或者creatFailWidget方法就可以了奇徒。
  1. BaseCommonController 設(shè)計
    和BaseCommonView配套的還有BaseCommonController。方便網(wǎng)絡(luò)請求的方法和一些參數(shù)調(diào)用缨硝。
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../../base/abstract_network.dart';
import '../../base/base_controller.dart';

/**
 *  普通視圖控制器
 * */

abstract class BaseCommonController extends BaseController
    with AbstractNetWork {

  @override
  void getnetworkdata(Map<String, dynamic> info) {
    // TODO: implement getnetworkdata
  }

  @override
  Map<String, dynamic> configNetWorkParmas() {
    // TODO: implement configNetWorkParmas
    throw UnimplementedError();
  }
}

BaseCommonController就是為了方便在子Controller進行網(wǎng)絡(luò)請求而設(shè)計的摩钙。子類直接重寫網(wǎng)絡(luò)請求方法getnetworkdata和設(shè)置Map就可以了,不需要重復定義查辩。BaseCommonController混入with AbstractNetWork類胖笛。
  1. AbstractNetWork 設(shè)計


    image.png

6.總結(jié):有了這幾個base類在開發(fā)中就非常方便了,只需要注意自己的widget(view)和業(yè)務(wù)邏輯controller就行了宜岛,不需要在view中進行繁瑣的判斷了长踊,還是非常適合快速開發(fā)的,另外在項目里面還有針對列表設(shè)計的BaseListControllerBaseListView使用方法和BaseCommonView大同小異萍倡,在這里就不貼代碼了身弊,感興趣的可以自己下載 demo觀看。

總體設(shè)計圖

總體設(shè)計
六.架構(gòu)升級

新增state狀態(tài)層列敲,負責定義屬性阱佛,賦值等操作。因為隨著業(yè)務(wù)的增加戴而,一個頁面在業(yè)務(wù)層既要定義屬性又要處理邏輯代碼維護起來很是頭層瘫絮。

七.局部刷新方法

為了性能考慮,有些需要局部刷新的頁面填硕,就不需要刷新全部麦萤。有兩種方法實現(xiàn)吧扁眯。
1.使用Obx來實現(xiàn)壮莹,在需要改變的widget外面嵌套一層Obx,當狀態(tài)改變時只刷新被Obx包裹的widget姻檀。
2.使用GetBuilder來實現(xiàn)命满,多個GetBuilder使用同一個CounterController的變量,但是我們只想更新其中一個GetBuilder的變量绣版,就可以在添加id參數(shù)

update(["id"]); // 只對id為“id”的GetBuilder

view層

GetBuilder<CounterController1>( // 不刷新
  builder: (controller){
   return Text(
     "count的值為:${counter.count}",
     style: const TextStyle(color: Colors.redAccent,fontSize: 20),
   );
},
八.GetX 遇到的坑

跳轉(zhuǎn)相同頁面時比如說:1-2-3-2,會發(fā)現(xiàn)2頁面的數(shù)據(jù)不會刷新,這是GetX內(nèi)部的緩存機制造成,傳一個唯一tag值就可以了.

六.項目配置項,手動更改環(huán)境(后端域名)

實際開發(fā)中會有開發(fā)換進,測試環(huán)境,發(fā)布環(huán)境,正式環(huán)境等,每次給測試發(fā)包會很麻煩,所以就做了一個切換環(huán)境的功能.思路就是通過全局環(huán)境一個字段來進行讀取baseUrl,有需要直接看代碼即可,思路很簡單.

demo地址請移步: 項目地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末胶台,一起剝皮案震驚了整個濱河市歼疮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌诈唬,老刑警劉巖韩脏,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異铸磅,居然都是意外死亡赡矢,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門阅仔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吹散,“玉大人,你說我怎么就攤上這事八酒】彰瘢” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵羞迷,是天一觀的道長袭景。 經(jīng)常有香客問我,道長闭树,這世上最難降的妖魔是什么耸棒? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮报辱,結(jié)果婚禮上与殃,老公的妹妹穿的比我還像新娘。我一直安慰自己碍现,他們只是感情好幅疼,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著昼接,像睡著了一般爽篷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上慢睡,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天逐工,我揣著相機與錄音,去河邊找鬼漂辐。 笑死泪喊,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的髓涯。 我是一名探鬼主播袒啼,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蚓再?” 一聲冷哼從身側(cè)響起滑肉,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎摘仅,沒想到半個月后靶庙,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡实檀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了按声。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片膳犹。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖签则,靈堂內(nèi)的尸體忽然破棺而出须床,到底是詐尸還是另有隱情,我是刑警寧澤渐裂,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布豺旬,位于F島的核電站,受9級特大地震影響柒凉,放射性物質(zhì)發(fā)生泄漏族阅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一膝捞、第九天 我趴在偏房一處隱蔽的房頂上張望坦刀。 院中可真熱鬧,春花似錦蔬咬、人聲如沸鲤遥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盖奈。三九已至,卻和暖如春狐援,著一層夾襖步出監(jiān)牢的瞬間钢坦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工啥酱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留场钉,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓懈涛,卻偏偏與公主長得像逛万,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

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