Flutter Provider+MVVM搭建通用项目架构

2,561 阅读18分钟
前言:
做flutter开发有些时间了,之前用过GetX和Bloc,在之前的文章中也总结过这两个框架的用法和一些常见问题,最近挤出点时间搞了一个Provider,之前在项目中也使用过Provider,但是怎么说呢,那会也是初学者用的稀里糊涂的,用的不优雅,不透彻,今天来盘一盘,MVVM+ Provider的项目写法.

Flutter 基于getX搭建通用项目架构 Flutter 基于 Bloc搭建通用项目架构

老规矩,先上效果。
首页gif小说.gif书架.gif我的.gif
一. Provider 基本用法

Provider有两个重要的角色。提供者:提供数据, 消费者:消费数据。他的使用也是围绕着这两个角色来展开的。

首先定义提供者,Provider为我们提供了非常多的提供者,总共有八种。

1. 基础 Provider(Provider)

  • 用途:提供不可变数据(值或对象),适用于不需要监听变化的场景。

  • 特点

    • 不会自动更新 UI(除非重新构建)。
    • 适用于配置、常量或一次性数据。
  • 示例

    dart

    Provider<String>(
      create: (context) => "Hello, World!",
      child: MyWidget(),
    )
    

2. ChangeNotifierProvider

  • 用途:提供 ChangeNotifier(可变状态),适用于需要监听状态变化的场景。

  • 特点

    • 当 notifyListeners() 被调用时,自动更新依赖的 UI。
    • 适用于状态管理(如 MVVM 或简单状态管理)。
  • 示例

    dart

    ChangeNotifierProvider<Counter>(
      create: (context) => Counter(),
      child: MyWidget(),
    )
    

3. ListenableProvider

  • 用途:提供 Listenable 对象(如 AnimationController),比 ChangeNotifierProvider 更通用。

  • 特点

    • 适用于任何实现了 Listenable 的对象(不限于 ChangeNotifier)。
  • 示例

    dart

    ListenableProvider<AnimationController>(
      create: (context) => AnimationController(),
      child: MyWidget(),
    )
    

4. ValueListenableProvider

  • 用途:提供 ValueListenable<T> (如 ValueNotifier),适用于监听单个值的变化。

  • 特点

    • 比 ChangeNotifier 更轻量级,仅监听一个值的变化。
  • 示例

    dart

    ValueListenableProvider<int>(
      value: ValueNotifier(0),
      child: MyWidget(),
    )
    

5. StreamProvider

  • 用途:提供 Stream 数据(如 Firebase、WebSocket 等异步数据流)。

  • 特点

    • 自动监听 Stream 并更新 UI。
    • 可以处理初始值、加载状态和错误。
  • 示例

    dart

    StreamProvider<User>(
      create: (context) => FirebaseAuth().userChanges(),
      initialData: null,
      child: MyWidget(),
    )
    

6. FutureProvider

  • 用途:提供 Future 数据(如 API 请求)。

  • 特点

    • 自动处理 Future 的加载、完成和错误状态。
    • 适用于一次性异步操作(如网络请求)。
  • 示例

    dart

    FutureProvider<Profile>(
      create: (context) => Api.fetchProfile(),
      initialData: Profile.loading(),
      child: MyWidget(),
    )
    

7. ProxyProvider

  • 用途:基于其他 Provider 的值动态生成新值(不监听变化)。

  • 特点

    • 适用于组合或计算数据,但不自动更新 UI。
  • 示例

    dart

    ProxyProvider<AuthService, UserProfile>(
      update: (context, auth, previous) => UserProfile(auth.userId),
    )
    

8. ChangeNotifierProxyProvider

  • 用途:基于其他 Provider 的值动态更新 ChangeNotifier(监听变化)。

  • 特点

    • 适用于需要依赖其他 Provider 的可变状态管理。
  • 示例

    dart

    ChangeNotifierProxyProvider<AuthService, UserController>(
      create: (context) => UserController(),
      update: (context, auth, controller) => controller..updateUser(auth.user),
    )
    

9. MultiProvider

  • 用途:同时提供多个 Provider,避免嵌套。

  • 特点

    • 适用于多个 Provider 同时使用的情况。
  • 示例

    dart

    MultiProvider(
      providers: [
        Provider(create: (_) => Api()),
        ChangeNotifierProvider(create: (_) => UserModel()),
        StreamProvider(create: (_) => SocketStream()),
      ],
      child: MyApp(),
    )
    

但我们比较常用的是ChangeNotifierProvider, MultiProvider, ChangeNotifierProxyProvider, ProxyProvider关于其他的提供者可根据自己的实际应用场景来了解。可以用一个ModelViewModel继承于或者混入ChangeNotifier,然后让需要使用数据的widget注册Provider,这样当数据变化时就可以通过Provider 来提供数据,完成页面的操作。

二. Provider也提供了三个消费者

Provider第三方是基于InheritedWidget封装的:InheritedWidget Provider也提供了三个消费者:Provider.ofConsumer(会刷新不必要刷新的组件)Selector(更精细化)

1、Provider.of

InheritedWidget有个默认的约定:如果状态是希望暴露出的,应当提供一个 “of” 静态方法来获取其对象,开发者便可直接通过该方法来获取。

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

其中 listen:默认true监听状态变化,false为不监听状态改变。

Provider.of<T>(context)Provider 为我们提供的静态方法,当我们使用该方法去获取值的时候会返回查找到的最近的 T 类型的 provider 给我们,且也不会遍历整个组件树。

2、Consumer

Provider 中使用比较频繁的消费者,查看源码:

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,
  );
}

发现它就是通过 Provider.of(context) 来实现的。而且实际开发中使用 Provider.of(context) 比 Consumer 简单好用太多,那 Consumer有什么优势吗?

对比一下,我们发现 Consumer 有个 Widget? child,它非常重要,能够在复杂项目中,极大地缩小你的控件刷新范围。

就是在实际的开发当中只需要将需要刷新的widget放在Consumerbuilder 方法中,不需要刷新的方法child中,这样,大大提升了性能。

3、Selector

Selector 也是一个消费者。与Consumer类似,只是对build调用Widget方法时提供更精细的控制。 Consumer 是监听一个 Provider 中所有数据的变化,Selector 则是监听某一个/多个值的变化。

比如资讯模型 InfoModel, Selector 可以监听里面是否点赞这个属性的变化,当点赞属性变化才会刷新 点赞 widget, 其他的widget不刷新,可以做到更精细化的刷新。

但是当Selector 监听基本数据类型时,比较的是两个值是否相同,这样是没有什么问题的,当监听的是对象时,比较的是两个对象的内存地址,所以当Selector 监听对象时,对象进行增删操作时并不会引起Selector 的刷新,这种就比较恶心,需要自己处理一下。

我的思路是自定义一个Class,代码如下

import 'package:flutter/cupertino.dart';

/// select 刷新 对比的是两个对象的内存地址,用这个类来解决这个问题
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;
  }
}

这个对象有两个默认值 int _version = 0; int _lastVersion = -1; 当对象初始化时 或者 set value时,这两个version是不会相等的,所有可以用这两version来判断是否需要刷新。

Selector 中可以封装成以下代码,直接调用上面对象的next.shouldRebuild 去决定是否进行刷新。

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 将需要监听的数组包一下,就能完成数组变化的监听了,对于其他对象也是一样。

三. 针对Provider+MVVM模式设计
1、ViewModel

针对于ViewModel的封装其实很简单,就是继承于ChangeNotifier监听数据变化,它用于向监听器发送通知。换言之,如果被定义为 ChangeNotifier,你可以订阅它的状态变化。

维护了一个状态属性,写了个构造方法。

final S state;
BaseViewModel(this.state);

这样继承于 BaseViewModelViewModel只要 HomeViewModel() : super(HomeState());这样一行代码就得到一个全局的state不需要在每次都去创建了,也是按照Bloc的思路来实现的.

例子:

class HomeViewModel extends BaseViewModel<HomeState> {
  HomeViewModel() : super(HomeState());

  /// 获取列表数据
  Future<void> getListData(bool isRefresh) async {
    int page = state.page;
    if (isRefresh) {
      page = 1;
    } else {
      page++;
    }
    ResponseModel? responseModel = await HomeRepository.getListData<CarDataModel>(page);
    state.netState = HandleState.handle(responseModel, successCode: 0);
    if (state.netState == NetState.dataSuccessState) {
      CarDataModel carDataModel = responseModel.data;
      if (page == 1) {
        state.dataList = carDataModel.feeds;
        if (AppUtil.isEmpty(state.dataList)) {
          state.netState = NetState.emptyDataState;
        }
      } else {
        state.dataList?.addAll(carDataModel.feeds ?? []);
      }
      state.page = page;
      refreshController.refreshCompleted();
      refreshController.loadComplete();

      /// 显示没有更多数据了
      if (AppUtil.isEmpty(carDataModel.feeds)) {
        refreshController.loadNoData();
      }
    }
    notifyListeners();
  }

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

另外为了方便给列表做上拉刷新和下拉加载,还增加了ScrollControllerRefreshController。在列表的viewModel中可以直接使用。

代码如下所示:

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

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

 /// 状态
 final S state;

 /// 释放资源
 @override
 void dispose() {
   /// 释放资源(如取消订阅 Stream、关闭控制器等)
   super.dispose();
 }

 BaseViewModel(this.state);
}
2、State

状态层,主要用来定义一些属性,来进行代码上的隔离。 state这层必须要单独分出来,因为某个页面一旦维护的状态很多,将状态变量和逻辑方法混在一起,后期维护会非常头痛。

State都继承BaseState里面有一个属性NetState这个属性根据网络状态来赋值,页面根据这个NetState来展示不同的页面,如果说展示暂无数据页面 加载失败 骨架屏等等,都是根据NetState来决定的。

/// BaseState
/// 项目中所有需要根据网络状态显示页面的state必须继承于BaseState
enum NetState {
  /// 加载状态
  loadingState,

  /// 错误状态,显示失败界面
  error404State,

  /// 错误状态,显示刷新按钮
  errorShowRefresh,

  /// 空数据状态
  emptyDataState,

  /// 加载超时
  timeOutState,

  /// 数据获取成功状态
  dataSuccessState,

  /// 取消请求
  cancelRequest,

  /// 未知情况
  unknown;
}

abstract class BaseState {
  /// 页面状态
  NetState netState = NetState.loadingState;

  /// 分页页码
  int page = 1;
}
3、ConsumerWidget

Consumer在项目中用的还是很普遍,所以直接封装了一个ConsumerWidget 。在项目中需要使用ConsumerWidget的地方直接使用这个类即可,需要说明的是这个类不是异步操作来刷新的,比如说需要请求网络,然后再展示页面的话就不能使用这个类(下面会介绍使用这个类ConsumerStatusWidget)。

代码如下:

import 'package:flutter/widgets.dart';
import 'package:mvvm_provider/base/base_state.dart';
import 'package:mvvm_provider/base/base_view_model.dart';
import 'package:provider/provider.dart';

/// 不需要根据数据来显示的页面

class ConsumerWidget<T extends BaseViewModel<S>, S extends BaseState> extends StatelessWidget {
  const ConsumerWidget({super.key, required this.builder, this.child});

  final Widget Function(BuildContext context, S state, Widget? child) builder;
  final Widget? child;
  @override
  Widget build(BuildContext context) {
    return Consumer<T>(
      builder: (context, viewModel, child) {
        return builder(context, viewModel.state, child);
      },
      child: child,
    );
  }
}

使用场景:列表滑动改变导航栏透明度一类的需求.

例子:

ConsumerWidget<BookShelfOffsetlViewModel, BookShelfDetailState>(
  builder: (context, state, child) {
    return Opacity(
      opacity: state.appBarAlpha,
      child: Container(
        height: statusBarH + kToolbarHeight,
        alignment: Alignment.center,
        padding: EdgeInsets.only(left: 50.w, right: 50.w, top: 40.h),
        decoration: BoxDecoration(
          color: Colors.white,
          border: Border(
            bottom: BorderSide(width: 1.h, color: Colors.black12),
          ),
        ),
        child: Row(
          children: [
            Container(
              width: state.offset >= 150.h ? 1.sw - 150.w : 1.sw - 110.w,
              margin: EdgeInsets.only(right: 10.w),
              child: Text(
                getViewModel<BookShelfDetailViewModel>().state.mainModel?.title ?? '',
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
                style: TextStyle(fontSize: 15.sp),
              ),
            ),
            Visibility(
              visible: state.offset >= 150.h ? true : false,
              child: Container(
                alignment: Alignment.center,
                width: 40.w,
                height: 20.h,
                decoration: BoxDecoration(
                    color: Colors.orange,
                    border: Border.all(width: 1.w, color: Colors.orange),
                    borderRadius: BorderRadius.only(
                        topLeft: Radius.circular(6.h), bottomRight: Radius.circular(6.h))),
                child: Text(
                  '追书',
                  style: TextStyle(
                      color: Colors.white, fontSize: 12.sp, fontWeight: FontWeight.w500),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  },
);
4、ConsumerStatusWidget (根据网络数据展示页面)

在项目中需要根据网络展示页面的,使用ConsumerStatusWidget的地方直接使用这个类即可,代码如下所示:

enum PlaceHolderType {
  /// ListView站位
  listViewPlaceHolder,

  /// GridView站位
  gridViewPlaceHolder,

  /// StaggeredGrid 站位
  staggeredGridPlaceHolder,

  /// 详情 站位
  detailPlaceHolder,

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

class ConsumerStatusWidget<T extends BaseViewModel<S>, S extends BaseState>
    extends StatelessWidget {
  final Widget? emptyWidget;
  final Widget? errorWidget;
  final String? emptyText;
  final String? errorText;
  final String? timeOutText;
  final Function? refreshMethod;
  final PlaceHolderType placeHolderType;
  final Widget Function(BuildContext context, S state, Widget? child) builder;
  final Widget? child;

  const ConsumerStatusWidget(
      {super.key,
      required this.builder,
      this.child,
      this.emptyWidget,
      this.errorWidget,
      this.emptyText,
      this.errorText,
      this.timeOutText,
      this.refreshMethod,
      required this.placeHolderType});

  @override
  Widget build(BuildContext context) {
    return Consumer<T>(
      builder: (context, viewModel, child) {
        return MultiStateWidget(
          netState: viewModel.state.netState,
          placeHolderType: placeHolderType,
          emptyWidget: emptyWidget,
          errorWidget: errorWidget,
          timeOutText: timeOutText,
          emptyText: emptyText,
          errorText: errorText,
          refreshMethod: refreshMethod,
          builder: (BuildContext context) {
            return builder(context, viewModel.state, child);
          },
        );
      },
      child: child,
    );
  }
}

另外里面的MultiStateWidget这个类已经根据网络请求结果返回了具体的页面,也业务层无须处理这些逻辑了:

/// 空视图 builder方法 回调函数
typedef Builder = Widget Function(BuildContext context);

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 ?? '网络404了');
        break;
      case NetState.emptyDataState:
        resultWidget = EmptyWidget(title: emptyText ?? '暂无数据');
        break;
      case NetState.errorShowRefresh:
        resultWidget = NetErrorWidget(title: errorText ?? '网络错误', 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;
  }
}

使用时需要传递的参数:

泛型限制 <HomeViewModel, HomeState>

emptyWidget: 可选值,暂无数据页面,需要自定义时传递

errorWidget: 可选值,网络报错页面,需要自定义时传递

emptyText: 可选值,'暂无数据'文案

errorText: 可选值,'网络异常'文案

timeOutText: 可选值,'网络加载超时'文案

refreshMethod: 可选值,'重新加载'事件

PlaceHolderType placeHolderType: 必填值,针对于几种常见样式,写了几种骨架屏

Widget Function(BuildContext context, S state, Widget? child) builder: 必填值,回调构建页面所需要的元素,展示网络请求成功之后的页面.

Widget? child: 可选值

使用例子:

return ConsumerStatusWidget<MineViewModel, MineState>(
  placeHolderType: PlaceHolderType.listViewPlaceHolder,
  builder: (context, state, child) {
    return BaseListView(
      refreshController: getViewModel<MineViewModel>().refreshController,
      scrollController: getViewModel<MineViewModel>().scrollController,
      bgColor: const Color(0xFFF3F4F8),
      enablePullDown: true,
      enablePullUp: true,
      data: state.dataList,
      onRefresh: _onRefresh,
      onLoading: _onLoading,
      itemBuilder: (InfoModel model, int index) => InfoWidget(
        model: model,
        onTap: () {
          NavigatorUtils.push(context, MineRouter.mineDetailPage, arguments: {
            'entityId': model.id,
          });
        },
      ),
    );
  },
);
5、SelectorWidget 局部刷新小组件(Consumer+Selector = BuildWhen)

class SelectorWidget<T extends BaseViewModel<S>, S extends BaseState, A>
    extends StatelessWidget {
  final Widget Function(BuildContext context, dynamic value, Widget? child) builder;
  final Widget? child;

  /// 判断是否需要刷新的字段 特别需要指明的是selector的结果,必须是不可变的对象。 如果同一个对象,只是改变对象属性,那shouldRebuild的两个值永远是相等的。
  final SelectorPlusData Function(BuildContext context, S state)? plusDataSelector;
  final A Function(BuildContext context, S state)? selector;

  const SelectorWidget(
      {super.key, required this.builder, this.child, this.plusDataSelector, this.selector});

  @override
  Widget build(BuildContext context) {
    if (plusDataSelector != null) {
      return Selector<T, SelectorPlusData>(
        builder: builder,
        selector: (context, value) => plusDataSelector!(context, value.state),
        shouldRebuild: (pre, next) => next.shouldRebuild(),
        child: child,
      );
    }
    return Selector<T, A>(
      builder: builder,
      selector: (context, value) => selector!(context, value.state),
      shouldRebuild: (pre, next) => pre != next,
      child: child,
    );
  }
}

使用时需要传递的参数:

泛型限制 <HomeDetailViewModel, HomeDetailState, int>

Function(BuildContext context, dynamic value, Widget? child) builder: 必填值,回调构建页面所需要的元素,展示网络请求成功之后的页面,dynamic value根据什么来刷新返回什么.

plusDataSelector : 选填,根据哪个对象来刷新

selector : 选填,根据哪个基本数据类型来刷新

使用例子:

SelectorWidget<MineDetailLikeViewModel, MineDetailType2State, bool>(
   builder: (context, isLike, child) {
     return Positioned(bottom: bottomSafeBarH, child: articleTooWidget());
   },
   selector: (context, state) => state.isLike),
5、BaseView设计
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();

  ///子类实现
  BasePageState getState();
}

abstract class BasePageState<T extends BasePage> extends State<T> {
  /// 是否渲染buildPage内容
  bool _isRenderPage = false;

  /// 是否渲染导航栏
  bool isRenderHeader = true;

  /// 导航栏颜色
  Color? navColor;

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

  /// 导航栏高度
  double navBarH = AppBar().preferredSize.height;

  /// 顶部状态栏高度
  double statusBarH = 0.0;

  /// 底部安全区域高度
  double bottomSafeBarH = 0.0;

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

  /// header显示页面title
  String pageTitle = '';

  /// 是否允许某个页iOS滑动返回,Android物理返回键返回
  bool isAllowBack = true;

  bool resizeToAvoidBottomInset = true;

  /// 是否允许点击返回上一页
  bool isBack = true;

  @override
  void initState() {
    super.initState();
    _getBarInfo();
    _addFirstFrameListener();
    print('当前类:$runtimeType');
  }

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

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

  void buildComplete() {}

  /// 获取屏幕状态栏和顶部导航栏的高度
  void _getBarInfo() {
    WidgetsBinding.instance.addPostFrameCallback((mag) {
      statusBarH = ScreenUtil().statusBarHeight;
      bottomSafeBarH = ScreenUtil().bottomBarHeight;
      // if (SystemUtil.isIOS() && ScreenUtil().bottomBarHeight > 0) {
      //   bottomSafeBarH = 14.h;
      // }
      setState(() {
        _isRenderPage = true;
      });
    });
  }

  /// 点击左边按钮
  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),
    );
  }

  ///子类实现,构建各自页面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,
        ),
      ),
    );
  }
}

7、项目截图如下

![项目截图.png]1751877734449.jpg

四. 一个简单的列表写法案例

思路就是 定义一个viewModel 继承自BaseViewModel,在viewModel中请求接口获取数据,根据接口返回数据给state 赋值,然后一定记得给state里面的netStata完成赋值操作(这个很重要,因为页面是通过netStata的状态来展示的,如果不赋值,默认一直展示loading或者骨架屏)。

创建一个view继承自BasePage,然后在需要使用的数据的地方使用ConsumerStatusWidget或者SelectorWidget,然后builder里面返回需要展示的UI,完成页面的加载。

代码如下:

viewModel
import 'package:mvvm_provider/base/base_state.dart';
import 'package:mvvm_provider/page/home/data/model/cartoon_model.dart';
import 'package:mvvm_provider/page/home/data/repository/home_repository.dart';
import 'package:mvvm_provider/page/home/states/home_state.dart';
import 'package:mvvm_provider/until/app_util.dart';
import '../../../base/base_view_model.dart';
import '../../../config/handle_state.dart';
import '../../../model/response_model.dart';

class HomeViewModel extends BaseViewModel<HomeState> {
  HomeViewModel() : super(HomeState()); // 初始化 state

  /// 获取列表数据
  Future<void> getListData(bool isRefresh) async {
    int page = state.page;
    if (isRefresh) {
      page = 1;
    } else {
      page++;
    }
    ResponseModel? responseModel = await HomeRepository.getListData<CarDataModel>(page);
    state.netState = HandleState.handle(responseModel, successCode: 0);
    if (state.netState == NetState.dataSuccessState) {
      CarDataModel carDataModel = responseModel.data;
      if (page == 1) {
        state.dataList = carDataModel.feeds;
        if (AppUtil.isEmpty(state.dataList)) {
          state.netState = NetState.emptyDataState;
        }
      } else {
        state.dataList?.addAll(carDataModel.feeds ?? []);
      }
      state.page = page;
      refreshController.refreshCompleted();
      refreshController.loadComplete();

      /// 显示没有更多数据了
      if (AppUtil.isEmpty(carDataModel.feeds)) {
        refreshController.loadNoData();
      }
    }
    notifyListeners();
  }

  void changeIsLike() {
    state.isLike = !(state.isLike);
    notifyListeners();
  }
}
HomeRepository
import 'package:mvvm_provider/model/response_model.dart';
import 'package:mvvm_provider/net/http_config.dart';
import 'package:mvvm_provider/net/ltt_https.dart';
import 'package:mvvm_provider/page/home/data/model/cartoon_model.dart';
import 'package:mvvm_provider/page/home/data/repository/home_api.dart';

class HomeRepository {
  /// 请求主数据
  static Future<ResponseModel> getMainData<T>() async {
    ResponseModel? responseModel =
        await LttHttp().request<T>(HomeApi.homeDetailMainURL, method: HttpConfig.mock);
    return responseModel;
  }

  /// 请求系列数据
  static Future<ResponseModel> getSeriesData<T>() async {
    ResponseModel? responseModel =
        await LttHttp().request<T>(HomeApi.homeDetailSeriesURL, method: HttpConfig.mock);
    return responseModel;
  }

  /// 请求推荐数据
  static Future<ResponseModel> getRecommendData<T>() async {
    ResponseModel? responseModel =
        await LttHttp().request<T>(HomeApi.homeDetailRecommendURL, method: HttpConfig.mock);
    return responseModel;
  }

  /// 请求列表数据
  static Future<ResponseModel> getListData<T>(int number) async {
    String getUrl(int number) {
      String urlStr = '';
      if (number == 1) {
        urlStr = HomeApi.homeRecommendList1URL;
      } else if (number == 2) {
        urlStr = HomeApi.homeRecommendList2URL;
      } else if (number == 3) {
        urlStr = HomeApi.homeRecommendList3URL;
      }
      return urlStr;
    }

    ResponseModel responseModel =
        await LttHttp().request<T>(getUrl(number), method: HttpConfig.mock);
    if (number > 3) {
      return ResponseModel(data: CarDataModel(), code: 0, message: '暂无数据');
    }

    return responseModel;
  }

  /// 请求轮播数据
  static Future<ResponseModel> getBannerData<T>() async {
    ResponseModel? responseModel =
        await LttHttp().request<T>(HomeApi.homeDetailBannersURL, method: HttpConfig.mock);
    return responseModel;
  }
}
Model

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

View
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:mvvm_provider/base/consumer_status_widget.dart';
import 'package:mvvm_provider/const/theme_provider.dart';
import 'package:mvvm_provider/page/home/states/home_state.dart';
import 'package:provider/provider.dart';
import '../../../base/base_grid_view.dart';
import '../../../base/base_stateful_page.dart';
import '../../../base/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';

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

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

class _HomePageState extends BasePageState<HomePage> {
  @override
  void initState() {
    super.initState();
    super.pageTitle = '首页';
    isBack = false;
  }

  @override
  void buildComplete() {
    super.buildComplete();
    _onRefresh();
  }

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

  /// 上拉加载
  void _onLoading() {
    getListData();
  }

  /// 下拉刷新
  void _onRefresh() {
    getListData(isRefresh: true);
  }

  void getListData({bool isRefresh = false}) {
    getViewModel<HomeViewModel>().getListData(isRefresh);
  }

  /// 暗黑模式
  Widget _blackWidget() {
    final themeProvider = context.watch<ThemeProvider>();
    return Container(
      height: 60.h,
      width: 1.sw,
      color: Colors.blue,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            !themeProvider.isDarkMode ? '打开暗黑模式' : '关闭暗黑模式',
            style: TextStyle(fontSize: 12.sp, color: Colors.white, fontWeight: FontWeight.w500),
          ),
          Switch(
              activeColor: Colors.greenAccent,
              value: themeProvider.isDarkMode,
              onChanged: (bool change) {
                final newMode = themeProvider.isDarkMode ? ThemeMode.light : ThemeMode.dark;
                themeProvider.setThemeMode(newMode);
              }),
          InkWell(
              onTap: () {
                NavigatorUtils.push(
                  context,
                  HomeRouter.streamProviderPage,
                );
              },
              child: Text(
                'StreamProvider案例',
                style: TextStyle(fontSize: 12.sp, color: Colors.white, fontWeight: FontWeight.w500),
              ))
        ],
      ),
    );
  }

  @override
  Widget buildPage(BuildContext context) {
    return Column(
      children: [
        _blackWidget(),
        Expanded(
          child: ConsumerStatusWidget<HomeViewModel, HomeState>(
            placeHolderType: PlaceHolderType.gridViewPlaceHolder,
            builder: (context, state, child) {
              return BaseGridView(
                enablePullDown: true,
                enablePullUp: true,
                onRefresh: _onRefresh,
                onLoading: _onLoading,
                refreshController: getViewModel<HomeViewModel>().refreshController,
                scrollController: getViewModel<HomeViewModel>().scrollController,
                data: state.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: state.dataList![index],
                    onTap: () async {
                      NavigatorUtils.push(context, HomeRouter.homeDetailPage,
                          arguments: {"imageUrl": state.dataList?[index].image});
                    },
                  );
                },
              );
            },
          ),
        )
      ],
    );
  }
}
注册使用
router.define(homeDetailPage, handler: Handler(handlerFunc: (context, params) {
  Map? argument = context!.settings!.arguments as Map?;
  String imageUrl = argument?['imageUrl'];
  return ChangeNotifierProvider<HomeDetailViewModel>(
    create: (_) => HomeDetailViewModel(),
    builder: (context, widget) {
      return HomeDetailPage(
        imageUrl: imageUrl,
      );
    },
  );
}));
注意使用

ChangeNotifierProvider 两种创建方式

create:

  • 懒加载:只有在首次被访问时才会调用 create 方法创建对象。

  • 自动销毁:当 ChangeNotifierProvider 被移除时,自动调用 dispose() 释放资源。

  • 适合场景:对象生命周期与 Widget 树绑定,且无需外部传入已有实例。

value:

  • 立即初始化:需要直接传入一个已存在的对象

  • 手动管理生命周期:不会自动调用 dispose(),需开发者自行处理。

  • 适合场景:需要复用外部已有的对象,或对象生命周期由其他逻辑控制。

慎用value防止内存泄漏

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

需求分析: 这个页面分为三个接口返回数据,分别是小说主信息接口系列作品接口,和更多推荐接口

一个页面使用三个接口,正常来说使用并发方式请求完成所有的接口再拼装数据比较好,这样用时较短对于用户用户体验较好。但是也有的情况第二个接口请求的入参,需要第一个接口的返回值,这种就必须串行了。因此,针对这个页面串行和并发两种方式都写了一下。

页面在滑动时,导航栏的透明度是随着ListView的滑动距离来改变的,在滑动的过程中只有导航栏这个widget在变化,其他的widget并不会发生变化,所以没有必要在根节点处刷新整个widget,仅仅需要刷新导航栏widget就可以了。完成这个局部刷新有三种思路吧,都是可以的。

  1. 导航栏widget 抽离出去,在这个小的widget内部,使用 setStates 方法来完成刷新。

  2. 使用 两个ConsumerWidget 和 两个ViewModel来实现。 ViewModel A 请求接口,完成数据组装,发送通知notifyListeners()ConsumerWidget A 放在页面根节点,根据数据完成整个页面的加载展示。 ViewModel B 更新ListView滑动改变距离,发送通知notifyListeners()ConsumerWidget B 放在导航栏widget子节点,根据ListView滑动距离的改变来刷新 widget

  3. 使用 两个SelectorWidget 和 一个ViewModel来实现。 ViewModel 请求接口,完成数据组装,更新ListView滑动改变距离,发送通知notifyListeners()SelectorWidget A 放在页面根节点,根据数据完成整个页面的加载展示。根据小说的主id来决定主页面刷新还是不刷新。 SelectorWidget B 放在导航栏widget子节点,根据ListView滑动距离的改变来刷新 widget。(最能体现 Selector 颗粒刷新 优势)

代码实现

自行下载Demo观看吧。

9.页面多接口并发+局部刷新写法案例

还是这个页面,只不过是第一种方式的优化版了,接口是并发请求的,局部刷新用的是 两个SelectorWidget 和 一个ViewModel来实现的。

并发请求代码

/// 请求全部数据
  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();
    });
  }

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

  /// 请求系列数据
  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;
  }

  /// 请求推荐数据
  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;
  }

注意点:需要根据三个接口的状态来完成页面netState赋值操作。

  1. ProxyProvider使用案例就不贴代码了,下载查看吧

结束:

就写到这里吧,针对于MVVM+Provider的项目架构设计已经可以满足项目使用了,一直认为,技术就是用来沟通的,没有沟通就没有长进,在此,欢迎各种大佬吐槽沟通。Coding不易,如果感觉对您有些许的帮助,欢迎点赞评论。

声明:

仅开源供大家学习使用,禁止从事商业活动,如出现一切法律问题自行承担!!!

仅学习使用,如有侵权,造成影响,请联系本人删除,谢谢

Demo下载地址 Demo

安装二维码