状态变化时重建 UI
如何使用 ChangeNotifier 管理状态的说明。
学习使用 ListenableBuilder 在状态变化时自动重建 UI,并用 switch 表达式处理所有可能的状态。
你将完成的内容
步骤
1
简介
简介
View 层就是你的 UI,在 Flutter 中,这指的是你应用中的 widget。就本教程而言,重要的是将 UI 与 ViewModel 的数据变化关联起来。
ListenableBuilder
是一个可以「监听」ChangeNotifier
的 widget,当其提供的 ChangeNotifier 调用 notifyListeners() 时会自动重建。
2
创建文章视图 widget
创建文章视图 widget
创建 ArticleView widget,用于管理页面的布局与 ViewModel 生命周期。由于必须在渲染前显式初始化数据获取,请将其实现为 StatefulWidget。
先创建基本的 stateful 结构:
import 'package:flutter/material.dart';
class ArticleView extends StatefulWidget {
const ArticleView({super.key});
@override
State<ArticleView> createState() => _ArticleViewState();
}
class _ArticleViewState extends State<ArticleView> {
// The view model will be instantiated here next.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Wikipedia Flutter')),
body: const Center(child: Text('Loading...')),
);
}
}
3
实例化文章 ViewModel
实例化文章 ViewModel
接下来,初始化 ArticleViewModel 并将其与 state 的生命周期绑定。在 initState() 中提供 ViewModel 并执行 fetchArticle():
class ArticleView extends StatefulWidget {
const ArticleView({super.key});
@override
State<ArticleView> createState() => _ArticleViewState();
}
class _ArticleViewState extends State<ArticleView> {
final ArticleViewModel viewModel = ArticleViewModel(ArticleModel());
@override
void initState() {
super.initState();
viewModel.fetchArticle();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Wikipedia Flutter')),
body: const Center(child: Text('Loading...')),
);
}
}
4
更新应用以包含文章视图
更新应用以包含文章视图
通过更新 MainApp 以包含已完成的 ArticleView,将所有部分连接起来。
用以下更新版本替换现有的 MainApp:
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: ArticleView());
}
}
这一更改从基于控制台的测试切换到具备完善状态管理的完整 UI 体验。
5
监听状态变化
监听状态变化
用 ListenableBuilder
包裹 UI 以监听状态变化,并向其传入 ChangeNotifier 对象。在本例中,ArticleViewModel 继承自 ChangeNotifier。
class ArticleView extends StatefulWidget {
const ArticleView({super.key});
@override
State<ArticleView> createState() => _ArticleViewState();
}
class _ArticleViewState extends State<ArticleView> {
final ArticleViewModel viewModel = ArticleViewModel(ArticleModel());
@override
void initState() {
super.initState();
viewModel.fetchArticle();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Wikipedia Flutter')),
body: ListenableBuilder(
listenable: viewModel,
builder: (context, child) {
return const Center(child: Text('Loading...'));
},
),
);
}
}
ListenableBuilder 使用 builder 模式,它需要回调而不是 child widget 来构建其下方的 widget 树。这些 widget 很灵活,因为你可以在回调中执行操作,根据状态构建不同的 widget。
6
处理 ViewModel 的可能状态
处理 ViewModel 的可能状态
回顾 ArticleViewModel,它有三个 UI 关心的属性:
Summary? summarybool isLoadingException? error
根据这些属性的组合状态, UI 可以显示不同的 widget。利用 Dart 对 switch 表达式 的支持,以清晰、可读的方式处理所有可能的组合:
class ArticleView extends StatefulWidget {
const ArticleView({super.key});
@override
State<ArticleView> createState() => _ArticleViewState();
}
class _ArticleViewState extends State<ArticleView> {
final ArticleViewModel viewModel = ArticleViewModel(ArticleModel());
@override
void initState() {
super.initState();
viewModel.fetchArticle();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Wikipedia Flutter')),
body: Center(
child: ListenableBuilder(
listenable: viewModel,
builder: (context, _) {
return switch ((
viewModel.isLoading,
viewModel.summary,
viewModel.error,
)) {
(true, _, _) => const CircularProgressIndicator(),
(_, _, final Exception e) => Text('Error: $e'),
(_, final summary?, _) => ArticlePage(
summary: summary,
nextArticleCallback: viewModel.fetchArticle,
),
_ => const Text('Something went wrong!'),
};
},
),
),
);
}
}
这是声明式、响应式框架(如 Flutter)与 MVVM 等模式如何协同工作的绝佳示例: UI 根据状态渲染,并在状态变化需要时更新,但它不管理任何状态,也不管理自我更新的过程。业务逻辑与渲染完全彼此分离。
7
完成 UI
完成 UI
剩下要做的就是用 ViewModel 提供的属性和方法来构建 UI。
现在创建 ArticlePage widget 以显示实际文章内容。这个可复用 widget 接收摘要数据和回调函数:
class ArticlePage extends StatelessWidget {
const ArticlePage({
super.key,
required this.summary,
required this.nextArticleCallback,
});
final Summary summary;
final VoidCallback nextArticleCallback;
@override
Widget build(BuildContext context) {
return const Center(
child: Text('Article content will be displayed here...'),
);
}
}
8
添加可滚动布局
添加可滚动布局
用可滚动的 Column 布局替换占位内容:
class ArticlePage extends StatelessWidget {
const ArticlePage({
super.key,
required this.summary,
required this.nextArticleCallback,
});
final Summary summary;
final VoidCallback nextArticleCallback;
@override
Widget build(BuildContext context) {
return const SingleChildScrollView(
child: Column(
children: [Text('Article content will be displayed here...')],
),
);
}
}
9
添加文章内容与按钮
添加文章内容与按钮
用文章 widget 和导航按钮完成布局:
class ArticlePage extends StatelessWidget {
const ArticlePage({
super.key,
required this.summary,
required this.nextArticleCallback,
});
final Summary summary;
final VoidCallback nextArticleCallback;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: [
ArticleWidget(summary: summary),
ElevatedButton(
onPressed: nextArticleCallback,
child: const Text('Next random article'),
),
],
),
);
}
}
10
创建 ArticleWidget
创建 ArticleWidget
ArticleWidget 负责以合适的样式和条件渲染显示实际文章内容。
搭建基本文章结构
#从接受 summary 参数的 widget 开始:
class ArticleWidget extends StatelessWidget {
const ArticleWidget({super.key, required this.summary});
final Summary summary;
@override
Widget build(BuildContext context) {
return const Text('Article content will be displayed here...');
}
}
添加内边距与 Column 布局
#用合适的内边距和布局包裹内容:
class ArticleWidget extends StatelessWidget {
const ArticleWidget({super.key, required this.summary});
final Summary summary;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: Column(
spacing: 10,
children: [const Text('Article content will be displayed here...')],
),
);
}
}
添加条件图片显示
#添加仅在可用时显示的文章图片:
class ArticleWidget extends StatelessWidget {
const ArticleWidget({super.key, required this.summary});
final Summary summary;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: Column(
spacing: 10,
children: [
if (summary.hasImage) Image.network(summary.originalImage!.source),
const Text('Article content will be displayed here...'),
],
),
);
}
}
用带样式的文本内容完成
#用带样式的标题、描述和摘录替换占位文本:
class ArticleWidget extends StatelessWidget {
const ArticleWidget({super.key, required this.summary});
final Summary summary;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: Column(
spacing: 10,
children: [
if (summary.hasImage) Image.network(summary.originalImage!.source),
Text(
summary.titles.normalized,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.displaySmall,
),
if (summary.description != null)
Text(
summary.description!,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
Text(summary.extract),
],
),
);
}
}
该 widget 演示了几个重要的 UI 概念:
-
条件渲染:
if语句仅在内容可用时显示。 -
文本样式:不同文本样式借助 Flutter 的主题系统建立视觉层次。
-
合适间距:
spacing参数提供一致的垂直间距。 -
溢出处理:
TextOverflow.ellipsis防止文本破坏布局。
11
运行完整应用
运行完整应用
最后一次热重载应用。你现在应能看到:
初始文章加载时显示的加载指示器。
文章的标题、描述和摘要摘录。
图片(若文章有图片)。
用于加载另一篇随机文章的按钮。
要查看响应式 UI 的实际效果,点击 Next random article(下一篇随机文章)按钮。应用会显示加载状态、获取新数据,并自动更新显示。
12
回顾
回顾
你已完成的内容
以下是你本课构建与学习内容的摘要。使用 ListenableBuilder 自动重建 UI
ListenableBuilder 会监听你的 ViewModel,并在每次调用 notifyListeners() 时自动重建其子 widget。在 MVVM 模式中,这是 ViewModel 与 View 之间的关键连接。
用 switch 表达式处理所有可能的状态
通过 switch 表达式,你为可能的状态组合提供了合适的用户界面,有条件地显示加载指示器、错误消息或实际文章内容。有了这种处理,UI 现在更加健壮和完整。
用合适的样式构建完整的 View 层
你创建了 ArticleView、ArticlePage 和 ArticleWidget,包含条件渲染、文本样式、合适间距和溢出处理。这些是你将在每个 Flutter 应用中使用的核心 UI 模式。
完成了 MVVM 架构
你已构建包含 Model(数据操作)、 ViewModel(状态管理)和 View(响应式 UI)层的完整应用。这种关注点分离有助于代码更易测试、维护和扩展。
13
自测
自测
ListenableBuilder 测验
1 / 2-
基于 ChangeNotifier 创建动画。
不正确。
ListenableBuilder 在状态变化时重建 UI,并非专门用于动画。
-
监听 ChangeNotifier,并在调用
notifyListeners()时自动重建其子 widget。正确!
ListenableBuilder 监听 Listenable,并在收到通知时重建其 builder 函数。
-
手动控制何时应重建 widget。
不正确。
调用 notifyListeners() 时重建是自动的;你无需手动控制。
-
缓存 widget 构建以提升性能。
不正确。
ListenableBuilder 用于响应式更新,而非缓存。
-
每次应用帧刷新时。
不正确。
ListenableBuilder 仅在收到通知时重建,而非每一帧。
-
当它监听的 Listenable 调用 notifyListeners() 时。
正确!
ListenableBuilder 订阅 Listenable,并在每次调用
notifyListeners()时重建其 builder 函数。 -
仅在 widget 首次挂载时。
不正确。
它在每次调用
notifyListeners()时都会重建,而不仅限于挂载时。 -
当父 widget 重建时。
不正确。
ListenableBuilder 根据 Listenable 重建,而非父 widget 重建。
除非另有说明,本文档之所提及适用于 Flutter 3.44.0 版本。本页面最后更新时间:2026-06-18。查看文档源码 或者 为本页面内容提出建议。