请写一篇关于【移动应用架构设计:MVVM实战】的技术文章

App码农
2026-01-14 21:44
阅读 1497

广州老城区的夏天,热得连蚊子都懒得咬你——但需求文档照常在凌晨两点发来。

去年十月,我刚拿到某一线大厂的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解析全塞进去了
  • 状态混乱isLoadingusererror混在一起,稍复杂点就爆炸
  • 无法测试:想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;
}

一行注解,自动生成==hashCodetoJsoncopyWith……省下我至少两小时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,内部状态用AsyncValueResult封装就行。”

第二个坑:生命周期管理
Flutter的StatefulWidget销毁时,如果ViewModel还在发请求,就会报“setState called after dispose”。解决方案是用CancelTokenCompleter取消未完成的操作——但这又增加了复杂度。

第三个坑:团队认知成本
有一次产品跑来问:“为什么改个按钮颜色要等三天?”因为颜色逻辑藏在ViewModel的某个状态字段里,前端得先改ViewModel,再改View。而以前MVC时代,直接在build方法里改就行。

架构的收益是长期的,但成本是即时的。 尤其在敏捷开发中,平衡“快”和“稳”是个永恒难题。


为什么我坚持用MVVM?

回到开头那个周五晚上。我熬到凌晨四点,终于把用户中心重构完。第二天上线,零崩溃,零回滚。阿强在群里发了个红包,说“技术给力”。

那一刻我突然明白了:MVVM不是为了炫技,而是为了在需求海啸中,给自己留一条逃生通道。

当产品说“加个新字段”,我不用翻遍整个页面找哪里要改;
当测试说“这个状态没覆盖”,我能快速写个单元测试验证;
当新人接手代码,他看一眼ViewModel就知道这个页面有哪些状态、会触发什么操作。

好的架构,是让混乱变得可预测。


写给同样在挣扎的你

如果你和我一样,是个刚进大厂的应届生,面对祖传代码瑟瑟发抖——别怕。
MVVM不是一天建成的,你可以从小模块开始:

  1. 先把网络请求抽到Model
  2. 把状态管理移到ViewModel
  3. View只保留UI和事件绑定

过程中肯定会踩坑,会被Code Review打回来十几次,会怀疑人生。但记住:所有整洁的代码,都曾是一团乱麻。

我现在工资涨了,房租贵了,但深夜加班时,至少不用再对着300行if-else祈祷“千万别崩”。这大概就是架构带来的安全感吧。


最后一点思考

技术选型从来不是非黑即白。MVVM适合中大型项目,但对于一个只有三个页面的小Demo,可能MVC反而更快。

工具服务于人,而不是人跪拜工具。

在广州这座既传统又现代的城市里,我学会了两件事:

  • 早茶要慢慢叹(享受过程)
  • 代码要慢慢重构(追求可持续)

希望下次见面,你也能笑着说出:“这个需求?交给我的ViewModel吧。”

—— 一个住在越秀老巷、用MVVM对抗需求变更的广府程序员
2024年6月于广州

评论 0

最热最新
暂无评论
App码农Lv.1
0
影响力
0
文章
0
粉丝