Flutter Provider+MVVM搭建通用項(xiàng)目架構(gòu)

前言:
做flutter開發(fā)有些時間了,之前用過GetX和Bloc,在之前的文章中也總結(jié)過這兩個框架的用法和一些常見問題,最近擠出點(diǎn)時間搞了一個Provider,之前在項(xiàng)目中也使用過Provider,但是怎么說呢,那會也是初學(xué)者用的稀里糊涂的,用的不優(yōu)雅,不透徹,今天來盤一盤,MVVM+ Provider的項(xiàng)目寫法.

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

老規(guī)矩,先上效果。
首頁gif
小說.gif
書架.gif
我的.gif
一. Provider 基本用法

Provider有兩個重要的角色。提供者:提供數(shù)據(jù), 消費(fèi)者:消費(fèi)數(shù)據(jù)。他的使用也是圍繞著這兩個角色來展開的。

首先定義提供者,Provider為我們提供了非常多的提供者,總共有八種。但我們比較常用的是ChangeNotifierProvider 和 MultiProvider 和 ChangeNotifierProxyProvider關(guān)于其他的提供者可根據(jù)自己的實(shí)際應(yīng)用場景來??梢杂靡粋€ModelViewModel繼承于或者混入ChangeNotifier,然后讓需要使用數(shù)據(jù)的widget繼承于ChangeNotifierProvider,這樣當(dāng)數(shù)據(jù)變化時就可以通過ChangeNotifierProvider來提供數(shù)據(jù),完成頁面的刷新工作。關(guān)于幾個提供者的用法就不一一說了,感興趣的可以自己百度。

二. Provider也提供了三個消費(fèi)者

Provider第三方是基于InheritedWidget封裝的:InheritedWidget
Provider也提供了三個消費(fèi)者:Provider.of、Consumer(會刷新不必要刷新的組件)、Selector(更精細(xì)化)

1、Provider.of

InheritedWidget有個默認(rèn)的約定:如果狀態(tài)是希望暴露出的,應(yīng)當(dāng)提供一個 “of” 靜態(tài)方法來獲取其對象,開發(fā)者便可直接通過該方法來獲取。

static T of<T>(BuildContext context, {bool listen = true})

其中 listen:默認(rèn)true監(jiān)聽狀態(tài)變化,false為不監(jiān)聽狀態(tài)改變。

Provider.of<T>(context)Provider 為我們提供的靜態(tài)方法,當(dāng)我們使用該方法去獲取值的時候會返回查找到的最近的 T 類型的 provider 給我們,且也不會遍歷整個組件樹。

2、Consumer

Provider 中使用比較頻繁的消費(fèi)者,查看源碼:

Consumer({
  Key? key,
  required this.builder,
  Widget? child,
}) : super(key: key, child: child);

...

@override
Widget buildWithChild(BuildContext context, Widget? child) {
  return builder(
    context,
    Provider.of<T>(context),
    child,
  );
}

發(fā)現(xiàn)它就是通過 Provider.of<T>(context) 來實(shí)現(xiàn)的。而且實(shí)際開發(fā)中使用 Provider.of<T>(context) 比 Consumer 簡單好用太多,那 Consumer有什么優(yōu)勢嗎?

對比一下,我們發(fā)現(xiàn) Consumer 有個 Widget? child,它非常重要,能夠在復(fù)雜項(xiàng)目中,極大地縮小你的控件刷新范圍。

就是在實(shí)際的開發(fā)當(dāng)中只需要將需要刷新的widget放在Consumerbuilder方法中,不需要刷新的方法child中,這樣,大大提升了性能。

3、Selector

Selector 也是一個消費(fèi)者。與Consumer類似,只是對build調(diào)用Widget方法時提供更精細(xì)的控制。 Consumer 是監(jiān)聽一個 Provider 中所有數(shù)據(jù)的變化,Selector 則是監(jiān)聽某一個/多個值的變化。

比如資訊模型 InfoModel, Selector 可以監(jiān)聽里面是否點(diǎn)贊這個屬性的變化,當(dāng)點(diǎn)贊屬性變化才會刷新 點(diǎn)贊 widget, 其他的widget不刷新,可以做到更精細(xì)化的刷新。

但是當(dāng)Selector 監(jiān)聽基本數(shù)據(jù)類型時,比較的是兩個值是否相同,這樣是沒有什么問題的,當(dāng)監(jiān)聽的是對象時,比較的是兩個對象的內(nèi)存地址,所以當(dāng)Selector 監(jiān)聽對象時,對象進(jìn)行增刪操作時并不會引起Selector 的刷新,這種就比較惡心,需要自己處理一下。

我的思路是自定義一個Class,代碼如下

import 'package:flutter/cupertino.dart';

/// select 刷新 對比的是兩個對象的內(nèi)存地址,用這個類來解決這個問題
class SelectorPlusData<T> {
  T? _value;
  int _version = 0;
  int _lastVersion = -1;

  T? get value => _value;

  SelectorPlusData({Key? key, T? value}) {
    _value = value;
  }

  set value(T? value) {
    _version++;
    _value = value;
  }

  bool shouldRebuild() {
    bool isUpdate = _version != _lastVersion;
    if (isUpdate) {
      _lastVersion = _version;
    }
    return isUpdate;
  }
}

這個對象有兩個默認(rèn)值 int _version = 0; int _lastVersion = -1; 當(dāng)對象初始化時 或者 set value時,這兩個version是不會相等的,所有可以用這兩version來判斷是否需要刷新。

Selector中可以封裝成以下代碼,直接調(diào)用上面對象的next.shouldRebuild去決定是否進(jìn)行刷新。

Selector<T, SelectorPlusData>(
                builder: widget.builder,
                selector: widget.plusDataSelector!,
                shouldRebuild: (pre, next) => next.shouldRebuild(),
                child: widget.child,
              )

使用代碼如下:

ProviderSelectorWidget<NovelViewModel, List>(
        viewModel: novelViewModel,
        builder: (context, selectorPlusData, child) {
          return Container();
        },
        plusDataSelector: (context, viewModel) => SelectorPlusData(value: novelViewModel.dataList))

SelectorPlusData將需要監(jiān)聽的數(shù)組包一下,就能完成數(shù)組變化的監(jiān)聽了,對于其他對象也是一樣。

三. 針對Provider+MVVM模式設(shè)計(jì)
1、ViewModel

針對于ViewModel的封裝其實(shí)很簡單,就是繼承于ChangeNotifier監(jiān)聽數(shù)據(jù)變化,它用于向監(jiān)聽器發(fā)送通知。換言之,如果被定義為 ChangeNotifier,你可以訂閱它的狀態(tài)變化。

另外為了方便給列表做上拉刷新和下拉加載,還增加了ScrollControllerRefreshController。在列表的viewModel中可以直接使用。

代碼如下所示:

class BaseViewModel extends ChangeNotifier {
  /// 列表控制器
  final ScrollController scrollController = ScrollController();

  /// 刷新組建控制器
  final RefreshController refreshController = RefreshController(initialRefresh: false);

}
2、State

狀態(tài)層,主要用來定義一些屬性,來進(jìn)行業(yè)務(wù)上的解耦和代碼上的隔離。 state這層必須要單獨(dú)分出來,因?yàn)槟硞€頁面一旦維護(hù)的狀態(tài)很多,將狀態(tài)變量和邏輯方法混在一起,后期維護(hù)會非常頭痛。

State里面定義一個屬性NetState這個屬性根據(jù)網(wǎng)絡(luò)狀態(tài)來賦值,頁面根據(jù)這個NetState來展示不同的頁面,如果說展示暫無數(shù)據(jù)頁面 加載失敗 骨架屏等等,都是根據(jù)NetState來決定的。

3、ProviderConsumerWidget

Consumer在項(xiàng)目中用的還是很普遍,所以直接封裝了一個ProviderConsumerWidget。在項(xiàng)目中需要使用Consumer的地方直接使用這個類即可。

代碼如下:

class ProviderConsumerWidget<T extends ChangeNotifier> extends StatefulWidget {
  final Widget Function(BuildContext context, T value, Widget? child) builder;
  final T viewModel;
  final Widget? child;
  final Function(T)? onReady;

  /// ChangeNotifierProvider 在某種場景下,程序熱啟動時會失效,但是ChangeNotifierProvider<T>.value會一直保持狀態(tài)
  final bool? isValue;
  const ProviderConsumerWidget(
      {super.key,
      required this.viewModel,
      this.child,
      this.onReady,
      required this.builder,
      this.isValue});

  @override
  State<ProviderConsumerWidget> createState() => _ProviderConsumerWidgetState<T>();
}

class _ProviderConsumerWidgetState<T extends ChangeNotifier>
    extends State<ProviderConsumerWidget<T>> {
  @override
  void initState() {
    super.initState();
    if (widget.onReady != null) {
      widget.onReady!(widget.viewModel);
    }
  }

  @override
  Widget build(BuildContext context) {
    if (widget.isValue == true) {
      return ChangeNotifierProvider<T>.value(
        value: widget.viewModel,
        child: Consumer<T>(
          builder: widget.builder,
          child: widget.child,
        ),
      );
    } else {
      return ChangeNotifierProvider<T>(
        create: (_) => widget.viewModel,
        child: Consumer<T>(
          builder: widget.builder,
          child: widget.child,
        ),
      );
    }
  }
}

使用時需要傳遞的參數(shù):

T viewModel:就是viewModel對象,需要監(jiān)聽的數(shù)據(jù)變化的對象。
Widget? child:不需要刷新的widget,可傳可不傳。
Function(T)? onReady:數(shù)據(jù)請求或者數(shù)據(jù)變化的方法,可傳可不傳,不傳的話在view中的initState方法中進(jìn)行數(shù)據(jù)請求一樣的。
builder函數(shù),然后會回調(diào)BuildContext context, T value, Widget? child這三個參數(shù)給view,其中T value返回的就是傳入的viewModel對象,Widget? child就是傳入的不需要刷新的Consumerwidget
bool? isValue: 控制的是ChangeNotifierProvider兩種構(gòu)造方法。
當(dāng)isValuetrue時對應(yīng)的是ChangeNotifierProvider<T>.value的構(gòu)造方法。

ChangeNotifierProvider<T>.value(
        value: widget.viewModel,
        child: Consumer<T>(
          builder: widget.builder,
          child: widget.child,
        ),
      )

當(dāng)isValuefalse時對應(yīng)的是ChangeNotifierProvider<T> create的構(gòu)造方法。

return ChangeNotifierProvider<T>(
        create: (_) => widget.viewModel,
        child: Consumer<T>(
          builder: widget.builder,
          child: widget.child,
        ),
      );
問題:ChangeNotifierProvider<T>.value 和 ChangeNotifierProvider<T> create 差異之處和使用注意事項(xiàng)。

說實(shí)話一開始我并沒有把重點(diǎn)放在這兩個方法上面,上來直接使用的ChangeNotifierProvider<T> create,但是當(dāng)我用Selector來做首頁Tabar切換這個功能時,遇到了問題。就是每當(dāng)我熱重載或者熱啟動時,點(diǎn)擊Tabar就是失效了,然后重啟項(xiàng)目發(fā)現(xiàn)是好使的,只要是一熱更新就失效。對此很是納悶。百度了一下發(fā)現(xiàn)這方面的資料很少,就索性看了下ChangeNotifierProvider的源碼實(shí)現(xiàn)。

ChangeNotifierProvider ->繼承了 ListenableProvider ->繼承了 InheritedProvider ->繼承了SingleChildStatelessWidget ->繼承了SingleChildStatelessWidget

結(jié)論:
ChangeNotifierProvider(builder模式)的父類構(gòu)造器多了一個dispose,當(dāng)ChangeNotifierProviderwidget樹中被移除時會自動調(diào)用dispose方法移除相應(yīng)的數(shù)據(jù),使得內(nèi)存占用永遠(yuǎn)保持著一個合理的水平。

ChangeNotifierProvider.value在被移除widget樹的時候不會自動調(diào)用dispose,需要手動去管理數(shù)據(jù),比如在被移除的時候依然有其它地方想使用這個數(shù)據(jù),并在合適的時候再去手動關(guān)閉。

那么新問題又來了,flutter熱啟動時ChangeNotifierProvider會被移除widget樹嗎?
答案肯定是不會的,我這個問題出現(xiàn)的原因是因?yàn)槲沂褂玫?code>StatelessWidget,當(dāng)熱啟動或者熱重載時,
StatelessWidget會失效,最后把StatelessWidget換成StatefulWidget就解決了這個問題.

但是對ChangeNotifierProvider的探索還是有用的,因?yàn)樵陧?xiàng)目開發(fā)中,經(jīng)常會遇到局部刷新的場景,也就是一個view中可能會用到多個ProviderConsumerWidget或者ProviderSelectorWidget,但是他們又持有同一個viewModel對象,由于ChangeNotifierProvider create是自己釋放對象的。所以這種場景就會造成當(dāng)頁面釋放時,第一個ProviderConsumerWidget釋放的時候把viewModel釋放了,第二個ProviderConsumerWidget釋放的時候發(fā)現(xiàn)自己持有的viewModel已經(jīng)釋放了,就報(bào)錯了。這種情況下就需要使用ChangeNotifierProvider.value初始化方法,然后在頁面的dispose方法中自己手動釋放viewModel。

4、ProviderSelectorWidget
class ProviderSelectorWidget<T extends ChangeNotifier, A> extends StatefulWidget {
  final Widget Function(BuildContext context, dynamic value, Widget? child) builder;
  final T viewModel;
  final Widget? child;
  final Function(T)? onReady;

  /// ChangeNotifierProvider 在某種場景下,程序熱啟動時會失效,但是ChangeNotifierProvider<T>.value會一直保持狀態(tài)
  final bool? isValue;

  /// 判斷是否需要刷新的字段 特別需要指明的是selector的結(jié)果,必須是不可變的對象。 如果同一個對象,只是改變對象屬性,那shouldRebuild的兩個值永遠(yuǎn)是相等的。
  final SelectorPlusData Function(BuildContext, T)? plusDataSelector;
  final A Function(BuildContext, T)? selector;

  const ProviderSelectorWidget(
      {super.key,
      required this.viewModel,
      this.child,
      this.onReady,
      required this.builder,
      this.selector,
      this.plusDataSelector,
      this.isValue});

  @override
  State<ProviderSelectorWidget> createState() => _ProviderSelectorWidgetState<T, A>();
}

class _ProviderSelectorWidgetState<T extends ChangeNotifier, A>
    extends State<ProviderSelectorWidget<T, A>> {
  @override
  void initState() {
    super.initState();
    if (widget.onReady != null) {
      widget.onReady!(widget.viewModel);
    }
  }

  @override
  Widget build(BuildContext context) {
    if (widget.isValue == true) {
      return ChangeNotifierProvider<T>.value(
        value: widget.viewModel,
        child: widget.plusDataSelector != null
            ? Selector<T, SelectorPlusData>(
                builder: widget.builder,
                selector: widget.plusDataSelector!,
                shouldRebuild: (pre, next) => next.shouldRebuild(),
                child: widget.child,
              )
            : Selector<T, A>(
                builder: widget.builder,
                selector: widget.selector!,
                shouldRebuild: (pre, next) => pre != next,
                child: widget.child,
              ),
      );
    } else {
      return ChangeNotifierProvider<T>(
        create: (_) => widget.viewModel,
        child: widget.plusDataSelector != null
            ? Selector<T, SelectorPlusData>(
                builder: widget.builder,
                selector: widget.plusDataSelector!,
                shouldRebuild: (pre, next) => next.shouldRebuild(),
                child: widget.child,
              )
            : Selector<T, A>(
                builder: widget.builder,
                selector: widget.selector!,
                shouldRebuild: (pre, next) => pre != next,
                child: widget.child,
              ),
      );
    }
  }
}

使用時需要傳遞的參數(shù):

T viewModel:就是viewModel對象,需要監(jiān)聽的數(shù)據(jù)變化的對象。
Widget? child:不需要刷新的widget,可傳可不傳。
Function(T)? onReady:數(shù)據(jù)請求或者數(shù)據(jù)變化的方法,可傳可不傳,不傳的話在view中的initState方法中進(jìn)行數(shù)據(jù)請求一樣的。
builder函數(shù),然后會回調(diào)BuildContext context, dynamic value, Widget? child這三個參數(shù)給view,其中dynamic value返回的就是傳入的A對象,
例子

ProviderSelectorWidget<TabberViewModel, int>(
      viewModel: tabberViewModel,
      selector: (context, index) => tabberViewModel.selectIndex,
      onReady: (viewModel) {
        viewModel.changeSelectIndex(0);
      },
      builder: (context, index, child) {
        return _buildPage(context, index);
      },
    );

此時value返回的就是傳入的int類型的index

Widget? child就是傳入的不需要刷新的Consumerwidget
bool? isValue: 控制的是ChangeNotifierProvider兩種構(gòu)造方法。
當(dāng)isValuetrue時對應(yīng)的是ChangeNotifierProvider<T>.value的構(gòu)造方法。
final A Function(BuildContext, T)? selector當(dāng)監(jiān)聽的數(shù)據(jù)是基本數(shù)據(jù)類型時使用selector
final SelectorPlusData Function(BuildContext, T)? plusDataSelector當(dāng)監(jiān)聽的數(shù)據(jù)是對象時使用plusDataSelector
例子

ProviderSelectorWidget<NovelViewModel, List>(
        viewModel: novelViewModel,
        onReady: (viewModel) {
          getListData();
        },
        builder: (context, selectorPlusData, child) {
          return Container();
        },
        plusDataSelector: (context, viewModel) => SelectorPlusData(value: novelViewModel.dataList))

此時value返回的就是傳入的SelectorPlusData對象,獲取返回值時使用selectorPlusData.value來獲取。

5、BaseView設(shè)計(jì)
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../routers/navigator_utils.dart';
import '../widgets/easy_loading.dart';
import 'base_state.dart';
import 'base_will_pop.dart';

typedef BodyBuilder = Widget Function(BaseState baseState, BuildContext context);

abstract class BasePage extends StatefulWidget {
  const BasePage({Key? key}) : super(key: key);

  @override
  BasePageState createState() => getState();

  ///子類實(shí)現(xiàn)
  BasePageState getState();
}

abstract class BasePageState<T extends BasePage> extends State<T> {
  /// 是否渲染buildPage內(nèi)容
  bool _isRenderPage = false;

  /// 是否渲染導(dǎo)航欄
  bool isRenderHeader = true;

  /// 導(dǎo)航欄顏色
  Color? navColor;

  /// 左右按鈕橫向padding
  final EdgeInsets _btnPaddingH = EdgeInsets.symmetric(horizontal: 14.w, vertical: 14.h);

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

  /// 頂部狀態(tài)欄高度
  double statusBarH = 0.0;

  /// 底部安全區(qū)域高度
  double bottomSafeBarH = 0.0;

  /// 頁面背景色
  Color pageBgColor = const Color(0xFFF9FAFB);

  /// header顯示頁面title
  String pageTitle = '';

  /// 是否允許某個頁iOS滑動返回,Android物理返回鍵返回
  bool isAllowBack = true;

  bool resizeToAvoidBottomInset = true;

  /// 是否允許點(diǎn)擊返回上一頁
  bool isBack = true;

  @override
  void initState() {
    super.initState();
    _getBarInfo();
    _addFirstFrameListener();
    print('當(dāng)前類:$runtimeType');
  }

  @override
  void dispose() {
    XsEasyLoading.dismiss();
    super.dispose();
  }

  void _addFirstFrameListener() {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      buildComplete();
    });
  }

  void buildComplete() {}

  /// 獲取屏幕狀態(tài)欄和頂部導(dǎo)航欄的高度
  void _getBarInfo() {
    WidgetsBinding.instance.addPostFrameCallback((mag) {
      statusBarH = ScreenUtil().statusBarHeight;
      bottomSafeBarH = ScreenUtil().bottomBarHeight;
      // if (SystemUtil.isIOS() && ScreenUtil().bottomBarHeight > 0) {
      //   bottomSafeBarH = 14.h;
      // }
      setState(() {
        _isRenderPage = true;
      });
    });
  }

  /// 點(diǎn)擊左邊按鈕
  void onTapLeft() {
    if (!isBack) return;
    NavigatorUtils.unFocus();
    NavigatorUtils.pop(context);
  }

  ///抽象header上的組件
  Widget left() {
    return Image(
      image: const AssetImage("assets/images/back_black.png"),
      height: 20.h,
      width: 20.w,
    );
  }

  Widget right() => SizedBox(width: 20.w);

  /// 左邊組件
  Widget _left() {
    return InkWell(
      onTap: onTapLeft,
      child: Container(
        padding: _btnPaddingH,
        child: left(),
      ),
    );
  }

  /// 右邊組件
  Widget _right() {
    return Container(
      padding: _btnPaddingH,
      child: right(),
    );
  }

  /// 頁面
  Widget _content() {
    return Container(
      color: pageBgColor,
      height: 1.sh,
      width: 1.sw,
      child: buildPage(context),
    );
  }

  ///子類實(shí)現(xiàn),構(gòu)建各自頁面UI控件
  Widget buildPage(BuildContext context);

  @override
  Widget build(BuildContext context) {
    return AnnotatedRegion<SystemUiOverlayStyle>(
      sized: false,
      value: SystemUiOverlayStyle.light,
      child: BaseWillPopPage(
        isAllowBack: isAllowBack,
        child: Scaffold(
          appBar: isRenderHeader == true
              ? AppBar(
                  centerTitle: true,
                  title: Text(pageTitle,
                      style: TextStyle(
                          color: Colors.black, fontSize: 17.sp, fontWeight: FontWeight.w500)),
                  leading: _left(),
                  elevation: 0.2,
                  actions: [_right()],
                  backgroundColor: navColor ?? Colors.white,
                )
              : null,
          body: _isRenderPage == false ? const SizedBox() : _content(),
          resizeToAvoidBottomInset: resizeToAvoidBottomInset,
        ),
      ),
    );
  }
}

6、MultiStateWidget設(shè)計(jì),根據(jù)state里面的netState狀態(tài),決定頁面的展示。

代碼如下

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:mvvm_provider/base/empty_widget.dart';
import 'package:mvvm_provider/base/time_out_widget.dart';
import '../widgets/placeholders.dart';
import 'base_state.dart';
import 'net_error_widget.dart';

/// 空視圖 builder方法 回調(diào)函數(shù)
typedef Builder = Widget Function(BuildContext context);

enum PlaceHolderType {
  /// ListView站位
  listViewPlaceHolder,

  /// GridView站位
  gridViewPlaceHolder,

  /// StaggeredGrid 站位
  staggeredGridPlaceHolder,

  /// 詳情 站位
  detailPlaceHolder,

  /// 無骨架屏展示loading
  noPlaceHolder,
}

class MultiStateWidget extends StatelessWidget {
  final Widget? emptyWidget;
  final Widget? errorWidget;
  final String? emptyText;
  final String? errorText;
  final String? timeOutText;
  final NetState netState;
  final Builder builder;
  final Function? refreshMethod;
  final PlaceHolderType placeHolderType;
  const MultiStateWidget(
      {super.key,
      this.emptyWidget,
      this.errorWidget,
      required this.netState,
      required this.placeHolderType,
      required this.builder,
      this.refreshMethod,
      this.emptyText,
      this.errorText,
      this.timeOutText});

  @override
  Widget build(BuildContext context) {
    Widget resultWidget;
    switch (netState) {
      case NetState.error404State:
        resultWidget = NetErrorWidget(title: errorText ?? '網(wǎng)絡(luò)404了');
        break;
      case NetState.emptyDataState:
        resultWidget = EmptyWidget(title: emptyText ?? '暫無數(shù)據(jù)');
        break;
      case NetState.errorShowRefresh:
        resultWidget = NetErrorWidget(title: errorText ?? '網(wǎng)絡(luò)錯誤', refreshMethod: refreshMethod);
        break;
      case NetState.timeOutState:
        resultWidget = TimeOutWidget(title: timeOutText ?? '加載超時請重試', refreshMethod: refreshMethod);
        break;
      case NetState.loadingState:
        if (placeHolderType == PlaceHolderType.gridViewPlaceHolder) {
          resultWidget = const GridViewPlaceHolder();
        } else if (placeHolderType == PlaceHolderType.listViewPlaceHolder) {
          resultWidget = const ListViewPlaceHolder();
        } else if (placeHolderType == PlaceHolderType.staggeredGridPlaceHolder) {
          resultWidget = const StaggeredGridPlaceHolder();
        } else if (placeHolderType == PlaceHolderType.detailPlaceHolder) {
          resultWidget = const DetailPlaceHolder();
        } else {
          resultWidget = const SizedBox();
        }
        break;
      case NetState.unknown:
        resultWidget = const EmptyWidget(title: '未知錯誤,請退出重試');
        break;
      case NetState.cancelRequest:
        resultWidget = const SizedBox();
        break;
      case NetState.dataSuccessState:
        resultWidget = builder(context);
        break;
    }
    return resultWidget;
  }
}

7、項(xiàng)目截圖如下
項(xiàng)目截圖.png
四. 一個簡單的列表寫法案例

思路就是 定義一個viewModel 繼承自BaseViewModel,在viewModel中請求接口獲取數(shù)據(jù),根據(jù)接口返回?cái)?shù)據(jù)給state 賦值,然后一定記得給state里面的netStata完成賦值操作(這個很重要,因?yàn)轫撁媸峭ㄟ^netStata的狀態(tài)來展示的,如果不賦值,默認(rèn)一直展示loading或者骨架屏)。

創(chuàng)建一個view繼承自BasePage,然后在需要使用的數(shù)據(jù)的地方使用ProviderConsumerWidget或者ProviderSelectorWidget,然后builder里面使用MultiStateWidget,將netState傳遞給MultiStateWidget,完成頁面的加載。

代碼如下:

viewModel
import 'package:flutter/cupertino.dart';
import 'package:mvvm_provider/base/base_state.dart';
import 'package:mvvm_provider/model/banner_model.dart';
import 'package:mvvm_provider/page/home/states/home_state.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../../../base/base_view_model.dart';
import '../../../config/handle_state.dart';
import '../../../model/response_model.dart';
import '../../../net/ltt_https.dart';
import '../../../net/http_config.dart';
import '../../../widgets/easy_loading.dart';
import '../model/cartoon_model.dart';

class HomeViewModel extends BaseViewModel {
  /// 創(chuàng)建state
  HomeState homeState = HomeState();

  /// 獲取列表數(shù)據(jù)
  Future<void> getListData(String url, int number) async {
    if (url == '') {
      /// 沒有更多數(shù)據(jù)了
      refreshController.refreshCompleted();
      refreshController.loadComplete();
      refreshController.loadNoData();

      homeState.netState = NetState.dataSuccessState;
      notifyListeners();
      return;
    }
    ResponseModel? responseModel =
        await LttHttp().request<CarDataModel>(url, method: HttpConfig.get);
    homeState.netState = HandleState.handle(responseModel, successCode: 0);
    if (homeState.netState == NetState.dataSuccessState) {
      CarDataModel carDataModel = responseModel.data;
      if (number == 1) {
        homeState.dataList = carDataModel.feeds;
        if ((homeState.dataList ?? []).isEmpty) {
          homeState.netState = NetState.emptyDataState;
        }
      } else {
        homeState.dataList?.addAll(carDataModel.feeds ?? []);

        /// 顯示沒有更多數(shù)據(jù)了
        if ((carDataModel.feeds ?? []).isEmpty) {
          refreshController.loadNoData();
        }
      }
      refreshController.refreshCompleted();
      refreshController.loadComplete();
    }
    notifyListeners();
  }

  void changeIsLike() {
    homeState.isLike = !homeState.isLike;
    notifyListeners();
  }
}

Model

省略。。。使用 JsontoDartBeanAction 插件來完成

View
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mvvm_provider/base/multi_state_widget.dart';
import '../../../base/base_grid_view.dart';
import '../../../base/base_stateful_page.dart';
import '../../../base/provider_consumer_widget.dart';
import '../../../routers/home_router.dart';
import '../../../routers/navigator_utils.dart';
import '../view_model/home_view_model.dart';
import '../widgets/car_toon_widget.dart';
import '../widgets/test.dart';

class HomePage extends BasePage {
  const HomePage({super.key});

  @override
  BasePageState<BasePage> getState() => _HomePageState();
}

class _HomePageState extends BasePageState<HomePage> {
  HomeViewModel homeViewModel = HomeViewModel();
  final ImagePicker _picker = ImagePicker();

  @override
  void initState() {
    super.initState();
    super.pageTitle = '首頁';
    isBack = false;

    _onRefresh();
  }

  @override
  Widget left() {
    return const SizedBox();
  }

  /// 請求分頁
  int _pageNum = 1;

  /// 上拉加載
  void _onLoading() {
    _pageNum++;
    getListData();
  }

  /// 下拉刷新
  void _onRefresh() {
    _pageNum = 1;
    getListData();
  }

  void getListData() {
    homeViewModel.getListData(getUrl(_pageNum), _pageNum);
  }

  String getUrl(int number) {
    String urlStr = '';
    if (_pageNum == 1) {
      urlStr = 'https://run.mocky.io/v3/8d98fef7-634f-4122-a837-8c9ee892365e';
    } else if (_pageNum == 2) {
      urlStr = 'https://run.mocky.io/v3/d415f483-bdbf-445d-ae12-703d1fd01e97';
    } else if (_pageNum == 3) {
      urlStr = 'https://run.mocky.io/v3/a9faaec6-d70f-4365-95b2-6abdd35a6e28';
    } else {
      urlStr = '';
    }
    return urlStr;
  }

  @override
  Widget buildPage(BuildContext context) {
    // return Container(
    //   color: Colors.white,
    //   child: TextButton(
    //     child: const Text('點(diǎn)擊拍照'),
    //     onPressed: () async {
    //       //拍照
    //       XFile? file = await _picker.pickImage(source: ImageSource.camera,imageQuality: 100);
    //       NavigatorUtils.push(context, HomeRouter.waterMarkPage,
    //           arguments: {"imagePath": file!.path});
    //     },
    //   ),
    // );


    return ProviderConsumerWidget<HomeViewModel>(
      viewModel: homeViewModel,
      builder: (context, viewModel, child) {
        return MultiStateWidget(
            netState: homeViewModel.homeState.netState,
            placeHolderType: PlaceHolderType.gridViewPlaceHolder,
            builder: (BuildContext context) {
              return Container(
                color: Colors.deepOrange,
                child: BaseGridView(
                  enablePullDown: true,
                  enablePullUp: true,
                  onRefresh: _onRefresh,
                  onLoading: _onLoading,
                  refreshController: viewModel.refreshController,
                  scrollController: viewModel.scrollController,
                  data: viewModel.homeState.dataList ?? [],
                  padding: EdgeInsets.all(10.h),
                  childAspectRatio: 0.7,
                  crossAxisSpacing: 10.w,
                  mainAxisSpacing: 10.h,
                  crossAxisCount: 2,
                  bgColor: const Color(0xFFF3F4F8),
                  itemBuilder: (context22, index) {
                    return CarToonWidget(
                      index: index,
                      model: viewModel.homeState.dataList![index],
                      onTap: () async {
                        NavigatorUtils.push(context, HomeRouter.homeDetailPage,
                            arguments: {"imageUrl": homeViewModel.homeState.dataList?[index].image});
                      },
                    );
                  },
                ),
              );
            });
      },
    );
  }
}

如果感興趣可以自行下載Demo觀看。

8.頁面多接口串行+局部刷新寫法案例


需求分析:
這個頁面分為三個接口返回?cái)?shù)據(jù),分別是小說主信息接口,系列作品接口,和更多推薦接口。

一個頁面使用三個接口,正常來說使用并發(fā)方式請求完成所有的接口再拼裝數(shù)據(jù)比較好,這樣用時較短對于用戶用戶體驗(yàn)較好。但是也有的情況第二個接口請求的入?yún)?,需要第一個接口的返回值,這種就必須串行了。因此,針對這個頁面串行和并發(fā)兩種方式都寫了一下。

頁面在滑動時,導(dǎo)航欄的透明度是隨著ListView的滑動距離來改變的,在滑動的過程中只有導(dǎo)航欄這個widget在變化,其他的widget并不會發(fā)生變化,所以沒有必要在根節(jié)點(diǎn)處刷新整個widget,僅僅需要刷新導(dǎo)航欄widget就可以了。完成這個局部刷新有三種思路吧,都是可以的。

  1. 導(dǎo)航欄widget 抽離出去,在這個小的widget內(nèi)部,使用 setStates 方法來完成刷新。

  2. 使用 兩個ProviderConsumerWidget 和 兩個ViewModel來實(shí)現(xiàn)。
    ViewModel A 請求接口,完成數(shù)據(jù)組裝,發(fā)送通知notifyListeners()。
    ProviderConsumerWidget A 放在頁面根節(jié)點(diǎn),根據(jù)數(shù)據(jù)完成整個頁面的加載展示。
    ViewModel B 更新ListView滑動改變距離,發(fā)送通知notifyListeners()。
    ProviderConsumerWidget B 放在導(dǎo)航欄widget子節(jié)點(diǎn),根據(jù)ListView滑動距離的改變來刷新 widget。

  3. 使用 兩個ProviderSelectorWidget 和 一個ViewModel來實(shí)現(xiàn)。
    ViewModel 請求接口,完成數(shù)據(jù)組裝,更新ListView滑動改變距離,發(fā)送通知notifyListeners()
    ProviderSelectorWidget A 放在頁面根節(jié)點(diǎn),根據(jù)數(shù)據(jù)完成整個頁面的加載展示。根據(jù)小說的主id來決定主頁面刷新還是不刷新。
    ProviderSelectorWidget B 放在導(dǎo)航欄widget子節(jié)點(diǎn),根據(jù)ListView滑動距離的改變來刷新 widget。(最能體現(xiàn) Selector 顆粒刷新 優(yōu)勢)

代碼實(shí)現(xiàn)

自行下載Demo觀看吧。

9.頁面多接口并發(fā)+局部刷新寫法案例


還是這個頁面,只不過是第一種方式的優(yōu)化版了,接口是并發(fā)請求的,局部刷新用的是 兩個ProviderSelectorWidget 和 一個ViewModel來實(shí)現(xiàn)的。

并發(fā)請求代碼

/// 請求全部數(shù)據(jù)
  getAllData() async {
    await Future.wait<dynamic>([getMainData(), getSeriesData(), getRecommendData()]).then((value) {
      if (value[0] == null || value[1] == null || value[2] == null) {
        netState = NetState.errorShowRefresh;
        notifyListeners();
        return;
      }
      mainModel = value[0];
      seriesList = value[1];
      recommendList = value[2];
      netState = NetState.dataSuccessState;
      notifyListeners();
    }).catchError((error) {
      netState = NetState.errorShowRefresh;
      notifyListeners();
    });
  }

  /// 請求主數(shù)據(jù)
  getMainData() async {
    ResponseModel? responseModel = await LttHttp().request<CartoonModelData>(
        'https://run.mocky.io/v3/315de364-a765-40e1-8383-f36d3ffe5bdd',
        method: HttpConfig.get);
    return responseModel.data;
  }

  /// 請求系列數(shù)據(jù)
  getSeriesData() async {
    ResponseModel? responseModel = await LttHttp().request<CartoonSeriesData>(
        'https://run.mocky.io/v3/c1fecbc3-296f-44c4-970c-5861970cc11b',
        method: HttpConfig.get);
    CartoonSeriesData cartoonSeriesData = responseModel.data;
    return cartoonSeriesData.seriesComics;
  }

  /// 請求推薦數(shù)據(jù)
  getRecommendData() async {
    ResponseModel? responseModel = await LttHttp().request<CartoonRecommendData>(
        'https://run.mocky.io/v3/7b0096eb-a1ea-4f3c-8273-e6e700a01128',
        method: HttpConfig.get);
    CartoonRecommendData cartoonRecommendData = responseModel.data;
    return cartoonRecommendData.infos;
  }

注意點(diǎn):需要根據(jù)三個接口的狀態(tài)來完成頁面netState賦值操作。

結(jié)束:

就寫到這里吧,針對于MVVM+Provider的項(xiàng)目架構(gòu)設(shè)計(jì)已經(jīng)可以滿足項(xiàng)目使用了,一直認(rèn)為,技術(shù)就是用來溝通的,沒有溝通就沒有長進(jìn),在此,歡迎各種大佬吐槽溝通。Coding不易,如果感覺對您有些許的幫助,歡迎點(diǎn)贊評論。

聲明:

僅開源供大家學(xué)習(xí)使用,禁止從事商業(yè)活動,如出現(xiàn)一切法律問題自行承擔(dān)?。?!

僅學(xué)習(xí)使用,如有侵權(quán),造成影響,請聯(lián)系本人刪除,謝謝

Demo下載地址 Demo
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容