什么是MVVM
很多文章也介紹過了,那就說一說自己對它的理解吧。MVVM是如今主流的應用架構(gòu),它將我們的應用結(jié)構(gòu)分為M(Model)、V(View)、VM(ViewModel),下面對這三部分進行簡單的描述。

M-Model層
數(shù)據(jù)倉庫層,你可以把你獲取數(shù)據(jù)的方法(不管是從服務(wù)器還是從本地,調(diào)用者不關(guān)心數(shù)據(jù)是從哪里來的,數(shù)據(jù)來源規(guī)則在自己內(nèi)部定義)、數(shù)據(jù)模型(各種entity、bean什么的)都放在這一層。
V-View層
表示層,一切用戶能看到的東西都在這層;比如頁面、彈窗、按鈕、Toast等等,并接收一切用戶輸入,我們將所有和UI相關(guān)的代碼都放在這一層。
VM-ViewModel層
業(yè)務(wù)邏輯層,我稱它為中間層。它負責從Model層獲取數(shù)據(jù),又為綁定它的View提供量身定制的數(shù)據(jù),它連接View與Model并為View提供服務(wù)。ViewModel既要響應View的輸入,又要在數(shù)據(jù)更新后通知View,它其實對于View來說就是一個被觀察者,在整個View的生命周期中一直被View關(guān)注。
為什么推薦使用MVVM構(gòu)建項目
若不使用一個好的架構(gòu)去構(gòu)建項目,當我們的項目逐漸龐大,維護起來是非常痛苦的。前不久就作死的重構(gòu)過一個3000行代碼的頁面,State里面各種變量、各種網(wǎng)絡(luò)請求、各種狀態(tài)、各種setState,那滋味別提多酸爽。早些年的MVC、MVP架構(gòu)在這里就不贅述了,MVVM是由它們演變而來的,實現(xiàn)了View與Model全解耦?;谶@個特點它的好處太多了;比如以后我們可以基于每個ViewModel編寫單元測試了、更細粒度的控制View的刷新以提高性能等等。
實現(xiàn)一個簡單的MVVM
這里實現(xiàn)一個簡陋的登陸案例,創(chuàng)建如下結(jié)構(gòu)的文件

user_entity.dart用戶屬性數(shù)據(jù)模型
class UserEntity {
String userId;
String userName;
int age;
UserEntity.fromJson(Map json){
userId = json['userId'];
userName = json['userName'];
age = json['age'];
}
}
login_model.dart請求登陸接口、獲取數(shù)據(jù)相關(guān)
class LoginModel {
Future<UserEntity> login(String account,String password) async {
///模擬網(wǎng)絡(luò)請求并解析成功
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<StatefulWidget> 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();
///監(jiān)聽登陸狀態(tài)展示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 {
///登陸結(jié)束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>(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
///登陸過程的狀態(tài)
enum LoginState{
init,
ing,
success,
fail,
}
class LoginViewModel extends ChangeNotifier {
String userName;
///登陸狀態(tài)
ValueNotifier<LoginState> 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();
}
}
}
然后為了實現(xiàn)細粒度的刷新,編寫了一個簡單的基于ChangeNotifier機制的Widget
///一個簡單的ChangeNotifierWidget,可以監(jiān)聽ChangeNotifier的notifyListeners方法進行重建,
///這里的ChangeNotifier其實就是我們的ViewModel
typedef MyConsumer<T> = Widget Function(BuildContext ctx,T vm);
class ChangeNotifierWidget<T extends ChangeNotifier> extends StatefulWidget {
final T viewModel;
final Widget Function(BuildContext,T vm) builder;
ChangeNotifierWidget(this.viewModel, this.builder);
@override
State<StatefulWidget> createState() => _ChangeNotifierState<T>();
}
class _ChangeNotifierState<T extends ChangeNotifier> extends State<ChangeNotifierWidget<T>> {
@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應該也被銷毀,所以這里調(diào)用了ChangeNotifier.dispose()
///但是?。?!還有種情況是,可能存在一個祖先ViewModel,這個Widget需要監(jiān)聽祖先ViewModel的數(shù)據(jù)變化,那么這里就不應該被銷毀,這里暫時不考慮這種情況
widget.viewModel.dispose();
}
}
ok到這里代碼就寫完了,跑起來

可以看到,點擊登陸顯示出了loading彈窗,因為
LoginViewModel里的state這個ValueNotifier的值被改成了LoginState.ing了,這時候View層監(jiān)聽到了這個值的變化,所以調(diào)用showDialog展示彈窗。然后我們在Model層用3秒延遲模擬了網(wǎng)絡(luò)請求然后返回假數(shù)據(jù),ViewModel層拿到結(jié)果后更新userName,最后調(diào)用notifyListeners通知View層,完成View層userName的顯示。
寫在最后
本文只是舉了一個比較簡單的例子,頁面也很簡單,其實并不是一個頁面就對應一個ViewModel類,它可以引用多個ViewModel,而引用多個ViewModel往往能帶來更多的好處,比如更細粒度的控制頁面中子Widget的更新、各個ViewModel實現(xiàn)自己的業(yè)務(wù)而不用把所有數(shù)據(jù)都放在頁面級這個ViewModel里面;有時可能會有一個子Widget完成了他的業(yè)務(wù)邏輯后就從這個頁面的element樹上被移除了的需求,這時候如果能為這個子Widget綁定一個專屬于它的ViewModel并且跟隨Widget的生命周期,那么對性能也是一種提升。