请写一篇关于【移动应用架构设计:MVVM实战】的技术文章
广州老城区的夏天,热得连蚊子都懒得咬你——但需求文档照常在凌晨两点发来。
去年十月,我刚拿到某一线大厂的offer,月薪从实习期的15k涨到22k,房租也从天河村的800块单间搬到了越秀区3500的“老破小”电梯房。老婆(对,应届生也可以有老婆,广府人结婚早嘛)一边帮我收拾行李一边嘀咕:“终于不用半夜被隔壁打呼吵醒了。”我嘴上答应着,心里却有点发虚——新项目要用MVVM重构整个App,而我上一份实习还在用MVC写“面条代码”。
被产品经理“背刺”的那个周五晚上
上周五晚上九点半,珠江新城的写字楼还亮着灯。我正坐在工位上啃肠粉(公司楼下那家开了二十年的老店,酱汁绝了),手机突然震了一下。
产品经理-阿强:@所有人 明天上线前必须把用户中心页面重构完,UI要按新设计稿走,交互逻辑全部重做。技术这边辛苦了!
我差点把肠粉喷出来。用户中心?那可是我们App里最臃肿的模块之一,光是网络请求就有十几个,状态判断写了三百多行if-else。更别提那些“临时加的需求”——比如“点击头像弹出三个选项但只显示两个,第三个等运营活动再开”这种经典祖传逻辑。
坐我旁边的前端大佬阿哲冷笑一声:“又来了,产品以为我们前端是魔术师,说变就变。”他转过头问我,“新人,你之前用过MVVM没?”
我咽下最后一口肠粉,硬着头皮点头:“学过……但没实战。”
阿哲拍了拍我肩膀:“那你今晚可以通宵了。不过别慌,MVVM就是来救我们的。”
MVVM到底救了谁?
说实话,刚接触MVVM时我也一脸懵。教科书上说它“解耦视图和逻辑”,听起来很高大上,但落到代码里,到底怎么写?
先说背景:我们用的是Flutter(对,现在大厂移动端基本都是跨端优先了),配合Provider做状态管理。MVVM在这里的意思是:
- Model:负责数据源,比如网络请求、数据库操作
- View:纯UI,只负责展示,不处理任何业务逻辑
- ViewModel:连接View和Model的“翻译官”,把原始数据转换成View能直接用的形式
听起来很清晰,对吧?但现实是——很多团队所谓的“MVVM”,最后写成了“VMVM”:View里偷偷调接口,ViewModel里塞了一堆UI逻辑,Model干脆不存在,全靠硬编码。
我第一次提交的PR就被阿哲打回来了,理由是:“你在View里直接用了http.get()?兄弟,这是2024年,不是2014年。”
真实战场:重构用户中心
我决定从最痛的点下手:用户信息加载。
老代码长这样(别笑):
class UserProfilePage extends StatefulWidget {
@override
_UserProfilePageState createState() => _UserProfilePageState();
}
class _UserProfilePageState extends State<UserProfilePage> {
User? user;
bool isLoading = true;
@override
void initState() {
super.initState();
_loadUser(); // 直接在State里发请求
}
_loadUser() async {
try {
final response = await http.get(Uri.parse('https://api.xxx/user'));
if (response.statusCode == 200) {
setState(() {
user = User.fromJson(json.decode(response.body));
isLoading = false;
});
}
} catch (e) {
// 错误处理?算了,弹个toast吧
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('加载失败')));
}
}
@override
Widget build(BuildContext context) {
if (isLoading) return CircularProgressIndicator();
return Column(
children: [
Text(user!.name),
// 还有一堆if判断用户等级、是否认证、有没有勋章……
],
);
}
}
这代码的问题在哪?
- View层干了Model的活:网络请求、JSON解析全塞进去了
- 状态混乱:
isLoading、user、error混在一起,稍复杂点就爆炸 - 无法测试:想mock数据?除非你愿意写一堆条件编译
用MVVM重写后:
第一步:定义Model
// model/user_model.dart
class UserModel {
Future<User> fetchUser(String userId) async {
final response = await http.get(Uri.parse('https://api.xxx/user/$userId'));
if (response.statusCode != 200) throw Exception('Failed to load user');
return User.fromJson(json.decode(response.body));
}
}
第二步:ViewModel负责转换
// viewmodel/user_viewmodel.dart
class UserViewModel with ChangeNotifier {
final UserModel _userModel = UserModel();
User? _user;
bool _isLoading = false;
String? _error;
User? get user => _user;
bool get isLoading => _isLoading;
String? get error => _error;
Future<void> loadUser(String userId) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_user = await _userModel.fetchUser(userId);
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
}
第三步:View只管展示
// view/user_profile_page.dart
class UserProfilePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final viewModel = Provider.of<UserViewModel>(context);
if (viewModel.isLoading) return CircularProgressIndicator();
if (viewModel.error != null) {
return Text('Error: ${viewModel.error}');
}
if (viewModel.user == null) return Text('No user');
return Column(
children: [
Text(viewModel.user!.name),
// 其他UI组件
],
);
}
}
看到区别了吗?
- View不再知道“数据从哪来”,它只关心“数据长什么样”
- ViewModel隔离了副作用(网络、异常),View永远面对干净的状态
- Model可以独立测试,甚至换掉实现(比如改成从本地缓存读)
工具链:没有趁手的工具,再好的架构也是空谈
光有架构不够,还得有工具支撑。
我们团队用了一套组合拳:
- Riverpod + AsyncNotifier:比Provider更灵活,支持异步状态管理
- Freezed:自动生成不可变数据类,避免手写
copyWith - Mockito:给Model写单元测试,确保ViewModel的输入可靠
- Flutter Inspector:实时查看Widget树和状态变化
举个例子,用Freezed定义User:
@freezed
class User with _$User {
const factory User({
required String id,
required String name,
required int level,
}) = _User;
}
一行注解,自动生成==、hashCode、toJson、copyWith……省下我至少两小时debug时间。
而Mockito让我能在不联网的情况下测试ViewModel:
test('loadUser returns user when API succeeds', () async {
final mockModel = MockUserModel();
when(mockModel.fetchUser('123')).thenAnswer((_) async => User(id: '123', name: '阿强', level: 5));
final viewModel = UserViewModel(model: mockModel);
await viewModel.loadUser('13');
expect(viewModel.user?.name, '阿强');
});
工具的意义,就是把重复劳动自动化,让你专注在真正需要思考的地方。
那些没人告诉你的坑
当然,MVVM也不是银弹。
第一个坑:过度分层。
我一开始把每个小功能都拆成独立的ViewModel,结果一个页面有五个Provider嵌套,调试时找状态来源像玩密室逃脱。
阿哲看了直摇头:“ViewModel不是越多越好,一个页面一个主ViewModel,内部状态用AsyncValue或Result封装就行。”
第二个坑:生命周期管理。
Flutter的StatefulWidget销毁时,如果ViewModel还在发请求,就会报“setState called after dispose”。解决方案是用CancelToken或Completer取消未完成的操作——但这又增加了复杂度。
第三个坑:团队认知成本。
有一次产品跑来问:“为什么改个按钮颜色要等三天?”因为颜色逻辑藏在ViewModel的某个状态字段里,前端得先改ViewModel,再改View。而以前MVC时代,直接在build方法里改就行。
架构的收益是长期的,但成本是即时的。 尤其在敏捷开发中,平衡“快”和“稳”是个永恒难题。
为什么我坚持用MVVM?
回到开头那个周五晚上。我熬到凌晨四点,终于把用户中心重构完。第二天上线,零崩溃,零回滚。阿强在群里发了个红包,说“技术给力”。
那一刻我突然明白了:MVVM不是为了炫技,而是为了在需求海啸中,给自己留一条逃生通道。
当产品说“加个新字段”,我不用翻遍整个页面找哪里要改;
当测试说“这个状态没覆盖”,我能快速写个单元测试验证;
当新人接手代码,他看一眼ViewModel就知道这个页面有哪些状态、会触发什么操作。
好的架构,是让混乱变得可预测。
写给同样在挣扎的你
如果你和我一样,是个刚进大厂的应届生,面对祖传代码瑟瑟发抖——别怕。
MVVM不是一天建成的,你可以从小模块开始:
- 先把网络请求抽到Model
- 把状态管理移到ViewModel
- View只保留UI和事件绑定
过程中肯定会踩坑,会被Code Review打回来十几次,会怀疑人生。但记住:所有整洁的代码,都曾是一团乱麻。
我现在工资涨了,房租贵了,但深夜加班时,至少不用再对着300行if-else祈祷“千万别崩”。这大概就是架构带来的安全感吧。
最后一点思考
技术选型从来不是非黑即白。MVVM适合中大型项目,但对于一个只有三个页面的小Demo,可能MVC反而更快。
工具服务于人,而不是人跪拜工具。
在广州这座既传统又现代的城市里,我学会了两件事:
- 早茶要慢慢叹(享受过程)
- 代码要慢慢重构(追求可持续)
希望下次见面,你也能笑着说出:“这个需求?交给我的ViewModel吧。”
—— 一个住在越秀老巷、用MVVM对抗需求变更的广府程序员
2024年6月于广州

评论 0