Flutter Mvvm实践

Posted 浩辉-Hy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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模式,首先上一幅图:

我们从图中可以看出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);
  
  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);


  
  BaseViewModelState<BaseViewModelWidget,VM> createState();


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

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

  ///
  
  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);


  
  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的设计,最后经过查阅博客与资料,发现了原生的两个观察者类ChangeNotifierValueNotifier,至少可以保证了是原生方案,安全感是大大的有,在这里我就只介绍简单的用法,后面我会再出一篇源码分析

2.3.2 ChangeNotifier简单介绍

/// 
class ChangeNotifier implements Listenable 

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

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

  ///释放全部监听者(防止内存泄漏)
  
  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);
  
  
  T get value => _value;
  T _value;
  set value(T newValue) 
    if (_value == newValue) 
      return;
    
    _value = newValue;
    notifyListeners();
  

  
  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);

  
  State<StatefulWidget> createState() => _NotifierState<T>();


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

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

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

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

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

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

3.登录页面实践

3.1 ViewModel相关代码

class LoginVm extends PageViewModel 

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

  //账号
  String account = "";

  //密码
  String password = "";

  
  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);

  
  BasePageState<BasePage<PageViewModel>, LoginVm> createState() 
    return _LoginState();
  


class _LoginState extends BasePageState<LoginPage, LoginVm> 

  
  void initData() 

  ///初始化ViewModel
  
  LoginVm initViewModel() 
    return LoginVm(context);
  

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

感谢大家的观看。

Flutter Provider状态管理---MVVM架构实战

源码仓库地址

github仓库地址

MVVM介绍

MVVM架构分为M(Model)、V(View)、VM(ViewModel)三个部分,他们分别处理自己的分工,在ViewModel之间使用ViewModel作为中介者,使ViewModel不受业务逻辑影响。

Model: 模型层,处理Api数据、模型相关业务

View: 视图层,UI呈现、使用者互动等。

ViewModel: 视图模型,处理逻辑、将数据绑定给View展示。

MVVM运行原理

当界面需要展示数据时,ViewViewModel绑定,ViewModelModel取得数据后,在ViewModel处理对应的业务逻辑,然后将数据处理,最后通过View更新并展示。

MVVM优点

  • 易于变更需求,降低耦合
  • 权责分工明确
  • 方便测试

MVVM缺点

  • 文件数量增加
  • bug定位较为不易
  • 数据绑定消耗资源

MVVM实战

下面这个项目实战是用ProviderMVVM搭建的架构,是一个笑话段子列表。

它包含了5主要类:

  • Service: 网络请求类
  • Model: 主要负责转换模型
  • View: 主要负责呈现UI,通过ViewModel获取数据并展示
  • Widgets: 单独的UI模块分离
  • ViewModel: 处理业务逻辑,将数据绑定给View展示

定义模型

将网络请求回来的数据转换为对应的模型

import 'dart:convert';

JokeModel jokeModelFromJson(String str) => JokeModel.fromJson(json.decode(str));

String jokeModelToJson(JokeModel data) => json.encode(data.toJson());

class JokeModel 
  JokeModel(
    this.data,
  );

  final List<Joke>? data;

  factory JokeModel.fromJson(Map<String, dynamic> json) => JokeModel(
    data: List<Joke>.from(json["data"].map((x) => Joke.fromJson(x))),
  );

  Map<String, dynamic> toJson() => 
    "data": List<dynamic>.from(data!.map((x) => x.toJson())),
  ;


class Joke 
  Joke(
    this.content,
    this.hashId,
    this.unixtime,
    this.updatetime,
  );

  final String? content;
  final String? hashId;
  final int? unixtime;
  final String? updatetime;

  factory Joke.fromJson(Map<String, dynamic> json) => Joke(
    content: json["content"],
    hashId: json["hashId"],
    unixtime: json["unixtime"],
    updatetime: json["updatetime"],
  );

  Map<String, dynamic> toJson() => 
    "content": content,
    "hashId": hashId,
    "unixtime": unixtime,
    "updatetime": updatetime,
  ;

定义网络请求类

网络请求用到第三方网路请求库Dio ^4.0.0,将请求回来的数据转换为模型,并更新ViewModel数据。

import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter_provider_example/provider_mvvm_example/model/joke_model.dart';
import 'package:flutter_provider_example/provider_mvvm_example/view_model/joke_view_model.dart';

class JokeService 
  static Future<void> getJokes(JokeViewModel jokeViewModel) async 

    var response = await Dio().get("http://v.juhe.cn/joke/content/text.php?page=1&pagesize=20&key=03303e4d34effe095cf6a4257474cda9");
    if (response.statusCode == 200) 
      // 转换模型
      JokeModel jokeModel = jokeModelFromJson(json.encode(response.data["result"]));
      // 更新数据
      jokeViewModel.setJokeList(jokeModel);
    
  

定义ViewModel

这个ViewModel主要负责把请求回来的数据进行处理,并通知View层更新数据

import 'package:flutter/material.dart';
import 'package:flutter_provider_example/provider_mvvm_example/model/joke_model.dart';

class JokeViewModel with ChangeNotifier 

  List<Joke>? _jokeList = [];

  late Joke _joke;

  bool loading = true;

  setJokeList(JokeModel jokeModel) 
    _jokeList = [];
    _jokeList = jokeModel.data;
    loading = false;
    notifyListeners();
  

  setJoke(Joke joke) 
    _joke = joke;
  

  List<Joke>? get jokeList => _jokeList;

  Joke get joke => _joke;

定义View

我们在页面刚进入时进行初始化,然后通过ProviderConsumer来进行监听状态的变化。

import 'package:flutter/material.dart';
import 'package:flutter_provider_example/provider_mvvm_example/service/joke_service.dart';
import 'package:flutter_provider_example/provider_mvvm_example/view_model/joke_view_model.dart';
import 'package:flutter_provider_example/provider_mvvm_example/widgets/joke_item.dart';
import 'package:provider/provider.dart';

class JokeView extends StatefulWidget 
  @override
  _JokeViewState createState() => _JokeViewState();


class _JokeViewState extends State<JokeView> 
  @override
  void initState() 

    // 获取接口数据
    JokeService.getJokes(Provider.of<JokeViewModel>(context, listen: false));

    super.initState();
  

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(
        title: Text("Provider + MVVM"),
      ),
      body: Consumer<JokeViewModel>(
        builder: (_, jokeViewModel, child) 

          JokeViewModel _jokeViewModel = jokeViewModel;


          if (jokeViewModel.loading) 
            return Center(
              child: CircularProgressIndicator(),
            );
          

          return ListView.separated(
            itemBuilder: (_, index) 
              _jokeViewModel.setJoke(_jokeViewModel.jokeList![index]);

              return JokeItem(jokeViewModel: _jokeViewModel);
            ,
            itemCount: _jokeViewModel.jokeList?.length ?? 0,
            separatorBuilder: (_, index) 
              return Divider(
                height: 1,
              );
            ,
          );

        ,
      ),
    );
  

定义Widgets

把需要单独抽离的UI放在widgets中,并把ViewModel传入进来。

import 'package:flutter/material.dart';
import 'package:flutter_provider_example/provider_mvvm_example/model/joke_model.dart';
import 'package:flutter_provider_example/provider_mvvm_example/view_model/joke_view_model.dart';

class JokeItem extends StatelessWidget 

  JokeItem(
    Key? key,
    this.jokeViewModel
  ) : super(key: key);

  final JokeViewModel? jokeViewModel;

  @override
  Widget build(BuildContext context) 
    return Container(
      padding: EdgeInsets.only(
        left: 15,
        right: 15,
        top: 10,
        bottom: 10
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        children: [
          Text("$jokeViewModel?.joke?.content ?? """,
            style: TextStyle(
              color: Colors.black87,
              letterSpacing: 1.3,
              wordSpacing: 2
            ),
          ),
          SizedBox(height: 5,),
          Text("$jokeViewModel?.joke?.updatetime ?? "--"")
        ],
      ),
    );
  

引用View

import 'package:flutter/material.dart';
import 'package:flutter_provider_example/provider_mvvm_example/view/joke_view.dart';

class ProviderMvvmExample extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return JokeView();
  

应用程序入口设置

运行结果

总结

以上就是一个很简单的列表功能MVVM示例,在实际的情况下也不见得这是最好的方式,MVVM还有很多变种写法,但核心是一样的。

最后说一句,架构只是辅助而已,世界没有最好的架构。与其讨论这些,还不如想想这些架构为什么会出现?它的前因后果又是什么?在什么情况下要使用哪种架构?

 

以上是关于Flutter Mvvm实践的主要内容,如果未能解决你的问题,请参考以下文章

Flutter 最佳实践 - 03

在 Flutter Native Android 代码中实现 MVVM 架构

Flutter Provider状态管理---MVVM架构实战

Flutter开发 - 使用GetX框架实现类似MVVM架构

从Kotlin到Flutter,嗯,还是MVVM更香!

Flutter 绘制实践 | 路径篇 - 阴影模糊