Flutter Mvvm实践

1.Mvvm简单介绍

学习完Flutter后在准备上手进行项目搭建的时候,我突然有一个很大的疑问,就是怎么使用Mvvm进行开发?网上查询了一下相关文章后,也没有一个很详细具体的方案,也有部分方案是基于一些热门的第三方框架结合而成的,万一这些框架以后不维护了那怎么办,需要有一套原生的方案,经过资料和博客的查阅,自己实践了一套Mvvm方案分享给大家,如果大家在阅读的过程中发现有不对的地方也希望大家可以留言提出,如果觉得对你有帮助也不要吝啬点赞、收藏~

如果大家对Mvvm已经很熟悉了,可以直接跳到第2点开始阅读具体实践。

1.1 为什么需要Mvvm?

Mvvm其实在移动端开发已经是老生常谈的知识了,我现在提出一个疑问,如果我们不使用Mvvm模式,我们的代码会是怎么样的样子?没错,无论是UI代码,还是业务逻辑都会耦合在一个文件或者类里面,当经过不断地迭代,你会发现这个类会发展成成千上万行代码,耦合还特别严重,当你想改某个点的时候,极有可能牵一发动全身,那么这个时候就需要一种方式,将代码分好类,并且规定类与类之间的交互与通信方式,这样分好类的代码才会更加容易扩展、复用还有阅读。

1.2 Mvvm介绍

首先我不会以十分学术性的去讲述Mvvm,避免讲得太过笼统,会偏直白简单的去描述一下Mvvm,有什么不对的地方希望大家可以友善的提出。

其实Mvvm是经过发展后得出的产物,它的前身是Mvc、Mvp,但是基于篇幅,这里不会去介绍Mvc和Mvp,我们只需要知道Mvc和Mvp也是代码分类的一种思想,接下来会简单介绍Mvvm模式,首先上一幅图:

image

我们从图中可以看出2个信息:

  1. 代码是如何分类的
  2. 每类代码他们是如何通信交互的

我们可以看出,Mvvm将代码分成三类,按照我的理解分为以下三类:

  1. View(视图代码)

毫无疑问,View就是用于编写视图相关的代码的一类,在Flutter中典型的实现就是我们的StatefulWidget与StatelessWidget,我们通常会将代码写在其中,所以在StatefulWidget与StatelessWidget就是我们的View层,我们会将UI代码写在其中,需要注意的是,View层中不会直接跟Model进行通讯(比如网络请求等),View会通过ViewModel进行对Model的获取。

  1. ViewModel(逻辑代码)

ViewModel存在目的在于抽离View中展示的业务逻辑,主要负责业务逻辑的处理,同时与 Model 层 和 View层交互。

  1. Model(数据)

负责与数据库和网络层通信,获取并存储数据。与MVP的区别在于Model层不再通过回调通知业务逻辑层数据改变,而是通过观察者模式实现。

看到这里,估计大家会对View-ViewModel-model是什么,应该有一个简单的认识了,但是对他们具体怎么交互通信可能还是有点抽象,我们可以从图中看到:

  1. 实线部分:View层通过ViewModel层对Model进行操作
  2. 虚线部分:Model层通过ViewModel层对View层进行驱动

所以View与Model不会直接关联,会通过ViewModel这一个中枢机构去交互,ViewModel本身处理业务逻辑的同时也会处理Model与View的通信和事件。

这样做有个好处就是,他们的流向十分清晰,开发者会知道,View产生任何的事件,都一定是流向ViewModel的,View接收任何事件,都是从ViewModel接收的。

MVVM架构的本质是数据驱动,它的最大的特点是单向依赖。MVVM架构通过观察者模式让ViewModel与View解耦,实现了View依赖ViewModel,ViewModel依赖Model的单向依赖。

2.Mvvm在Flutter中的实现

了解Mvvm之后,接着的具体的步骤就是以下几步:

  1. 定义ViewModel层
  2. 定义View层
  3. 定义Model层
  4. 处理View与ViewModel的双向数据绑定(重点)

2.1 定义ViewModel

ViewModel我这边只定义了ViewModel的生命周期,在我的设计下面,ViewModel的生命周期是依赖View层的,所以先定义与Widget的生命周期一致的函数,但是我目前暂时先定义onCreate(对应initState)与onDispose(对应Widget的销毁)

/// 基类
abstract class BaseViewModel extends ChangeNotifier {
 
  BaseViewModel() ;

  //尽量在onCreate方法中编写初始化逻辑
  onCreate();

  //对应的widget被销毁时
  onDispose() {}

}

/// 页面继承的ViewModel
abstract class PageViewModel extends BaseViewModel {
  PageViewModel(super.context);
  @override
  onCreate() {
  }

}

///

ViewModel中我区分了BaseViewModel与PageViewModel,后面还会有WidgetViewModel,趁早区分一下继承的关系,后面好扩展。

2.2 View层的封装

大家都知道,Flutter中UI是离不开Widget的,Flutter中的Widget分两种,一种是无状态控件(StatelessWidget),一种是有状态控件(StatefulWidget),无状态控件自身并没有刷新UI的功能,所以我们重点还是关注对StatefulWidget的封装,但是封装的关键还是其State,StatefulWidget本身没有太多逻辑

///定义ViewModelWidget基类
abstract class BaseViewModelWidget<VM extends BaseViewModel> extends StatefulWidget {
  const BaseViewModelWidget({Key? key}) : super(key: key);


  @override
  BaseViewModelState<BaseViewModelWidget,VM> createState();
}

///定义ViewModelState(相关逻辑在这里)
abstract class BaseViewModelState<T extends StatefulWidget,
    VM extends BaseViewModel> extends BaseState<T> {
    
  ///定义其ViewModel
  late VM viewModel;

  ///进行初始化ViewModel相关操作
  @override
  void initState() {
    super.initState();
    ///初始化ViewModel,并同步生命周期
    viewModel = initViewModel();
    //延迟初始化,避免在initState中对context进行了操作,此时context可能为空
    Future.delayed(Duration.zero).then((value) {
      ///此刻可以进行一些绑定操作
      initObserver();
      ///调用ViewModel的生命周期,此时可以进行一些初始化,比如网络请求等
      viewModel.onCreate();
      ///Widget本身的一些数据初始化,比如参数之类的
      initData();
    });
  }

  ///
  @override
  void dispose() {
    super.dispose();
    viewModel.dispose();
  }

  //初始化ViewModel
  VM initViewModel();

  ///子类重写,初始化数据
  void initData();

  ///子类重写,初始化监听
  void initObserver() {}

}

进一步的派生出BasePage,区分页面与普通的Widget的继承关系。

abstract class BasePage<VM extends PageViewModel> extends BaseViewModelWidget<VM> {
  const BasePage({Key? key}) : super(key: key);


  @override
  BasePageState<BasePage,VM> createState();

}


abstract class BasePageState<T extends BasePage, VM extends PageViewModel>
    extends BaseViewModelState<T, VM> {

}


目前都是空继承,如果有Page自有的特性,可以直接在BasePage和BasePageState中加。

2.3 数据绑定

在说双向数据绑定之前,我们会遇到两个问题:

  1. 怎么进行数据观察?
  2. 怎么局部刷新控件?

关于第二点,要先说说Flutter的setState,我们都知道通过setState会刷新当前整个State,这样成本和性能都会比较大,我们需要的是只刷新布局中某个跟数据绑定的元素而已,那么在Flutter中要怎么局部的绑定数据刷新呢?

确认了要解决的问题之后,我们就一个一个去解决。

2.3.1 数据观察

数据观察这里,我也有看过一些第三方框架比如Provider还有Riverpod等等,但是我始终觉得没安全感,而且在使用上总有一些怪怪的感觉,并不适合我这个MVVM的设计,最后经过查阅博客与资料,发现了原生的两个观察者类==ChangeNotifier==与==ValueNotifier==,至少可以保证了是原生方案,安全感是大大的有,在这里我就只介绍简单的用法,后面我会再出一篇源码分析

2.3.2 ChangeNotifier简单介绍

/// 
class ChangeNotifier implements Listenable {

  static final List<VoidCallback?> _emptyListeners = List<VoidCallback?>.filled(0, null);
  
  ///观察者回调列表
  List<VoidCallback?> _listeners = _emptyListeners;
 
  ///添加监听回调
  @override
  void addListener(VoidCallback listener) {
   ///...
  }

  ///去掉某个监听回调
  @override
  void removeListener(VoidCallback listener) {
   ///...
  }

  ///释放全部监听者(防止内存泄漏)
  @mustCallSuper
  void dispose() {
   ///...
  }

  
  ///触发监听列表回调
  void notifyListeners() {
   ///...
  }
}

ChangeNotifier本身只能添加和移除观察者、通知观察者,是一个纯触发事件的类,本身不会维护任何字段,接着介绍ValueNotifier

2.3.3 ValueNotifier

ValueNotifier是ChangeNotifier的子类,内部还会维护一个泛型的字段数据,当数据改变时,会通知观察者,我们简单看看源码

class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
  /// Creates a [ChangeNotifier] that wraps this value.
  ValueNotifier(this._value);
  
  @override
  T get value => _value;
  T _value;
  set value(T newValue) {
    if (_value == newValue) {
      return;
    }
    _value = newValue;
    notifyListeners();
  }

  @override
  String toString() => '${describeIdentity(this)}($value)';
}


我们可以看出来,ValueNotifier内部维护了一个泛型 _value字段,当我们调用set方法的时候,就会通知观察者,ValueNotifier本身并不是很复杂。

那么,我们找到了原生观察者的方案后,接下来就解决第二个问题,怎么局部刷新控件?

2.4 局部刷新

数据观察类有了,就差一个局部刷新,至于为什么需要局部刷新,上面已经描述过一遍了,我希望数据改变的时候,只刷新受影响的UI,那么先上成果~

///局部刷新控件
class NotifierWidget<T extends ChangeNotifier> extends StatefulWidget {
  ///需要监听的数据观察类
  final T data;
  ///构建UI的Builder,类似ListView的ItemBuilder
  final Widget Function(BuildContext context, T data) builder;
  ///构造函数
  ///data是需要监听的ValueNotifer或者ChangeNotifier
  const NotifierWidget(this.data, this.builder, {Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() => _NotifierState<T>();
}

class _NotifierState<T extends ChangeNotifier> extends State<NotifierWidget<T>> {

  ///刷新UI
  refreshState() {
    setState(() {});
  }
 
  ///initState时会对data进行监听
  @override
  void initState() {
    super.initState();
    //先清空一次已注册的Listener,防止重复触发
    widget.data.removeListener(refreshState);
    //添加监听,数据源改变时或者事件触发时,都会调用我们的refreshState函数,刷新ui,达到局部刷新的效果
    widget.data.addListener(refreshState);
  }

  ///build的时候,调用我们的builder,绘制对应的视图,并且将data回传
  @override
  Widget build(BuildContext context) {
    T data = widget.data;
    return widget.builder(context, data);
  }

 ///自动反注册监听,防止内存泄漏
  @override
  void dispose() {
    super.dispose();
    //销毁时,反注册
    widget.data.removeListener(refreshState);
  }
}

这个控件其实只做了两件事,监听事件、刷新UI,并没有设计得十分复杂,大家可以仔细看看源码,不会十分难理解

那么写到了这里,一个基本的MVVM的框架已经出来了,接着我们就使用一个登录的页面做一个简单的实践。

3.登录页面实践

3.1 ViewModel相关代码

class LoginVm extends PageViewModel {

  LoginVm();
  
  //是否请求登录中,影响按钮状态
  bool ValueNotifer<bool> isLogining = ValueNotifer(false);

  //账号
  String account = "";

  //密码
  String password = "";

  @override
  onCreate() {
    
  }

  //登录函数
  login() async {
    if (account.isEmpty) {
      showToast("账号不能为空");
      return;
    }
    if (password.isEmpty) {
      showToast("密码不能为空");
      return;
    }
    //更改登录状态
    isLogining.value = true;
    //更改登录请求状态
    BaseRespose data = await LoginApi.login(account, password);
    if(data.isSuccess()){
        ///登录成功
    }else{
        ///登录失败
    }
    //更改登录请求状态
    isLogining.value = false;
  }

  
}

3.2 页面代码

class LoginPage extends BasePage<LoginVm> {
  const LoginPage({super.key});

  @override
  BasePageState<BasePage<PageViewModel>, LoginVm> createState() {
    return _LoginState();
  }
}

class _LoginState extends BasePageState<LoginPage, LoginVm> {

  @override
  void initData() {}

  ///初始化ViewModel
  @override
  LoginVm initViewModel() {
    return LoginVm(context);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Container(
        padding: EdgeInsets.symmetric(horizontal: 38.rpx),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Padding(padding: EdgeInsets.only(top: 44.rpx)),
            ///账号输入框
            _buildInputWidget("账号",  (text) {
              ///文本改变回调,直接赋值viewModel中的值
              viewModel.account = text;
            }, false),
            Padding(padding: EdgeInsets.only(top: 24.rpx)),
            ///密码输入框
            _buildInputWidget("密码", (text) {
              ///文本改变回调,直接赋值viewModel中的值
              viewModel.password = text;
            }, true),
            Padding(padding: EdgeInsets.only(top: 44.rpx)),
            ///登录按钮,监听登录状态
            ValueNotifierWidget<bool>(
              viewModel.isLogining,
              (context, isLogining) =>TextButton(
                  style: ButtonStyle(
                    backgroundColor: MaterialStateProperty.all(
                      isLogining.value?Colors.grey: HexColor("#FF462C")///根据登录状态改变颜色
                    ),
                  ),
                  onPressed: () {
                    ///登录中的时候不能重复点击
                    if(!isLogining.value) {
                      viewModel.login();
                    }else{
                      Log.d("登录中,请勿重复点击");
                    }
                  },
                  child: Text(
                  ///登录中改变登录按钮文案
                    isLogining.value?"登录中":"登录",
                    style: TextStyle(fontSize: 18.rsp, color: Colors.white),
                  ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  ///输入框抽取
  _buildInputWidget(String hintText,
      ValueChanged<String> onChangeListener, bool obscureText) {
    return Container(
      decoration: BoxDecoration(
          color: HexColor("#FFF8F8"),
          borderRadius: BorderRadius.circular(200.rpx)),
      padding: EdgeInsets.symmetric(vertical: 9.rpx, horizontal: 25.rpx),
      child: TextField(
        obscureText: obscureText,
        onChanged: onChangeListener,
        style: TextStyle(color: HexColor("#FF462C"), fontSize: 16.rsp),
        decoration: InputDecoration(
          hintText: hintText,
          border: InputBorder.none,
          hintStyle: TextStyle(color: HexColor("#FF462C"), fontSize: 16.rsp),
        ),
      ),
    );
  }
}

以上就是Mvvm的实践,主要关注了几个点:

  1. ViewModel的编写,主要负责了业务逻辑与数据观察类的声明。
  2. Page的编写,主要关注如何初始化ViewModel、如何与ViewModel通讯,在按钮里面实践了数据绑定,局部刷新。

至此,Flutter的Mvvm实践其实到到此结束了,如果能给大家提供思路或者有实际性的帮助,希望大家能不吝啬手中的赞或者收藏,如果对文章有任何疑问,也可以在下方留言,我查看后会做出对应的修改。

感谢大家的观看。

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
相关推荐
  • 暂无相关文章
  • 评论 抢沙发
    头像
    欢迎您留下宝贵的见解!
    提交
    头像

    昵称

    取消
    昵称表情代码图片

      暂无评论内容