Flutter ChangeNotifier用 ViewModel 管理应用状态(九)

前言

上一篇文章中,我们创建了 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 的变化并展示文章内容。

我们下篇文章见!

参考资料:Flutter 官方教程 - Use ChangeNotifier to update app state

相关推荐
用户4099322502122 小时前
Vue 3 静态与动态 Props 如何传递?TypeScript 类型约束有何必要?
前端·vue.js·后端
程序员库里2 小时前
TipTap简介
前端·javascript·面试
关中老四2 小时前
【js/web甘特图插件MZGantt】如何使用外部弹框添加和修改任务(updRows方法使用说明)
前端·javascript·甘特图·甘特图插件
nunumaymax2 小时前
css实现元素和文字从右向左排列
前端·css
yatum_20142 小时前
CentOS 7 集群 SSH 免密与主机名配置文档
linux·前端·网络
014-code2 小时前
Vue 生命周期完全指南
前端·javascript·vue.js
冴羽yayujs2 小时前
资深前端都在用的 9 个调试偏方
前端·javascript·调试
Amumu121382 小时前
CSS移动端
前端·css·css3