前言:
做flutter开发有些时间了,之前用过GetX和Bloc,在之前的文章中也总结过这两个框架的用法和一些常见问题,最近挤出点时间搞了一个Provider,之前在项目中也使用过Provider,但是怎么说呢,那会也是初学者用的稀里糊涂的,用的不优雅,不透彻,今天来盘一盘,MVVM+ Provider的项目写法.
Flutter 基于getX搭建通用项目架构
Flutter 基于 Bloc搭建通用项目架构
老规矩,先上效果。
|
|
|
|
---|
一. Provider 基本用法
Provider
有两个重要的角色。提供者:提供数据, 消费者:消费数据。他的使用也是围绕着这两个角色来展开的。
首先定义提供者,Provider
为我们提供了非常多的提供者,总共有八种。但我们比较常用的是ChangeNotifierProvider 和 MultiProvider 和 ChangeNotifierProxyProvider
关于其他的提供者可根据自己的实际应用场景来。可以用一个Model
和ViewModel
继承于或者混入ChangeNotifier
,然后让需要使用数据的widget
继承于ChangeNotifierProvider
,这样当数据变化时就可以通过ChangeNotifierProvider
来提供数据,完成页面的刷新工作。关于几个提供者的用法就不一一说了,感兴趣的可以自己百度。
二. Provider也提供了三个消费者
Provider第三方是基于InheritedWidget封装的:InheritedWidget
Provider也提供了三个消费者:Provider.of
、Consumer(会刷新不必要刷新的组件)
、Selector(更精细化)
1、Provider.of
InheritedWidget有个默认的约定:如果状态是希望暴露出的,应当提供一个 “of” 静态方法来获取其对象,开发者便可直接通过该方法来获取。
static T of(BuildContext context, {bool listen = true})
其中 listen
:默认true监听状态变化,false为不监听状态改变。
Provider.of
是 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(context),
child,
);
}
发现它就是通过 Provider.of
对比一下,我们发现 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? _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(
builder: widget.builder,
selector: widget.plusDataSelector!,
shouldRebuild: (pre, next) => next.shouldRebuild(),
child: widget.child,
)
使用代码如下:
ProviderSelectorWidget(
viewModel: novelViewModel,
builder: (context, selectorPlusData, child) {
return Container();
},
plusDataSelector: (context, viewModel) => SelectorPlusData(value: novelViewModel.dataList))
用SelectorPlusData
将需要监听的数组包一下,就能完成数组变化的监听了,对于其他对象也是一样。
三. 针对Provider+MVVM模式设计
1、ViewModel
针对于ViewModel的封装其实很简单,就是继承于ChangeNotifier
监听数据变化,它用于向监听器发送通知。换言之,如果被定义为 ChangeNotifier
,你可以订阅它的状态变化。
另外为了方便给列表做上拉刷新和下拉加载,还增加了ScrollController
和RefreshController
。在列表的viewModel
中可以直接使用。
代码如下所示:
class BaseViewModel extends ChangeNotifier {
/// 列表控制器
final ScrollController scrollController = ScrollController();
/// 刷新组建控制器
final RefreshController refreshController = RefreshController(initialRefresh: false);
}
2、State
状态层,主要用来定义一些属性,来进行业务上的解耦和代码上的隔离。 state
这层必须要单独分出来,因为某个页面一旦维护的状态很多,将状态变量和逻辑方法混在一起,后期维护会非常头痛。
State
里面定义一个属性NetState
这个属性根据网络状态来赋值,页面根据这个NetState
来展示不同的页面,如果说展示暂无数据页面
加载失败
骨架屏
等等,都是根据NetState
来决定的。
3、ProviderConsumerWidget
Consumer
在项目中用的还是很普遍,所以直接封装了一个ProviderConsumerWidget
。在项目中需要使用Consumer
的地方直接使用这个类即可。
代码如下:
class ProviderConsumerWidget extends StatefulWidget {
final Widget Function(BuildContext context, T value, Widget? child) builder;
final T viewModel;
final Widget? child;
final Function(T)? onReady;
/// ChangeNotifierProvider 在某种场景下,程序热启动时会失效,但是ChangeNotifierProvider.value会一直保持状态
final bool? isValue;
const ProviderConsumerWidget(
{super.key,
required this.viewModel,
this.child,
this.onReady,
required this.builder,
this.isValue});
@override
State createState() => _ProviderConsumerWidgetState();
}
class _ProviderConsumerWidgetState
extends State> {
@override
void initState() {
super.initState();
if (widget.onReady != null) {
widget.onReady!(widget.viewModel);
}
}
@override
Widget build(BuildContext context) {
if (widget.isValue == true) {
return ChangeNotifierProvider.value(
value: widget.viewModel,
child: Consumer(
builder: widget.builder,
child: widget.child,
),
);
} else {
return ChangeNotifierProvider(
create: (_) => widget.viewModel,
child: Consumer(
builder: widget.builder,
child: widget.child,
),
);
}
}
}
使用时需要传递的参数:
T viewModel:
就是viewModel对象,需要监听的数据变化的对象。
Widget? child:
不需要刷新的widget
,可传可不传。
Function(T)? onReady:
数据请求或者数据变化的方法,可传可不传,不传的话在view
中的initState
方法中进行数据请求一样的。
builder
函数,然后会回调BuildContext context, T value, Widget? child
这三个参数给view
,其中T value
返回的就是传入的viewModel
对象,Widget? child
就是传入的不需要刷新的Consumer
的widget
bool? isValue:
控制的是ChangeNotifierProvider
两种构造方法。
当isValue
为true
时对应的是ChangeNotifierProvider
的构造方法。
ChangeNotifierProvider.value(
value: widget.viewModel,
child: Consumer(
builder: widget.builder,
child: widget.child,
),
)
当isValue
为false
时对应的是ChangeNotifierProvider
的构造方法。
return ChangeNotifierProvider(
create: (_) => widget.viewModel,
child: Consumer(
builder: widget.builder,
child: widget.child,
),
);
问题:ChangeNotifierProvider.value 和 ChangeNotifierProvider create 差异之处和使用注意事项。
说实话一开始我并没有把重点放在这两个方法上面,上来直接使用的ChangeNotifierProvider
,但是当我用Selector
来做首页Tabar
切换这个功能时,遇到了问题。就是每当我热重载或者热启动时,点击Tabar
就是失效了,然后重启项目发现是好使的,只要是一热更新就失效。对此很是纳闷。百度了一下发现这方面的资料很少,就索性看了下ChangeNotifierProvider
的源码实现。
ChangeNotifierProvider
->继承了 ListenableProvider
->继承了 InheritedProvider
->继承了SingleChildStatelessWidget
->继承了SingleChildStatelessWidget
结论:
ChangeNotifierProvider
(builder模式)的父类构造器多了一个dispose
,当ChangeNotifierProvider
从widget
树中被移除时会自动调用dispose
方法移除相应的数据,使得内存占用永远保持着一个合理的水平。
ChangeNotifierProvider.value
在被移除widget
树的时候不会自动调用dispose
,需要手动去管理数据,比如在被移除的时候依然有其它地方想使用这个数据,并在合适的时候再去手动关闭。
那么新问题又来了,flutter热启动时ChangeNotifierProvider
会被移除widget
树吗?
答案肯定是不会的,我这个问题出现的原因是因为我使用的StatelessWidget
,当热启动或者热重载时,
StatelessWidget
会失效,最后把StatelessWidget
换成StatefulWidget
就解决了这个问题.
但是对ChangeNotifierProvider
的探索还是有用的,因为在项目开发中,经常会遇到局部刷新的场景,也就是一个view
中可能会用到多个ProviderConsumerWidget
或者ProviderSelectorWidget
,但是他们又持有同一个viewModel
对象,由于ChangeNotifierProvider create
是自己释放对象的。所以这种场景就会造成当页面释放时,第一个ProviderConsumerWidget
释放的时候把viewModel
释放了,第二个ProviderConsumerWidget
释放的时候发现自己持有的viewModel
已经释放了,就报错了。这种情况下就需要使用ChangeNotifierProvider.value
初始化方法,然后在页面的dispose
方法中自己手动释放viewModel
。
4、ProviderSelectorWidget
class ProviderSelectorWidget extends StatefulWidget {
final Widget Function(BuildContext context, dynamic value, Widget? child) builder;
final T viewModel;
final Widget? child;
final Function(T)? onReady;
/// ChangeNotifierProvider 在某种场景下,程序热启动时会失效,但是ChangeNotifierProvider.value会一直保持状态
final bool? isValue;
/// 判断是否需要刷新的字段 特别需要指明的是selector的结果,必须是不可变的对象。 如果同一个对象,只是改变对象属性,那shouldRebuild的两个值永远是相等的。
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 createState() => _ProviderSelectorWidgetState();
}
class _ProviderSelectorWidgetState
extends State> {
@override
void initState() {
super.initState();
if (widget.onReady != null) {
widget.onReady!(widget.viewModel);
}
}
@override
Widget build(BuildContext context) {
if (widget.isValue == true) {
return ChangeNotifierProvider.value(
value: widget.viewModel,
child: widget.plusDataSelector != null
? Selector(
builder: widget.builder,
selector: widget.plusDataSelector!,
shouldRebuild: (pre, next) => next.shouldRebuild(),
child: widget.child,
)
: Selector(
builder: widget.builder,
selector: widget.selector!,
shouldRebuild: (pre, next) => pre != next,
child: widget.child,
),
);
} else {
return ChangeNotifierProvider(
create: (_) => widget.viewModel,
child: widget.plusDataSelector != null
? Selector(
builder: widget.builder,
selector: widget.plusDataSelector!,
shouldRebuild: (pre, next) => next.shouldRebuild(),
child: widget.child,
)
: Selector(
builder: widget.builder,
selector: widget.selector!,
shouldRebuild: (pre, next) => pre != next,
child: widget.child,
),
);
}
}
}
使用时需要传递的参数:
T viewModel:
就是viewModel对象,需要监听的数据变化的对象。
Widget? child:
不需要刷新的widget
,可传可不传。
Function(T)? onReady:
数据请求或者数据变化的方法,可传可不传,不传的话在view
中的initState
方法中进行数据请求一样的。
builder
函数,然后会回调BuildContext context, dynamic value, Widget? child
这三个参数给view
,其中dynamic value
返回的就是传入的A
对象,
例子
ProviderSelectorWidget(
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
就是传入的不需要刷新的Consumer
的widget
bool? isValue:
控制的是ChangeNotifierProvider
两种构造方法。
当isValue
为true
时对应的是ChangeNotifierProvider
的构造方法。
final A Function(BuildContext, T)? selector
当监听的数据是基本数据类型时使用selector
。
final SelectorPlusData Function(BuildContext, T)? plusDataSelector
当监听的数据是对象时使用plusDataSelector
例子
ProviderSelectorWidget(
viewModel: novelViewModel,
onReady: (viewModel) {
getListData();
},
builder: (context, selectorPlusData, child) {
return Container();
},
plusDataSelector: (context, viewModel) => SelectorPlusData(value: novelViewModel.dataList))
此时value
返回的就是传入的SelectorPlusData
对象,获取返回值时使用selectorPlusData.value
来获取。
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 extends State {
/// 是否渲染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(
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设计,根据state里面的netState状态,决定页面的展示。
代码如下
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方法 回调函数
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 ?? '网络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;
}
}
7、项目截图如下
四. 一个简单的列表写法案例
思路就是 定义一个viewModel
继承自BaseViewModel
,在viewModel
中请求接口获取数据,根据接口返回数据给state
赋值,然后一定记得给state
里面的netStata
完成赋值操作(这个很重要,因为页面是通过netStata的状态来展示的,如果不赋值,默认一直展示loading或者骨架屏)。
创建一个view
继承自BasePage
,然后在需要使用的数据的地方使用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 {
/// 创建state
HomeState homeState = HomeState();
/// 获取列表数据
Future getListData(String url, int number) async {
if (url == '') {
/// 没有更多数据了
refreshController.refreshCompleted();
refreshController.loadComplete();
refreshController.loadNoData();
homeState.netState = NetState.dataSuccessState;
notifyListeners();
return;
}
ResponseModel? responseModel =
await LttHttp().request(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 ?? []);
/// 显示没有更多数据了
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 getState() => _HomePageState();
}
class _HomePageState extends BasePageState {
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('点击拍照'),
// onPressed: () async {
// //拍照
// XFile? file = await _picker.pickImage(source: ImageSource.camera,imageQuality: 100);
// NavigatorUtils.push(context, HomeRouter.waterMarkPage,
// arguments: {"imagePath": file!.path});
// },
// ),
// );
return ProviderConsumerWidget(
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.页面多接口串行+局部刷新写法案例
需求分析:
这个页面分为三个接口返回数据,分别是小说主信息接口
,系列作品接口
,和更多推荐接口
。
一个页面使用三个接口,正常来说使用并发方式请求完成所有的接口再拼装数据比较好,这样用时较短对于用户用户体验较好。但是也有的情况第二个接口请求的入参,需要第一个接口的返回值,这种就必须串行了。因此,针对这个页面串行和并发两种方式都写了一下。
页面在滑动时,导航栏的透明度是随着ListView
的滑动距离来改变的,在滑动的过程中只有导航栏这个widget
在变化,其他的widget
并不会发生变化,所以没有必要在根节点处刷新整个widget
,仅仅需要刷新导航栏widget
就可以了。完成这个局部刷新
有三种思路吧,都是可以的。
将
导航栏widget
抽离出去,在这个小的widget内部,使用setStates
方法来完成刷新。使用 两个
ProviderConsumerWidget
和 两个ViewModel
来实现。
ViewModel A
请求接口,完成数据组装,发送通知notifyListeners()
。
ProviderConsumerWidget A
放在页面根节点,根据数据完成整个页面的加载展示。
ViewModel B
更新ListView
滑动改变距离,发送通知notifyListeners()
。
ProviderConsumerWidget B
放在导航栏widget
子节点,根据ListView
滑动距离的改变来刷新widget
。使用 两个
ProviderSelectorWidget
和 一个ViewModel
来实现。
ViewModel
请求接口,完成数据组装,更新ListView
滑动改变距离,发送通知notifyListeners()
。
ProviderSelectorWidget A
放在页面根节点,根据数据完成整个页面的加载展示。根据小说的主id
来决定主页面刷新还是不刷新。
ProviderSelectorWidget B
放在导航栏widget
子节点,根据ListView
滑动距离的改变来刷新widget
。(最能体现 Selector颗粒刷新
优势)
代码实现
自行下载Demo观看吧。
9.页面多接口并发+局部刷新写法案例
还是这个页面,只不过是第一种方式的优化版了,接口是并发请求的,局部刷新用的是 两个ProviderSelectorWidget
和 一个ViewModel
来实现的。
并发请求代码
/// 请求全部数据
getAllData() async {
await Future.wait([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(
'https://run.mocky.io/v3/315de364-a765-40e1-8383-f36d3ffe5bdd',
method: HttpConfig.get);
return responseModel.data;
}
/// 请求系列数据
getSeriesData() async {
ResponseModel? responseModel = await LttHttp().request(
'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(
'https://run.mocky.io/v3/7b0096eb-a1ea-4f3c-8273-e6e700a01128',
method: HttpConfig.get);
CartoonRecommendData cartoonRecommendData = responseModel.data;
return cartoonRecommendData.infos;
}
注意点:需要根据三个接口的状态来完成页面netState
赋值操作。
结束:
就写到这里吧,针对于MVVM+Provider
的项目架构设计已经可以满足项目使用了,一直认为,技术就是用来沟通的,没有沟通就没有长进,在此,欢迎各种大佬吐槽沟通。Coding不易,如果感觉对您有些许的帮助,欢迎点赞评论。
声明:
仅开源供大家学习使用,禁止从事商业活动,如出现一切法律问题自行承担!!!
仅学习使用,如有侵权,造成影响,请联系本人删除,谢谢