MVVM在Flutter中的实现


什么是MVVM

很多文章也介绍过了,那就说一说自己对它的理解吧。MVVM是如今主流的应用架构,它将我们的应用结构分为M(Model)V(View)VM(ViewModel),下面对这三部分进行简单的描述。

M-Model层

数据仓库层,你可以把你获取数据的方法(不管是从服务器还是从本地,调用者不关心数据是从哪里来的,数据来源规则在自己内部定义)、数据模型(各种entity、bean什么的)都放在这一层。

V-View层

表示层,一切用户能看到的东西都在这层;比如页面、弹窗、按钮、Toast等等,并接收一切用户输入,我们将所有和UI相关的代码都放在这一层。

VM-ViewModel层

业务逻辑层,我称它为中间层。它负责从Model层获取数据,又为绑定它的View提供量身定制的数据,它连接ViewModel并为View提供服务。ViewModel既要响应View的输入,又要在数据更新后通知View,它其实对于View来说就是一个被观察者,在整个View的生命周期中一直被View关注。


为什么推荐使用MVVM构建项目

若不使用一个好的架构去构建项目,当我们的项目逐渐庞大,维护起来是非常痛苦的。前不久就作死的重构过一个3000行代码的页面,State里面各种变量、各种网络请求、各种状态、各种setState,那滋味别提多酸爽。早些年的MVCMVP架构在这里就不赘述了,MVVM是由它们演变而来的,实现了ViewModel全解耦。基于这个特点它的好处太多了;比如以后我们可以基于每个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层,完成ViewuserName的显示。


写在最后

本文只是举了一个比较简单的例子,页面也很简单,其实并不是一个页面就对应一个ViewModel类,它可以引用多个ViewModel,而引用多个ViewModel往往能带来更多的好处,比如更细粒度的控制页面中子Widget的更新、各个ViewModel实现自己的业务而不用把所有数据都放在页面级这个ViewModel里面;有时可能会有一个子Widget完成了他的业务逻辑后就从这个页面的element树上被移除了的需求,这时候如果能为这个子Widget绑定一个专属于它的ViewModel并且跟随Widget的生命周期,那么对性能也是一种提升。

你可能感兴趣的:(MVVM在Flutter中的实现)