Provider 原理介绍与实战(MVVM)(下)

1,003 阅读4分钟

这是我参与更文挑战的第19天,活动详情查看: 更文挑战

MVVM 是什么和优势

MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑

优点

MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),

1. 低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。

2. 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。

3. 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,使用Expression Blend可以很容易设计界面并生成xaml代码。

4. 可测试。界面素来是比较难于测试的,测试可以针对ViewModel来写。

View绑定到ViewModel,然后执行一些命令在向它请求一个动作。而反过来,ViewModel跟Model通讯,告诉它更新来响应UI。这样便使得为应用构建UI非常的容易。

img

Flutter中的MVVM模式的几种方式

在不使用任何第三方包的时候,官方也提供了不错的选择,那就是StatefulWidget,当我们需要改变状态来刷新UI时,只需要调用setState()方法。

这种方法简单直接,而且也可以理解为一种MVVM模式,只不过View和Model仍然耦合在一起,ViewModel并没有承担起它应有的角色。随着我们的工程变得越来越大时,代码里的setState()就会变得越来越多,显得非常混乱,并且有时候会忘记调用setState(),导致浪费很多时间来定位问题。

官方早期也提供的一种状态管理模式叫做BLOC。这种方式依赖于第三方包rxDart,以流(Stream)的方式很好地解决了setState()的问题。但是这种学习难度较大,对Flutter的新手并不友好。后来出现了一种第三方库Provider,这是一种先进的状态管理和依赖注入的工具,并且易于学习和理解,所以目前官方也推荐首选Provider

使用ViewModel进行状态管理

既然我们在MVVM模式模式下进行开发APP,那么ViewModel是必不可少的。也就是当状态属性变化时,我们需要UI(也就是View层)进行相应的更改。

Provider中有ChangeNotifierProvider可以帮助我们监听是否状态发生了变化,它的child参数是一个Consumer可以帮我们来消费状态的变化。通俗来讲就是在这里调用Widget的build方法来进行UI刷新。

那在哪里去触发状态变化的通知呢?答案就是使用ChangeNotifier,当调用其中的notifyListeners()方法时,就可以通知监听它的ChangeNotifierProvider进行刷新。

@override
Widget build(BuildContext context) {
  return ChangeNotifierProvider<T>(
    child: Consumer<T>(
      //Widget的builder方法与child
      builder: widget.builder,
      child: widget.child,
    ),
    create: (BuildContext context) {
      //这里是我们的ViewModel,一个ChangeNotifier
      return model;
    },
  );
}

现在我们的生产者消费者都有了,可以完善我们的MVVM模式了。

基础页面逻辑分析

一般页面载入的时候会显示一个loading,然后加载成功展示数据,失败就展示失败页面

![截屏2021-06-20 下午11.47.16](/Users/liaoyp/Desktop/截屏2021-06-20 下午11.47.16.png)

所以枚举一个页面状态:

enum ViewState { Idle, Busy,Error }

ViewModel都会在页面状态属性改变后更新ui,通常会调用notifyListeners,把这一步移到BaseModel中:

class BaseModel extends ChangeNotifier {
  ViewState _state = ViewState.Loading;
 
  ViewState get state => _state;
 
  void setState(ViewState viewState) {
    _state = viewState;
    notifyListeners();
  }
}

ui里需要ChangeNotifierProvider提供Model,并且用Consumer更新ui。因此我们也将其内置到BaseView中:


enum ViewState { Loading, Success, Failure, None }
 
class BaseModel extends ChangeNotifier {
  ViewState _state = ViewState.None;
 
  ViewState get state => _state;
 
  void setState(ViewState viewState) {
    _state = viewState;
    notifyListeners();
  }
}
 
class BaseWidget<T extends ChangeNotifier> extends StatefulWidget {
  final Widget Function(BuildContext context, T model, Widget child) builder;
  final T model;
  final Widget child;
  final Function(T) onModelReady;
 
  BaseWidget({
    Key key,
    this.builder,
    this.model,
    this.child,
    this.onModelReady,
  }) : super(key: key);
 
  _BaseWidgetState<T> createState() => _BaseWidgetState<T>();
}
 
class _BaseWidgetState<T extends ChangeNotifier> extends State<BaseWidget<T>> {
  T model;
 
  @override
  void initState() {
    model = widget.model;
 
    if (widget.onModelReady != null) {
      widget.onModelReady(model);
    }
 
    super.initState();
  }
 
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<T>(
      create: (BuildContext context) => model,
      child: Consumer<T>(
        builder: widget.builder,
        child: widget.child,
      ),
    );
  }
}

我们用封装的基类完成登录页面:


class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BaseWidget<LoginViewModel>(
      model: LoginViewModel(loginServive: LoginServive()),
      builder: (context, model, child) => Scaffold(
        appBar: AppBar(
          title: Text('provider'),
        ),
        body: Column(
          children: <Widget>[
            model.state == ViewState.Loading
                ? Center(
                    child: CircularProgressIndicator(),
                  )
                : Text(model.info),
            FlatButton(
                color: Colors.tealAccent,
                onPressed: () => model.login("pwd"),
                child: Text("登录")),
          ],
        ),
      ),
    );
  }
}
 
/// viewModel
class LoginViewModel extends BaseModel {
  LoginServive _loginServive;
  String info = '请登录';
 
  LoginViewModel({@required LoginServive loginServive})
      : _loginServive = loginServive;
 
  Future<String> login(String pwd) async {
    setState(ViewState.Loading);
    info = await _loginServive.login(pwd);
    setState(ViewState.Success);
  }
}
 
/// api
class LoginServive {
  static const String Login_path = 'xxxxxx';
 
  Future<String> login(String pwd) async {
    return new Future.delayed(const Duration(seconds: 1), () => "登录成功");
  }
}

参考

examplecode.cn/2020/05/09/…