什么是MVVM
很多文章也介绍过了,那就说一说自己对它的理解吧。MVVM是如今主流的应用架构,它将我们的应用结构分为M(Model)
、V(View)
、VM(ViewModel)
,下面对这三部分进行简单的描述。
M-Model层
数据仓库层,你可以把你获取数据的方法(不管是从服务器还是从本地,调用者不关心数据是从哪里来的,数据来源规则在自己内部定义)、数据模型(各种entity、bean什么的)都放在这一层。
V-View层
表示层,一切用户能看到的东西都在这层;比如页面、弹窗、按钮、Toast等等,并接收一切用户输入,我们将所有和UI相关的代码都放在这一层。
VM-ViewModel层
业务逻辑层,我称它为中间层。它负责从Model
层获取数据,又为绑定它的View
提供量身定制的数据,它连接View
与Model
并为View
提供服务。ViewModel
既要响应View
的输入,又要在数据更新后通知View
,它其实对于View
来说就是一个被观察者,在整个View
的生命周期中一直被View
关注。
为什么推荐使用MVVM构建项目
若不使用一个好的架构去构建项目,当我们的项目逐渐庞大,维护起来是非常痛苦的。前不久就作死的重构过一个3000行代码的页面,State里面各种变量、各种网络请求、各种状态、各种setState,那滋味别提多酸爽。早些年的MVC
、MVP
架构在这里就不赘述了,MVVM
是由它们演变而来的,实现了View
与Model
全解耦。基于这个特点它的好处太多了;比如以后我们可以基于每个ViewModel
编写单元测试了、更细粒度的控制View
的刷新以提高性能等等。
实现一个简单的MVVM
这里实现一个简陋的登陆案例,创建如下结构的文件
user_entity.dart
用户属性数据模型
class UserEntity {
String userId;
String userName;
int age;
UserEntity.fromJson(Map json){
userId = json['userId'];
userName = json['userName'];
age = json['age'];
}
}
login_model.dart
请求登陆接口、获取数据相关
class LoginModel {
Future login(String account,String password) async {
///模拟网络请求并解析成功
await Future.delayed(Duration(seconds: 3));
return UserEntity.fromJson({'userId':'9527','userName':'爱静静真好','age':18});
/*http.Response resp = await http.post('https://www.baidu.com/login',body: {'account':account,'password':password});
if(resp.statusCode == 200){
return UserEntity.fromJson(jsonDecode(resp.body));
}
throw '登陆失败';*/
}
}
login_widget.dart
登陆页面
class LoginWidget extends StatefulWidget {
@override
State createState() => _LoginPageState();
}
class _LoginPageState extends State {
LoginViewModel loginViewModel;
TextEditingController accountCtrl, passwordCtrl;
BuildContext dialogCtx;
@override
void initState() {
super.initState();
///初始化LoginViewModel
loginViewModel = LoginViewModel();
accountCtrl = TextEditingController();
passwordCtrl = TextEditingController();
///监听登陆状态展示loading路由
loginViewModel.state.addListener(() async {
///登陆中展示loading
if (loginViewModel.state.value == LoginState.ing) {
await showDialog(
context: context,
builder: (ctx) {
dialogCtx = ctx;
return AlertDialog(content: Text('登陆中...'),);
});
} else {
///登陆结束dismiss弹窗
if (ModalRoute.of(dialogCtx).isCurrent) {
Navigator.maybePop(dialogCtx);
}
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
///这里有个Text需要显示登陆成功后的userName
ChangeNotifierWidget(loginViewModel, (ctx,vm){
return Text(vm.userName);
}),
TextField(
controller: accountCtrl,
),
TextField(
controller: passwordCtrl,
),
FlatButton(onPressed: () => loginViewModel.login(accountCtrl.text, passwordCtrl.text), child: Text('登陆')),
],
),
),
);
}
@override
void dispose() {
super.dispose();
///有些viewmodel可能需要释放
loginViewModel.dispose();
}
}
login_view_model.dart
登陆页面的ViewModel
///登陆过程的状态
enum LoginState{
init,
ing,
success,
fail,
}
class LoginViewModel extends ChangeNotifier {
String userName;
///登陆状态
ValueNotifier state;
LoginModel model;
LoginViewModel(){
userName = '';
model = LoginModel();
state = ValueNotifier(LoginState.init);
}
///登陆响应
void login(String account,String password) async {
state.value = LoginState.ing;
///这里可以做校验
if(account == null || password == null){
state.value = LoginState.fail;
return;
}
try {
UserEntity entity = await model.login(account, password);
userName = entity.userName;
state.value = LoginState.success;
} catch (e){
state.value = LoginState.fail;
} finally {
notifyListeners();
}
}
}
然后为了实现细粒度的刷新,编写了一个简单的基于ChangeNotifier
机制的Widget
///一个简单的ChangeNotifierWidget,可以监听ChangeNotifier的notifyListeners方法进行重建,
///这里的ChangeNotifier其实就是我们的ViewModel
typedef MyConsumer = Widget Function(BuildContext ctx,T vm);
class ChangeNotifierWidget extends StatefulWidget {
final T viewModel;
final Widget Function(BuildContext,T vm) builder;
ChangeNotifierWidget(this.viewModel, this.builder);
@override
State createState() => _ChangeNotifierState();
}
class _ChangeNotifierState extends State> {
@override
void initState() {
super.initState();
// ignore: invalid_use_of_protected_member
assert(!widget.viewModel.hasListeners, 'this ChangeNotifier has more than one listeners!');
widget.viewModel.addListener(() {
setState(() {});
});
}
@override
Widget build(BuildContext context) {
T vm = widget.viewModel;
return widget.builder(context,vm);
}
@override
void dispose() {
super.dispose();
///注意,这里Widget和ViewModel绑定,理论上来说当这个[StatefulWidget]对应的[StatefulElement]被移除的时候,
///与他绑定的ViewModel应该也被销毁,所以这里调用了ChangeNotifier.dispose()
///但是!!!还有种情况是,可能存在一个祖先ViewModel,这个Widget需要监听祖先ViewModel的数据变化,那么这里就不应该被销毁,这里暂时不考虑这种情况
widget.viewModel.dispose();
}
}
ok到这里代码就写完了,跑起来
可以看到,点击登陆显示出了loading弹窗,因为
LoginViewModel
里的state
这个ValueNotifier
的值被改成了LoginState.ing
了,这时候View层监听到了这个值的变化,所以调用showDialog
展示弹窗。然后我们在Model
层用3秒延迟模拟了网络请求然后返回假数据,ViewModel
层拿到结果后更新userName
,最后调用notifyListeners
通知View
层,完成View
层userName
的显示。
写在最后
本文只是举了一个比较简单的例子,页面也很简单,其实并不是一个页面就对应一个ViewModel类,它可以引用多个ViewModel,而引用多个ViewModel往往能带来更多的好处,比如更细粒度的控制页面中子Widget的更新、各个ViewModel实现自己的业务而不用把所有数据都放在页面级这个ViewModel里面;有时可能会有一个子Widget完成了他的业务逻辑后就从这个页面的element树上被移除了的需求,这时候如果能为这个子Widget绑定一个专属于它的ViewModel并且跟随Widget的生命周期,那么对性能也是一种提升。