前言
上一篇文章中,我们创建了 ArticleModel(Model 层),实现了从维基百科 API 获取文章数据。但 Model 只负责"取数据",它不知道数据什么时候该展示、界面什么时候该更新。
今天这篇文章基于官方教程的「Use ChangeNotifier to update app state」章节,我们将实现 MVVM 的第二层------ViewModel 。通过 Flutter 的 ChangeNotifier 类,ViewModel 能在数据变化时自动通知界面重绘。
一、ViewModel 在 MVVM 中的角色
回顾一下 MVVM 三层架构:
ViewModel 就像一个"调度中心":它从 Model 拿数据,管理数据的状态(加载中、成功、出错),然后通知 View 该显示什么。
二、认识 ChangeNotifier
2.1 它是什么?
ChangeNotifier 是 Flutter 内置的一个类,专门用于状态管理。它的核心能力只有一个:通知监听者数据发生了变化。
它的工作模式就像一个"广播电台":
- ViewModel 继承 ChangeNotifier → 变成一个"电台"
- View 注册为监听者 → 相当于"调到这个频道"
- ViewModel 调用
notifyListeners()→ 广播"数据更新了!" - View 收到通知 → 自动重新构建界面
2.2 和 setState 有什么区别?
在 Birdle 游戏中,我们用 setState 来触发界面更新。它简单直接,但有局限------状态和 UI 代码耦合在同一个 Widget 中。当应用变复杂后,这种方式会让代码难以维护和测试。
ChangeNotifier 把状态管理从 Widget 中独立出来,放进单独的类中。这样做的好处是:状态逻辑可以单独测试、可以被多个 Widget 共享、代码更清晰。
三、创建 ArticleViewModel
3.1 基本结构
scala
// ArticleViewModel:视图模型层(MVVM 中的 "VM")
//
// 继承 ChangeNotifier,获得 notifyListeners() 方法
// 职责:
// - 持有 Model 的引用,调用它获取数据
// - 管理三种状态:加载中(loading)、成功(summary)、出错(errorMessage)
// - 数据变化时调用 notifyListeners() 通知 UI 重绘
class ArticleViewModel extends ChangeNotifier {
// 持有 Model 的引用,通过它获取数据
final ArticleModel model;
// 三个状态属性:
Summary? summary; // 文章数据(成功时有值,出错时为 null)
String? errorMessage; // 错误信息(出错时有值,成功时为 null)
bool loading = false; // 是否正在加载(用于显示加载指示器)
// 构造函数:接收 Model 实例
ArticleViewModel(this.model);
}
3.2 三种状态的含义
ViewModel 用三个属性表达了数据的所有可能状态:
| 状态 | loading | summary | errorMessage | 界面该显示什么 |
|---|---|---|---|---|
| 加载中 | true |
null |
null |
加载指示器(转圈圈) |
| 成功 | false |
有值 | null |
文章内容 |
| 出错 | false |
null |
有值 | 错误提示 |
四、构造函数中自动获取数据
创建 ViewModel 时,应该立即开始获取数据,这样用户一打开应用就能看到内容:
scss
ArticleViewModel(this.model) {
// 构造函数中自动调用获取文章的方法
// 这样 ViewModel 一创建,就立刻开始请求数据
//
// 为什么不在构造函数中直接写 async?
// 因为 Dart 的构造函数不能是 async 的
// 所以委托给一个单独的 async 方法
getRandomArticleSummary();
}
五、实现数据获取方法
5.1 管理加载状态
csharp
Future<void> getRandomArticleSummary() async {
// ===== 开始加载 =====
// 将 loading 设为 true,通知 UI 显示加载指示器
loading = true;
notifyListeners(); // 广播:"我开始加载了,请显示转圈圈"
// TODO: 获取数据
// ===== 加载结束 =====
loading = false;
notifyListeners(); // 广播:"我加载完了,请更新界面"
}
注意 notifyListeners() 被调用了两次:开始加载时一次(让 UI 显示加载状态),加载完成后一次(让 UI 显示结果)。
5.2 获取数据并处理错误
ini
Future<void> getRandomArticleSummary() async {
// 开始加载
loading = true;
notifyListeners();
// 用 try-catch 包裹网络请求
// 网络操作可能失败(断网、服务器错误等),必须优雅处理
try {
// 调用 Model 的方法获取数据
summary = await model.getRandomArticleSummary();
// 成功:清除之前可能残留的错误信息
errorMessage = null;
} on HttpException catch (error) {
// 失败:保存错误信息,清除之前的文章数据
// 保持状态一致性:要么有文章,要么有错误,不能同时有
errorMessage = error.message;
summary = null;
}
// 加载结束
loading = false;
notifyListeners();
}
5.3 状态一致性
注意成功和失败时我们都做了"清理"操作:成功时清除 errorMessage,失败时清除 summary。这确保了任何时刻状态都是一致的------不会同时存在文章数据和错误信息。
六、测试 ViewModel
在构建完整 UI 之前,先用 print 验证 Model 和 ViewModel 是否连接正常:
ini
// 临时添加 print 语句验证
Future<void> getRandomArticleSummary() async {
loading = true;
notifyListeners();
try {
summary = await model.getRandomArticleSummary();
// 临时:打印文章标题到控制台
print('Article loaded: ${summary!.titles.normalized}');
errorMessage = null;
} on HttpException catch (error) {
// 临时:打印错误信息到控制台
print('Error loading article: ${error.message}');
errorMessage = error.message;
summary = null;
}
loading = false;
notifyListeners();
}
在 MainApp 中创建 ViewModel 来触发请求:
scala
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
// 创建 ViewModel,传入 Model 实例
// 构造函数中会自动调用 getRandomArticleSummary()
// 请求完成后会在控制台打印文章标题
final viewModel = ArticleViewModel(ArticleModel());
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Wikipedia Flutter'),
),
body: const Center(
// 提示用户查看控制台输出
child: Text('Check console for article data'),
),
),
);
}
}
热重载后查看终端,你应该能看到类似 Article loaded: Flutter (software) 的输出,说明 Model 和 ViewModel 连接成功!
七、本节知识点小结
ChangeNotifier: Flutter 内置的状态管理基类。继承它后获得 notifyListeners() 方法,调用它会通知所有监听的 Widget 重新构建。
ViewModel: MVVM 中的"调度中心"。从 Model 获取数据,管理加载/成功/出错三种状态,通过 notifyListeners 通知 UI 更新。
状态一致性: 成功时清除错误信息,失败时清除文章数据,确保任何时刻只有一种有效状态。用 try-catch 优雅处理网络错误。
构造函数初始化: 在 ViewModel 构造函数中自动发起数据请求,让用户一打开应用就能看到内容。构造函数不能是 async,所以委托给单独的异步方法。
八、下一步学习
Model 和 ViewModel 都就绪了,但 UI 还只是个"Loading..."占位页。下一课我们将学习 ListenableBuilder,创建 View 层,让界面自动监听 ViewModel 的变化并展示文章内容。
我们下篇文章见!