Flutter fast_mvvm 使用帮助
fast_mvvm 创作思路
基于原生Android开发基础,在初次学习Flutter的时候就开始查找关于MVVM框架的开源,但苦于Flutter正在成长中,没有一个很完整的整体框架,所以借鉴了网上部分思路最终开源了 fast_mvvm。
快速开发,自由配置,各种状态页,刷新颗粒度控制,兼容下拉刷新上拉加载,电匠工坊作者线上项目使用中, 安卓和苹果都已上架,放心使用。
有问题可以加我 QQ 275916180,欢迎来讨论。
项目在 GitHub上开源了,望大家点赞谢谢。
Pub https://pub.flutter-io.cn/packages/fast_router
GitHub https://github.com/StrangerKjq/fast_router
fast_router 修改自 fluro 的新版本 加入一些新方法
日常使用,加入一些便捷方法,可以抛弃Navigator了
支持ios 左滑 跟 android原生路由跳转
Pub https://pub.flutter-io.cn/packages/fast_develop
GitHub https://github.com/StrangerKjq/fast_develop
fast_develop 快速开发,支持配置,接入屏幕适配、数据安全转换、oss上传、快捷样式设置、状态栏、存储、多级联动、图片显示\选择\裁剪、dio封装、下拉刷新等常用组件。
Demo 讲解
这里模拟了文章列表。
Model:UserModel
BaseListViewModel: ArticleVM
View:ArticlePage
数据实体类 BaseEntity:ArticleEntity
主要讲解了初始化配置, 状态页效果,根布局刷新,数据获取。
UserModel
首先创建项目模块所需要用的Model。
这里创建UserModel。
class UserModel extends BaseModel {
/// 登录
Future<bool> login(String account, String psd) async {
await Future.delayed(Duration(seconds: 3));
return true;
}
/// 资讯列表
Future<DataResponse<ArticleEntity>> getArticleList() async {
await Future.delayed(Duration(seconds: 2));
var entity = ArticleEntity([
ArticleItem("1", "好的", "内容内容内容内容内容", DateTime.now().toString()),
ArticleItem("1", "好的", "内容内容内容内容内容", DateTime.now().toString()),
]);
DataResponse dataResponse =
DataResponse<ArticleEntity>(entity: entity, totalPageNum: 3);
return dataResponse;
}
}
初始化
在APP首页启动的时候初始化框架。
调用initMVVM(),装载UserModel,配置上拉加载下拉刷新;
选择文章页面是否根布局刷新选项,是否配置单独的状态页。
class App extends StatefulWidget {
@override
_AppState createState() => _AppState();
}
class _AppState extends State<App> {
@override
void initState() {
initMVVM<BaseViewModel>(
[UserModel()],
controllerBuild: () => EasyRefreshController(),
resetRefreshState: (c) =>
(c as EasyRefreshController)?.resetRefreshState(),
finishRefresh: (c, {bool success, bool noMore}) =>
(c as EasyRefreshController)
?.finishRefresh(success: success, noMore: noMore),
resetLoadState: (c) => (c as EasyRefreshController)?.resetLoadState(),
finishLoad: (c, {bool success, bool noMore}) =>
(c as EasyRefreshController)
?.finishLoad(success: success, noMore: noMore),
);
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: SelectPage(),
);
}
}
class SelectVM extends BaseViewModel {
ValueNotifier<bool> isLoadData = ValueNotifier(true);
ValueNotifier<bool> isConfigState = ValueNotifier(false);
}
class SelectPage extends StatelessWidget with BaseView<SelectVM> {
@override
ViewConfig<SelectVM> initConfig(BuildContext context) =>
ViewConfig(vm: SelectVM());
@override
Widget vmBuild(
BuildContext context, SelectVM vm, Widget child, Widget state) {
return Scaffold(
appBar: AppBar(title: Text("选择")),
body: ListView(
children: <Widget>[
ListTile(
title: Text("是否加载数据,用来测试状态页和重新加载数据"),
trailing: ValueListenableBuilder(
valueListenable: vm.isLoadData,
builder: (_, value, __) => Switch(
value: value,
onChanged: (value) => vm.isLoadData.value = value,
),
),
),
ListTile(
title: Text("是否单独配置状态页,用来测试状态页和重新加载数据"),
trailing: ValueListenableBuilder(
valueListenable: vm.isConfigState,
builder: (_, value, __) => Switch(
value: value,
onChanged: (value) => vm.isConfigState.value = value,
),
),
),
ListTile(
title: Text("根布局刷新"),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ArticlePage(
true,
configState: vm.isConfigState.value,
loadData: vm.isLoadData.value,
),
),
);
},
),
ListTile(
title: Text("根布局不刷新"),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ArticlePage(
false,
configState: vm.isConfigState.value,
loadData: vm.isLoadData.value,
),
),
);
},
),
],
),
);
}
}
ArticleEntity
模拟接口返回的数据实体类:
class ArticleEntity extends BaseEntity {
List<ArticleItem> list;
ArticleEntity(this.list);
}
class ArticleItem {
String id;
String title;
String content;
String time;
ArticleItem(this.id, this.title, this.content, this.time);
}
ArticleVM
UserModel 继承 BaseModel,ArticleEntity继承BaseEntity,ArticleItem 暂没要求。
是否首次加载数据测试空状态页的效果
创建vnTime 用来监听List第一个ArticleItem的时间刷新
class ArticleVM
extends BaseListViewModel<UserModel, ArticleEntity, ArticleItem> {
ArticleVM(this.isLoadData);
bool isLoadData = true;
/// 首次加载
bool firstLoad = true;
ValueNotifier<String> vnTime = ValueNotifier("暂无");
@override
void jointList(ArticleEntity newEntity) => entity.list.addAll(newEntity.list);
@override
List<ArticleItem> get list => entity.list;
@override
Future<DataResponse<ArticleEntity>> requestHttp(
{bool isLoad, int page, params}) {
/// 判断是否加载数据, 测试状态页用
if (!isLoadData && firstLoad) {
firstLoad = false;
return null;
}
return model.getArticleList();
}
@override
void initResultData() {
vnTime.value = list[0].time;
}
/// 修改第一个数据的时间
void modifyFistTime() {
list[0].time = DateTime.now().toString();
vnTime.value = list[0].time;
notifyListeners();
}
}
ArticlePage View
文章具体页面,显示一个列表,下方显示第一个item对应的时间,和根布局刷新的时间。
class ArticlePage extends StatelessWidget with BaseView<ArticleVM> {
const ArticlePage(
this.rootRefresh, {
Key key,
this.configState = false,
this.loadData = true,
}) : super(key: key);
/// 是否全局刷新
final bool rootRefresh;
final bool configState;
final bool loadData;
@override
ViewConfig<ArticleVM> initConfig(BuildContext context) {
var _empty = configState ? (vm) => Center(child: Text("单独配置:empty")) : null;
return rootRefresh
? ViewConfig<ArticleVM>(vm: ArticleVM(loadData), empty: _empty)
: ViewConfig<ArticleVM>.noRoot(vm: ArticleVM(loadData), empty: _empty);
}
@override
Widget vmBuild(
BuildContext context, ArticleVM vm, Widget child, Widget state) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(title: Text("文章")),
bottomNavigationBar: state != null
? SizedBox()
: Container(
color: Colors.amber,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
MaterialButton(
onPressed: vm.modifyFistTime,
color: Colors.white,
child: Text("修改第一个Item时间,测试全局刷新"),
),
ValueListenableBuilder<String>(
valueListenable: vm.vnTime,
builder: (_, value, __) {
return Text("第一个Item时间:$value");
},
),
Text("根布局刷新时间:${DateTime.now().toString()}"),
],
),
),
body: state ??
EasyRefresh(
controller: vm.refreshController,
onLoad: vm.loadMore,
onRefresh: vm.pullRefresh,
child: ListView.builder(
itemCount: vm.list.length,
itemBuilder: (ctx, index) {
return Selector<ArticleVM, ArticleItem>(
selector: (_, aVM) => aVM.list[index],
shouldRebuild: (pre, next) => pre == next,
builder: (_, ArticleItem value, __) => _item(value),
);
},
),
),
);
}
Widget _item(ArticleItem item) {
return Container(
color: Colors.lightGreen,
margin: EdgeInsets.all(8),
padding: EdgeInsets.all(4),
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(item.title),
Text(item.time),
],
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(item.content),
),
],
),
);
}
}
数据刷新
通过ValueListenableBuilder 监听在ArticleVM创建的ValueNotifier 对象,实现局部刷新。
通过Selector 监听ArticleEntity中List 的 ArticleItem 的变化
ValueListenableBuilder<String>(
valueListenable: vm.vnTime,
builder: (_, value, __) {
return Text("第一个Item时间:$value");
},
)
Selector<ArticleVM, ArticleItem>(
selector: (_, aVM) => aVM.list[index],
shouldRebuild: (pre, next) => pre == next,
builder: (_, ArticleItem value, __) => _item(value),
)
源码介绍
做开发大家或多或少都听说过MVC,而 MVP和MVVM都是基于MVC进化而来的产物,为了更好的拆分业务与界面,提高整体项目结构,便于后期维护与单元测试。关于具体的区别水平有限这些就不细讲了。
Entity 实体类
Entity 实体类主要负责将JSON数据进行格式转换。
业务实现时继承 BaseEntity 暂时没有处理,后期开发迭代会用到。
class ArticleEntity extends BaseEntity {
List<ArticleItem> list;
ArticleEntity(this.list);
}
class ArticleItem {
String id;
String title;
String content;
String time;
ArticleItem(this.id, this.title, this.content, this.time);
}
Model 数据服务层
Model 数据服务层,跟其他类MVC模式一样,不管最终对接的是数据库还是网络API或是其它,都是在负责数据的存储,并提供访问数据的接口,以支持数据的增删改查的基本操作。
源码介绍:
/// 基类的API 声明API
mixin BaseRepo {}
/// 基类Model 具体实现API
class BaseModel with BaseRepo {}
假设现在有一个用户模块UserModel继承BaseModel负责用户的所有接口。
具体业务实现:
class UserModel extends BaseModel {
/// 登录
Future<bool> login(String account, String psd) async {
await Future.delayed(Duration(seconds: 3));
return true;
}
/// 资讯列表
Future<DataResponse<ArticleEntity>> getArticleList() async {
await Future.delayed(Duration(seconds: 2));
var entity = ArticleEntity(
[ArticleItem("1", "好的", "内容内容内容内容内容", DateTime.now().toString())]);
DataResponse dataResponse =
DataResponse<ArticleEntity>(entity: entity, totalPageNum: 3);
return dataResponse;
}
}
接口数据处理封装
response接口未格式化数据。
class DataResponse<T> {
T entity;
bool result;
Response response;
int totalPageNum;
get data => response.data;
DataResponse({
@required this.entity,
this.result = false,
this.response,
this.totalPageNum = 1,
});
}
ViewModel 数据与页面的绑定调用层
ViewModel 主要负责调用Model层获取数据,将数据和方法提供给View层来调用,因暂时没有找到获取View层控件的方法该功能暂没有实现。
数据刷新基于官方的provider来实现。
BaseViewModel
源代码可以看到 这里继承了ChangeNotifier,并且需要范型BaseModel和BaseEntity。
/// 基类 VM
abstract class BaseViewModel<M extends BaseModel, E extends BaseEntity>
extends ChangeNotifier
页面状态管理枚举类ViewModelState 提供 idle 闲置 busy 加载中 empty 空数据 error 错误 unAuthorized 账号未授权
/// ViewModel的状态 控制页面基础显示
enum ViewModelState { idle, busy, empty, error, unAuthorized }
初始化ViewModel
defaultOfParams 在请求数据用的参数下面会讲到。
viewState 指定状态,默认为闲置。
自动填充BaseModel
/// 根据状态构造
/// 子类可以在构造函数指定需要的页面状态
/// FooModel():super(viewState:ViewState.busy);
BaseViewModel({ViewModelState viewState, this.defaultOfParams})
: _viewState = viewState ?? ViewModelState.idle {
init(false);
Future.delayed(Duration(seconds: 1), () => init(true));
}
/// model
M model;
M getModel() => null;
@mustCallSuper
void init(bool await) {
if (!await) {
model = getModel() ?? getModelGlobal<M>();
// if (isSaveVM()) _addVM(this);
} else {
_eventButAddInit(portMap);
_disposeInit();
}
}
初始化 EventBus 事件通知 批量端口注册 fast_event_bus fast_event_bus
子类实现 portMap
案例:
@override
Map<String, EventListen> get portMap => {
Constant.invoice_select: invoiceSelect,
Constant.address_select_send: addressSelect,
};
源码:
/// 端口 key 跟 回调监听
Map<String, EventListen> get portMap => Map<String, EventListen>();
/// 绑定端口跟回调
void _eventButBindListen(String key, EventListen listen) {
EventBus.getDefault().register(key, listen);
}
/// 绑定初始化 大量绑定
void _eventButAddInit(Map<String, EventListen> portMap) {
portMap?.forEach((key, callback) {
_eventButBindListen(key, callback);
});
}
/// 端口删除
void eventButDelete(String key) {
EventBus.getDefault().unregister(key);
}
/// 端口添加
@mustCallSuper
bool eventButAdd(String key, EventListen listen) {
portMap.update(key, (l) => listen, ifAbsent: () => listen);
return EventBus.getDefault().register(key, listen);
}
初始化 dispose注册在页面销毁的时候销毁内存占用
子类实现方法 waitDispose()
案例:
TextEditingController name = TextEditingController();
TextEditingController phone = TextEditingController();
TextEditingController area = TextEditingController();
TextEditingController tecAddress = TextEditingController();
@override
List waitDispose() => [name, phone, area, tecAddress];
源码:
List _disposeWait = [];
void _disposeInit() {
for (var item in waitDispose()) _disposeAdd(item);
}
void _disposeAdd(item) {
if (item.dispose != null) _disposeWait.add(item);
}
/// 清理内存占用
void _disposeList() {
for (var item in _disposeWait)
if (item != null) {
try {
if (item is StreamSubscription) {
item.cancel();
} else {
item.dispose();
}
} catch (e, s) {
handleCatch(e, s);
} finally {
item = null;
}
}
}
@override
void dispose() {
_disposed = true;
for (var key in portMap.keys) {
eventButDelete(key);
}
_disposeList();
super.dispose();
}
接下来看关于状态的设置:
/// 当前的页面状态,默认为busy,可在viewModel的构造方法中指定;
ViewModelState _viewState;
ViewModelState get viewState => _viewState;
/// 出错时的message
String _errorMessage;
String get errorMessage => _errorMessage;
/// 以下变量是为了代码书写方便,加入的变量.严格意义上讲,并不严谨
bool get busy => viewState == ViewModelState.busy;
bool get idle => viewState == ViewModelState.idle;
bool get empty => viewState == ViewModelState.empty;
bool get error => viewState == ViewModelState.error;
bool get unAuthorized => viewState == ViewModelState.unAuthorized;
void setBusy(bool value) {
_errorMessage = null;
viewState = value ? ViewModelState.busy : ViewModelState.idle;
}
void setEmpty() {
_errorMessage = null;
viewState = ViewModelState.empty;
}
void setError(String message) {
_errorMessage = message;
viewState = ViewModelState.error;
}
void setUnAuthorized() {
_errorMessage = null;
viewState = ViewModelState.unAuthorized;
}
/// 最终修改状态并通知页面刷新
set viewState(ViewModelState viewState) {
_viewState = viewState;
notifyListeners();
}
下面讲解页面数据请求
是否是http请求,还是本地数据装载。
在获取到数据后判断空值。
然后执行initResultData()对数据进行下一步自定义处理。
bool isHttp() => true;
/// 进入页面isInit loading
Future<void> viewRefresh({
bool showLoad = false,
dynamic params,
bool notifier = true,
bool busy = true,
}) async {
if (busy && !showLoad) setBusy(true);
bool result = false;
result = await _request(param: params);
_notifyIntercept = !notifier;
// LogUtil.printLog("notifier : $notifier _notifyIntercept:$_notifyIntercept");
if (!result) {
setEmpty();
} else {
///改变页面状态为非加载中
setBusy(false);
}
}
/// 请求数据
Future<bool> _request({param}) async {
try {
var data = await _httpOrData(false, BaseListViewModel.pageFirst, param);
if (data == null || data.entity == null) {
return false;
} else {
entity = data.entity;
initResultData();
return true;
}
} catch (e, s) {
handleCatch(e, s);
return false;
}
}
/// 判断http或者data
Future<DataResponse<E>> _httpOrData(bool isLoad, int page, param) async {
return isHttp()
? await requestHttp(
isLoad: isLoad, page: page, params: param ?? defaultOfParams)
: await requestData(isLoad, page);
}
本地数据装载
异步方法 并返回成功和失败
/// 非http请求
Future<DataResponse<E>> requestData(bool isLoad, int page) async => null;
http接口请求
源码:
/// http请求
Future<DataResponse<E>> requestHttp(
{@required bool isLoad, int page, params}) async =>
null;
在请求时调用model提供的API来获取数据。案例:
@override
Future<DataResponse<ArticleEntity>> request({bool isLoad, int page, params}) {
return model.getArticleList();
}
页面销毁
销毁初始化注册的事件端口和监听Steam等对象。防止内存泄露
@override
void dispose() {
_disposed = true;
for (var key in portMap.keys) {
eventButDelete(key);
}
_disposeList();
super.dispose();
}
BaseListViewModel
在BaseViewModel上进一步封装为长列表页面提供下拉刷新和上拉加载,额外需要一个List中Item类 可以是dynamic
/// 基类 ListVM
abstract class BaseListViewModel<M extends BaseModel, E extends BaseEntity, I>
extends BaseViewModel<M, E>
初始化参数
页面配置显示当前页码
BaseListViewModel({params}) : super(defaultOfParams: params);
/// 分页第一页页码
static int pageNumFirst = 1;
/// 当前页码
int _currentPageNum = pageNumFirst;
static int _totalPageNum = 1;
/// 跟EasyRefresh 相关配置
EasyRefreshController _refreshController = EasyRefreshController();
EasyRefreshController get refreshController => _refreshController;
提供一个List对外方法:
@protected
List<I> get list;
案例:
EasyRefresh(
controller: vm.refreshController,
onLoad: vm.loadMore,
onRefresh: vm.viewRefresh,
child: ListView.builder(
itemCount: vm.list.length,
itemBuilder: (ctx, index) => _item(vm.list[index])),
)
下拉刷新:
/// 下拉刷新
Future<bool> httpRequest({param}) async {
try {
_currentPageNum = pageNumFirst;
DataResponse<E> data = await request(
isLoad: false, page: pageNumFirst, params: param ?? defaultOfParams);
refreshController.finishRefresh();
refreshController.resetLoadState();
if (_checkData(false, data)) {
return false;
} else {
initResultData();
_totalPageNum = data.totalPageNum ?? 1;
refreshController.finishLoad(success: true);
return true;
}
} catch (e, s) {
refreshController.finishRefresh(success: false);
refreshController.resetLoadState();
handleCatch(e, s);
return false;
}
}
上拉加载:
/// 上拉加载更多
Future<void> loadMore() async {
// print('------> current: $_currentPageNum total: $_totalPageNum');
if (_currentPageNum >= _totalPageNum) {
refreshController.finishLoad(success: true, noMore: true);
} else {
var cPage = ++_currentPageNum;
//debugPrint('ViewStateRefreshListViewModel.loadMore page: $currentPage');
try {
var data =
await request(isLoad: true, page: cPage, params: defaultOfParams);
if (_checkData(true, data)) {
_currentPageNum--;
refreshController.finishLoad(success: true, noMore: true);
} else {
if (_currentPageNum >= _totalPageNum) {
refreshController.finishLoad(success: true, noMore: true);
} else {
refreshController.finishLoad(success: true, noMore: false);
refreshController.resetLoadState();
}
notifyListeners();
}
} catch (e, s) {
_currentPageNum--;
refreshController.finishLoad(success: false);
refreshController.resetLoadState();
debugPrint('error--->\n' + e.toString());
debugPrint('stack--->\n' + s.toString());
}
}
}
对数据判断提供的新的方法,并提供空数组子类的自己实现。
/// 验证数据是否为空
bool _checkData(bool isLoad, DataResponse<E> data) {
if (data == null || data.entity == null) return true;
if (isLoad) {
jointList(data.entity);
} else {
entity = data.entity;
}
return judgeNull(data);
}
/// 判断数组是否为空
@protected
bool judgeNull(DataResponse<E> data) => list == null || list.isEmpty;
数组拼接:
void jointList(E newModel);
案例:
当获取数据后对数组重装,因数据List的不确定性,这里由子类实现。
@override
void jointList(ArticleEntity newEntity) => entity.list.addAll(newEntity.list);
View 页面层
View 主要负责页面的呈现展示。
Flutter 主要有StatelessWidget和StatefulWidget两种控件用来展示页面,BaseVIew的设计思路采用的扩展,对StatelessWidget和StatefulWidget进行扩展使用,不破坏控件完整性。
ViewConfig
ViewConfig 页面配置类主要用于配置页面属性。
根布局刷新方式,空数据验证,是否初始加载,页面状态页自定义等。
源码:
/// view层 配置用类
class ViewConfig<VM extends BaseViewModel> {
ViewConfig({
@required this.vm,
this.child,
this.color,
this.load = true,
this.checkEmpty = true,
this.state,
this.value = false,
this.busy,
this.empty,
this.error,
this.unAuthorized,
}) : this.root = true,
this._firstLoad = true;
ViewConfig.value({
@required this.vm,
this.child,
this.color,
this.load = false,
this.checkEmpty = true,
this.state,
this.value = true,
this.busy,
this.empty,
this.error,
this.unAuthorized,
}) : this.root = true,
this._firstLoad = true;
ViewConfig.noRoot({
@required this.vm,
this.child,
this.color,
this.load = true,
this.checkEmpty = true,
this.state,
this.value = false,
this.busy,
this.empty,
this.error,
this.unAuthorized,
}) : this.root = false,
this._firstLoad = true;
/// VM
VM vm;
Widget child;
VSBuilder<VM> busy;
VSBuilder<VM> empty;
VSBuilder<VM> error;
VSBuilder<VM> unAuthorized;
/// 背景颜色
Color color;
/// 加载
bool load;
/// 是否根布局刷新 采用 [Selector]
bool root;
/// 首次加载
bool _firstLoad;
/// [ChangeNotifierProvider.value] 或者[ChangeNotifierProvider]
bool value;
/// 是否验证空数据
bool checkEmpty;
/// 页面变化控制
int state;
}
提供了三种构造方法。
默认构造必须传递ViewModel并且不能为空,首次创建,不能被监听。
ChangeNotifierProvider<T>(
create: (_) => changeNotifier.vm, child: child)
value构造 已经被创建,有监听存在,ViewModel可以为空。
可以用Providr.of(context)获取到ViewModel对象。
changeNotifier.vm = Provider.of<T>(context);
return ChangeNotifierProvider<T>.value(
value: changeNotifier.vm, child: child)
noRoot 构造 对根布局刷新进行优化,控制当前页面的根布局刷新不会因为ViewModel调用了notifyListeners()方法而全局刷新。
状态页讲解
案例:
全局配置状态, 这里的vm是BaseViewModel
initMVVM(
[UserModel(), ShopModel()],
empty: (vm) => VSEmptyWidget(onTap: (_) => vm.viewRefresh()),
error: (vm) => VSWidget(onTap: (_) => vm.viewRefresh()),
unAuthorized: (vm) => VSUnAuthWidget(onTap: (_) => vm.viewRefresh())
);
页面单独配置状态,vm是对应页面的BaseViewModel的子类,比如文章对应的ArticleVM
@override
ViewConfig<ArticleVM> initConfig(BuildContext context) {
return rootRefresh
? ViewConfig<ArticleVM>(vm: ArticleVM())
: ViewConfig<ArticleVM>.noRoot(vm: ArticleVM(), busy: (vm) => Container());
}
页面根布局处理
通过ViewConfig的配置,来进行根布局刷新管理,
默认情况根布局会响应每次的刷新 if (config.root) return true;
否则就只响应初始数据加载时的变化,后期页面刷新都会忽略进而优化页面渲染。
源码:
/// root 根节点加工 根节点是否需要刷新,不刷新就执行一次刷新 更新第一次状态变化
Widget _root<VM extends BaseViewModel>(
BuildContext context, ViewConfig config, VMBuilder builder) {
/// 是否根节点需要刷新
return _availableCNP<VM>(
context,
config,
child: Selector<VM, dynamic>(
child: config.child,
selector: (ctx, vm) => vm.entity,
shouldRebuild: (_, __) {
if (config.root) return true;
if (!config._firstLoad) return false;
config._firstLoad = false;
return true;
},
builder: (ctx, value, child) =>
_viewState(config, (state) => builder(ctx, config.vm, child, state)),
),
);
}
页面状态管理
这一步东西较多,主要负责页面的状态和后期跨页面刷新的逻辑。
首先判断页面状态ViewConfig配置,配置为空则采用默认状态页。
然后处理页面背景。
最后判断是否需要跨页面刷新 是否配置ViewConfig.state 提前埋点。
源码:
/// 页面状态展示 空 正常 错误 忙碌
Widget _viewState<VM extends BaseViewModel>(
ViewConfig data, Widget Function(Widget state) builder) {
VM viewModel = data.vm;
var bgColor = data.color;
var checkEmpty = data.checkEmpty;
var state = data.state;
var empty = data.empty == null ? null : data.empty(viewModel);
var busy = data.busy == null ? null : data.busy(viewModel);
var error = data.error == null ? null : data.error(viewModel);
var un = data.unAuthorized == null ? null : data.unAuthorized(viewModel);
Widget stateView;
if (viewModel == null || checkEmpty && viewModel.empty) {
stateView = empty ??
Container(
color: bgColor,
child: ViewStateEmptyWidget(onTap: () => viewModel.viewRefresh()),
);
} else if (viewModel.busy) {
stateView = busy ?? ViewStateBusyWidget(backgroundColor: bgColor);
} else if (viewModel.error) {
stateView = error ?? ViewStateWidget(onTap: () => viewModel.viewRefresh());
} else if (viewModel.unAuthorized) {
stateView =
un ?? ViewStateUnAuthWidget(onTap: () => viewModel.viewRefresh());
}
Widget view = builder(stateView);
if (bgColor != null) {
view = Container(child: view, color: bgColor);
}
if (state == null) {
return view;
} else {
/// view状态变化提醒
var changer = ValueListenableBuilder(
valueListenable: changerStateGet(state).vn,
builder: (_, changer, __) {
// LogUtil.printLog("state : ${state.toString()} value: $changer");
try {
var vsChanger = changerStateCheck(state);
if (vsChanger.changer) {
// LogUtil.printLog("state : ${state.toString()} value: $changer"
// "notifier: ${vsChanger.notifier}");
viewModel.viewRefresh(notifier: vsChanger.notifier, busy: false);
}
} catch (e) {
print(e);
}
return SizedBox();
},
);
return Stack(children: <Widget>[changer, Positioned.fill(child: view)]);
}
}
跨页面刷新
获取状态配置,验证是否已经刷新,更新页面
这里就是上面讲到了状态的埋点,然后其他页面处理时需要刷新则调用changerStateUpdate
举例:商城购物车,在商品详情点击添加购物车后,需要刷新购物车列表。
源码:
class _ViewStateNotifier {
ValueNotifier<bool> vn;
bool notifier;
_ViewStateNotifier(this.vn, {this.notifier = true});
}
/// 状态通知 跨页面通知数据需要变动
class ViewStateNotifier {
bool changer;
bool notifier;
ViewStateNotifier(this.changer, this.notifier);
}
/// 全局状态变动存储
Map<int, _ViewStateNotifier> _changerState = {};
/// 获取状态配置
_ViewStateNotifier changerStateGet(int state) {
if (!_changerState.containsKey(state)) {
_changerState[state] = _ViewStateNotifier(ValueNotifier(false));
}
return _changerState[state];
}
/// 更新页面状态
void changerStateUpdate(int state, {bool notifier = true}) {
if (_changerState.containsKey(state)) {
_changerState[state].vn.value = true;
_changerState[state].notifier = notifier;
}
}
/// 验证是否需要变化
ViewStateNotifier changerStateCheck(int state) {
var result = _changerState[state].vn.value;
_changerState[state].vn.value = false;
return ViewStateNotifier(result, _changerState[state].notifier);
}
BaseView
BaseView 是基于StatelessWidget实现,需要继承BaseViewModel范型。
子类必须实现initConfig和vmBuild,并且删除build的实现。
initConfig 提供页面的初始化配置。
vmBuild 页面最终呈现调用的方法。
源码介绍:
/// 基类 view 扩展[StatelessWidget]
mixin BaseView<VM extends BaseViewModel> on StatelessWidget {
/// 初始化配置
@protected
ViewConfig<VM> initConfig(BuildContext context);
/// VM 相关
@protected
Widget vmBuild(BuildContext context, VM vm, Widget child, Widget state);
/// 初始化操作 加载等
_init(BuildContext context, ViewConfig<VM> config) async {
config.vm.context ??= context;
if (config.load) await config.vm.viewRefresh();
}
/// 不要使用 推荐使用 [vmBuild]
@override
@deprecated
Widget build(BuildContext ctx) {
// LogUtil.printLog("build:----" + this.runtimeType.toString());
var config = initConfig(ctx);
if (config == null) throw "initConfig 方法 返回空值";
/// 是否需要加载
if (!config.load) return _root<VM>(ctx, config, vmBuild);
return FutureBuilder(
future: _init(ctx, config),
builder: (ctx, __) => _root<VM>(ctx, config, vmBuild));
}
}
vmBuild 详解
子类必须实现的方法,VM就是当前页面所需要的BaseViewModel,child 就是当前页面不会有状态变化的控件,state就是当前页面状态切换提供给子类展示和判断,只有当页面加载顺利完成才会为空。
源码:
/// VM 相关
@protected
Widget vmBuild(BuildContext context, VM vm, Widget child, Widget state);
案例:
文章页面可以看到,我返回了Scaffold,body里面我判断了state是否为空,当状态为空时,代表数据加载全部完成然后返回一个列表来展示数据。
@override
Widget vmBuild(
BuildContext context, ArticleVM vm, Widget child, Widget state) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(title: Text("文章")),
body: state ??
EasyRefresh(
controller: vm.refreshController,
onLoad: vm.loadMore,
onRefresh: vm.viewRefresh,
child: ListView.builder(
itemCount: vm.list.length,
itemBuilder: (ctx, index) => _item(vm.list[index])),
),
);
}
BaseViewOfState
针对某些页面需要保持当前滑动状态而必须用到StatefulWidget,对State进行封装。
基本使用跟BaseView类似。
/// 基类 state 扩展[StatefulWidget] 的 [State]
mixin BaseViewOfState<T extends StatefulWidget, VM extends BaseViewModel>
on State<T> {
拿退货退款举例,因为页面涉及到列表所以需要保持页面滑动状态所以必须使用StatefulWidget
案例:
/// 退货或者退款
class _ReturnedRefund extends StatefulWidget {
const _ReturnedRefund(
this.isRefund, {
Key key,
}) : super(key: key);
final bool isRefund;
@override
_ReturnedRefundState createState() => _ReturnedRefundState();
}
class _ReturnedRefundState extends State<_ReturnedRefund>
with
AutomaticKeepAliveClientMixin,
BaseViewOfState<_ReturnedRefund, ReturnedRefundListVM> {
@override
bool get wantKeepAlive => true;
@override
ViewConfig<ReturnedRefundListVM> initConfig(BuildContext context) =>
ViewConfig(vm: ReturnedRefundListVM(widget.isRefund));
@override
Widget vmBuild(BuildContext context, vm, Widget child, state) {
return PSDisplay(
primary: () => state,
secondary: () => EasyRefresh(
controller: vm.refreshController,
onRefresh: vm.viewRefresh,
onLoad: vm.loadMore,
child: ListIntervalView(
space: 32,
itemCount: vm.list.length,
itemBuilder: (_, index) => _OrderItem(vm, index),
),
),
);
}
}
写在最后
这是本人第一个认真整理并开源的框架,希望可以帮助到大家,让大家在日后做项目的时候能专心于业务的实现,啰啰嗦嗦写了一大堆,在思路上、代码水平等方面会有些不足,望大家提醒我会吸取教训并加以改进。
快速开发,自由配置,各种状态页,刷新颗粒度控制,兼容下拉刷新上拉加载,电匠工坊 作者线上项目使用中,安卓和苹果都已上架,放心使用。
有问题可以加我 QQ 275916180,欢迎来讨论。
项目在 GitHub上开源了,望大家点赞谢谢。
|
|