前言
在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'],
);
}
}