接著上一篇文章:基於MVVM架構封裝Flutter基礎庫
這篇文章主要使用基礎庫對MVVM做簡單實踐:Demo地址
一、MVVM回顧
Model:資料模型
1、XApi
2、通常來說,Model中儲存了相關業務的資料,負責提取和處理資料,資料來源可以是本地資料庫,也可以來自網路;
View:檢視
1、BaseView
2、View只做和UI相關的工作,不涉及任何業務邏輯,不涉及運算元據,不處理資料;
3、通俗講就是展示給使用者的介面及控制元件,比如Flutter中參與介面展示的Widget;
ViewModel:檢視模型
1、BaseViewModel
2、ViewModel將View和Model進行解耦,並且實現View與Model的互動;
3、簡單講就是所有的業務邏輯都由它負責,而不是將業務邏輯和View都糅合在一起;
Data Binding:繫結器
1、Provider
2、View通過資料繫結來關心ViewModel的資料變化;
3、通過Provider的Consumer/Selector等元件來實現資料繫結;
二、基礎庫MVVM元件介紹
1、BaseView功能說明
通過Consumer實現View和ViewModel繫結,ViewModel資料變化時通知View重新整理;
BaseView配合BaseViewModel使用,進入頁面時通過BaseViewModel.onLoading()方法觸發http請求;
BaseView通過FutureBuilder實現非同步UI更新,從而實現http通用載入錯誤頁,空白頁以及正常顯示頁UI和邏輯;
///@date: 2021/3/1 13:38
///@author: lixu
///@description:View 基類,配合[BaseViewModel]使用
///封裝http載入錯誤頁,空白頁以及正常顯示頁UI和邏輯,UI可自定義
class BaseView<T extends BaseViewModel> extends StatefulWidget {
///載入成功後顯示的頁面
final Widget child;
///載入中頁面
final Widget loadingChild;
///資料為空的頁面
final Widget emptyChild;
///請求失敗顯示的頁面
final Widget errorChild;
BaseView({@required this.child, this.loadingChild,
this.emptyChild, this.errorChild}) : assert(child != null);
@override
_BaseViewState<T> createState() => _BaseViewState<T>();
}
class _BaseViewState<T extends BaseViewModel> extends State<BaseView>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
///資料繫結
return Consumer<T>(
child: widget.child,
builder: (BuildContext context, T viewModel, Widget child) {
if (viewModel.isSuccess()) {
return child;
} else {
///非同步UI更新
return FutureBuilder(
///觸發網路請求:BaseViewModel.onLoading(context)
future: viewModel.onLoading(context),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
if (viewModel.isFail()) {
///載入失敗
return _getErrorWidget(viewModel);
} else if (viewModel.isEmpty()) {
///資料為空
return _getEmptyWidget(viewModel);
} else {
///載入成功
return child;
}
} else {
///載入中
return _getLoadingWidget();
}
},
);
}
},
);
}
...省略部分程式碼
@override
bool get wantKeepAlive => true;
}
複製程式碼
2、BaseViewModel功能說明
進入頁面時,需要http獲取資料後才能顯示UI的場景繼承BaseViewModel類,配合 BaseView使用,實現http載入錯誤頁,空白頁以及正常顯示頁UI和邏輯;
///@date: 2021/3/1 11:09
///@author: lixu
///@description: ViewModel基類
///進入View頁面時,需要http獲取資料後才能顯示UI的場景繼承[BaseViewModel]類,配合[BaseView]使用
///泛型T:進入頁面時,http獲取資料物件的型別
///1.列表載入(分頁載入)
///2.單個物件載入
abstract class BaseViewModel<T> extends BaseCommonViewModel {
...省略其它程式碼
///介面獲取的是否是列表資料,否則就是單個資料物件
///預設true
bool _isRequestListData;
///資料來源:針對列表請求
List<T> dataList;
///資料來源:針對單個物件請求
T dataBean;
///獲取http請求引數
Map<String, dynamic> getRequestParams();
///獲取http請求url
String getUrl();
///載入資料:進入頁面時觸發該方法
Future onLoading(BuildContext context) async {
LogUtils.i(getTag(), 'onLoading');
if (isLoading) {
LogUtils.w(getTag(), 'onLoading() is Loading');
return;
}
isLoading = true;
_refreshedText = '重新整理成功';
CancelToken cancelToken = CancelToken();
cancelTokenList.add(cancelToken);
if (_isRequestListData) {
///請求列表資料
await api.requestList<T>(
getUrl(),
params: getRequestParams(),
isShowLoading: false,
isShowFailToast: isShowFailToast(false),
cancelToken: cancelToken,
onSuccess: (List<T> list) {
///請求成功回撥
dataList = [];
list = list ?? [];
///解析json
dataList.addAll(list);
if (_isPageLoad) {
///list分頁載入
if (list.length < _pageSize) {
_canLoadMore = false;
} else {
_canLoadMore = true;
_pageNum++;
}
} else {
///list沒有分頁載入
_canLoadMore = false;
}
},
onError: (HttpErrorBean errorBean) {
///請求失敗回撥
_refreshedText = '重新整理失敗';
onErrorCallback(errorBean);
},
onComplete: () {
///請求完成回撥
isLoading = false;
cancelTokenList?.remove(cancelToken);
},
);
} else {
///請求單個資料物件
await api.request<T>(
getUrl(),
params: getRequestParams(),
isShowLoading: false,
isShowFailToast: isShowFailToast(false),
cancelToken: cancelToken,
onSuccess: (T bean) {
///請求成功回撥
dataBean = bean;
},
onError: (HttpErrorBean errorBean) {
///請求失敗回撥
_refreshedText = '重新整理失敗';
onErrorCallback(errorBean);
},
onComplete: () {
///請求完成回撥
isLoading = false;
cancelTokenList?.remove(cancelToken);
},
);
}
}
///請求失敗:重試重新整理頁面
void retryRefresh() {
if (!isDispose) {
notifyListeners();
} else {
LogUtils.e(getTag(), 'call retryRefresh() had dispose()');
}
}
///請求是否成功
bool isSuccess() {
if (_isRequestListData) {
return dataList != null && dataList.length > 0;
} else {
return dataBean != null;
}
}
///請求是否失敗
bool isFail() {
if (_isRequestListData) {
return dataList == null;
} else {
return dataBean == null;
}
}
///請求資料是否為空
bool isEmpty() {
if (_isRequestListData) {
return dataList == null || dataList.isEmpty;
} else {
return dataBean == null;
}
}
...省略其它程式碼
}
複製程式碼
3、BaseCommonViewModel功能說明
ViewModel 基類,進入View頁面時,直接顯示UI(不需要請求http獲取資料)的 場景__繼承BaseCommonViewModel類;
///@date: 2021/3/1 11:05
///@author: lixu
///@description: ViewModel 基類
///進入View頁面時,直接顯示UI(不需要請求http獲取資料)的場景繼承[BaseCommonViewModel]類
abstract class BaseCommonViewModel with ChangeNotifier {
///是否已經呼叫了dispose()方法
bool _isDispose = false;
///是否正在請求中
bool isLoading = false;
///網路請求物件,充當MVVM的Model層
XApi api = XApi();
///獲取tag,用於日誌tag
String getTag();
///儲存請求token,用於頁面關閉時取消請求
List<CancelToken> cancelTokenList = [];
///重新整理頁面
@override
notifyListeners() {
LogUtils.v(getTag(), 'notifyListeners() isDispose:$_isDispose');
if (!_isDispose) {
super.notifyListeners();
}
}
bool get isDispose => _isDispose;
///頁面關閉時回撥該方式,釋放資源
///使用ChangeNotifierProvider的預設構造方法來注入ViewModel,才能保證資源能正確釋放
@override
void dispose() {
super.dispose();
///頁面關閉取消請求
api?.cancelList(cancelTokenList);
_isDispose = true;
LogUtils.v(getTag(), 'dispose()');
}
}
複製程式碼
三、MVVM實踐
1、Model(UserDetailBean和XApi)
///@date: 2021/3/2 11:20
///@author: lixu
///@description: 使用者詳情物件
class UserDetailBean {
///頭像
String icon;
///使用者id
String userId;
///使用者名稱
String name;
UserDetailBean.fromJsonMap(Map<String, dynamic> map)
: userId = map["userId"]?.toString(),
name = map["name"],
icon = map["icon"];
}
複製程式碼
2、View(UserPage)
///@date: 2021/3/11
///@author: lixu
///@description:使用者列表頁面
///進入頁面時呼叫介面(使用者列表)獲取資料成功後,才能顯示UI
class UserPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('使用者列表頁面(MVVM)')),
///使用ChangeNotifierProvider的預設構造方法來注入ViewModel,
///保證資源能正確釋放
body: ChangeNotifierProvider(
create: (_) {
return UserListViewModel();
},
child: BaseView<UserListViewModel>(
///UserListView為請求成功後顯示的View(使用者列表)
child: UserListView(),
///自定義請求失敗的View
///如果不設定,會使用全域性失敗頁面(IResConfig中配置的資源)
errorChild: Center(
child: Text(
'這是自定義請求失敗的頁面:\n請求失敗,點選重試',
style: TextStyle(color: Colors.red,
fontSize: 20),
textAlign: TextAlign.center,
),
),
),
),
);
}
}
複製程式碼
3、ViewModel(UserListViewModel)
///@date: 2021/3/2 11:26
///@author: lixu
///@description: 使用者列表viewModel
///泛型[UserDetailBean]:進入頁面時,http獲取資料物件的型別
class UserListViewModel extends BaseViewModel<UserDetailBean> {
UserListViewModel() : super(isPageLoad: false);
///獲取http請求引數
@override
Map<String, dynamic> getRequestParams() {
return {
'userId': loginInfo.userBean?.userId,
'token': loginInfo.token,
};
}
@override
String getTag() {
return 'UserListViewModel';
}
///獲取http請求url
@override
String getUrl() {
return HttpUrls.userListUrl;
}
}
複製程式碼
四、總結
通過使用:BaseView+BaseViewModel+Provider+XApi 對MVVM進行了簡單的實現,更多功能可以下載demo體驗