前言:
做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)用場景來??梢杂靡粋€Model和ViewModel繼承于或者混入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放在Consumer的 builder方法中,不需要刷新的方法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)變化。
另外為了方便給列表做上拉刷新和下拉加載,還增加了ScrollController和RefreshController。在列表的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就是傳入的不需要刷新的Consumer的widget
bool? isValue: 控制的是ChangeNotifierProvider兩種構(gòu)造方法。
當(dāng)isValue為true時對應(yīng)的是ChangeNotifierProvider<T>.value的構(gòu)造方法。
ChangeNotifierProvider<T>.value(
value: widget.viewModel,
child: Consumer<T>(
builder: widget.builder,
child: widget.child,
),
)
當(dāng)isValue為false時對應(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)ChangeNotifierProvider從widget樹中被移除時會自動調(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就是傳入的不需要刷新的Consumer的widget
bool? isValue: 控制的是ChangeNotifierProvider兩種構(gòu)造方法。
當(dāng)isValue為true時對應(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)目截圖如下

四. 一個簡單的列表寫法案例
思路就是 定義一個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就可以了。完成這個局部刷新有三種思路吧,都是可以的。
將
導(dǎo)航欄widget抽離出去,在這個小的widget內(nèi)部,使用setStates方法來完成刷新。使用 兩個
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。使用 兩個
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)系本人刪除,謝謝



