前言:
做flutter开发有些时间了,之前用过GetX和Bloc,在之前的文章中也总结过这两个框架的用法和一些常见问题,最近挤出点时间搞了一个Provider,之前在项目中也使用过Provider,但是怎么说呢,那会也是初学者用的稀里糊涂的,用的不优雅,不透彻,今天来盘一盘,MVVM+ Provider的项目写法.
Flutter 基于getX搭建通用项目架构 Flutter 基于 Bloc搭建通用项目架构
老规矩,先上效果。
一. 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关于其他的提供者可根据自己的实际应用场景来了解。可以用一个Model或ViewModel继承于或者混入ChangeNotifier,然后让需要使用数据的widget注册Provider,这样当数据变化时就可以通过Provider 来提供数据,完成页面的操作。
二. Provider也提供了三个消费者
Provider第三方是基于InheritedWidget封装的:InheritedWidget
Provider也提供了三个消费者:Provider.of、Consumer(会刷新不必要刷新的组件)、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放在Consumer的 builder 方法中,不需要刷新的方法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);
这样继承于 BaseViewModel的ViewModel只要 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();
}
}
另外为了方便给列表做上拉刷新和下拉加载,还增加了ScrollController和RefreshController。在列表的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]
四. 一个简单的列表写法案例
思路就是 定义一个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就可以了。完成这个局部刷新有三种思路吧,都是可以的。
-
将
导航栏widget抽离出去,在这个小的widget内部,使用setStates方法来完成刷新。 -
使用 两个
ConsumerWidget和 两个ViewModel来实现。ViewModel A请求接口,完成数据组装,发送通知notifyListeners()。ConsumerWidget A放在页面根节点,根据数据完成整个页面的加载展示。ViewModel B更新ListView滑动改变距离,发送通知notifyListeners()。ConsumerWidget B放在导航栏widget子节点,根据ListView滑动距离的改变来刷新widget。 -
使用 两个
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赋值操作。
- ProxyProvider使用案例就不贴代码了,下载查看吧
结束:
就写到这里吧,针对于MVVM+Provider的项目架构设计已经可以满足项目使用了,一直认为,技术就是用来沟通的,没有沟通就没有长进,在此,欢迎各种大佬吐槽沟通。Coding不易,如果感觉对您有些许的帮助,欢迎点赞评论。
声明:
仅开源供大家学习使用,禁止从事商业活动,如出现一切法律问题自行承担!!!
仅学习使用,如有侵权,造成影响,请联系本人删除,谢谢