MVVM设计模式

77 阅读3分钟

前言

在Vue、React、SwiftUI、Flutter框架的出现和流行,让MVVM火起来。开发者确实从MVVM中得到了实惠。但如何更好的使用MVVM需要开发者进一步学习。

什么是MVVM?

在更早时候,开发者社区更推崇MVC设计模式,但MVC设计模式有一些问题,比如当业务复杂会出现“胖Controller”,并且VO和View的更新需要手动写代码,比较繁琐。甚至有些时候一个View作为业务组件会嵌入到其他多个View中,而把逻辑写到Controller并不能很好的实现这种需求。为了解决这些问题设计出MVVM和MVP。

因为早起iOS和Android都不支持VO更新自动更新View的进制,所以MVVM使用的并不广泛,尽管一些三方框架支持了这种能力,但从使用便捷度、性能、稳定性上都不尽如人意。

SwiftUI和Flutter的出现,让“VO更新自动更新View的进制”变的更稳定和高效。

MVVM是如何解决这些问题的?

  • 将Controller的逻辑拆分到不同的MV和M中
  • M拆分为更细分的M
  • View除了展示UI,还需提供绑定VO,实现VO更新自动更新UI。这部分功能通常由框架实现,程序员写胶水代码

比如有一个View的业务组件,需要嵌入到不同的Controller的页面中。那么我们可以帮这个业务组件打包为MVVM,在Controller的页面中将MVVM的V嵌入进去,就完成业务的多场景复用。

MVVM的职能划分是什么?

MVVM 的三个组成部分:

名称全称职责
View视图显示 UI,响应用户操作,绑定到 ViewModel
ViewModel视图模型提供数据和命令,与 View 绑定,不直接持有 View。viewmodle从modle获取数据do 并将do转换为vo 给到view
Model数据模型处理业务逻辑、网络请求、本地存储等

现实中我们是如何使用MVVM的?

实际开发过程中你会发现Model很单纯,多数是从网络/数据库获取数据,然后加工为DO,所以Model业务属性不强。Model上层会有一个具有很强业务属性的多个Service,这些Service具备很强的业务属性,不同的Service拿到多个Model的DO做进一步加工,将多种DO转换为具备业务属性的DTO,但DTO具备业务属性,不具备完全的UI属性。ViewModle会将DTO转换为具备强UI属性的VO,然后将VO给到View做数据绑定。

Net/DB-->Model-->Service-->ViewModel-->View

MVVM示例代码(Flutter)

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
      // home: A(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<String> initListViewData() {
    return List.generate(10, (index) => 'Item $index');
  }

  @override
  Widget build(BuildContext context) {
    print("build home page");

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: SafeArea(child: Column(children: [TT()])),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

class TT extends StatelessWidget {
  TT({super.key});

  final TestViewModel vm = TestViewModel();

  @override
  Widget build(BuildContext context) {
    print("build TT");
    if (vm.userInfo == null) vm.getUserInfo();

    return ListenableBuilder(
      listenable: vm,
      builder: (context, child) {
        print("build ListenableBuilder");
        return Padding(
          padding: const EdgeInsets.all(20.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text("userid: ${vm.userInfo?.userId ?? "null"}"),
              Text("title: ${vm.userInfo?.title ?? "null"}"),
              ElevatedButton(
                onPressed: () {
                  vm.getUserInfo();
                },
                child: Text("get data"),
              ),
            ],
          ),
        );
      },
    );
  }
}

class TestViewModel extends ChangeNotifier {
  UserInfo? userInfo;
  UserInfoModle userInfoModle = UserInfoModle();

  Future<void> getUserInfo() async {
    userInfo = await userInfoModle.getData();
    notifyListeners();
  }

  TestViewModel();
}

class UserInfoModle {
  UserInfo? userInfo;

  Future<String> _getData() async {
    var response = await http.get(
      Uri.parse("https://jsonplaceholder.typicode.com/albums/1"),
    );
    // print("getData called ${response.body}");
    return response.body;
  }

  Future<UserInfo?> getData() async {
    var data = await _getData();
    data = "{"userId":1,"id":1,"title":"quidem molestiae enim"}";
    try {
      userInfo = UserInfo.fromJson(jsonDecode(data) ?? "null");
    } catch (e) {
      print("error $e");
    }

    return userInfo;
  }
}

class UserInfo {
  int userId;
  int id;
  String title;
  bool? completed = false;

  UserInfo(this.userId, this.id, this.title, this.completed);

  factory UserInfo.fromJson(Map<String, dynamic> json) {
    return UserInfo(
      json['userId'],
      json['id'],
      json['title'],
      json['completed'],
    );
  }
}