依照上一篇中我们新建的flutter应用
让我们更改pubspec.yaml中的内容为
- name: namer_app
- description: A new Flutter project.
-
- publish_to: 'none' # Remove this line if you wish to publish to pub.dev
-
- version: 0.0.1+1
-
- environment:
- sdk: '>=2.19.4 <4.0.0'
-
- dependencies:
- flutter:
- sdk: flutter
-
- english_words: ^4.0.0
- provider: ^6.0.0
-
- dev_dependencies:
- flutter_test:
- sdk: flutter
-
- flutter_lints: ^2.0.0
-
- flutter:
- uses-material-design: true
pubspec.yaml 文件指定与您的应用相关的基本信息,例如其当前版本、依赖项以及其随附的资源。
注意:如果您为应用指定的名称不是 namer_app,则需要对第一行进行相应的更改
接下来,在项目中打开另一个配置文件 analysis_options.yaml。
将其内容替换为以下内容:
- include: package:flutter_lints/flutter.yaml
-
- linter:
- rules:
- prefer_const_constructors: false
- prefer_final_fields: false
- use_key_in_widget_constructors: false
- prefer_const_literals_to_create_immutables: false
- prefer_const_constructors_in_immutables: false
- avoid_print: false
此文件决定了 Flutter 在分析代码时的严格程度。由于这是您第一次使用 Flutter,您可以让分析器不用太严格。此后,您可以随时进行调整。事实上,在邻近发布实际正式版应用的阶段,您几乎肯定会希望分析器更加严格。
最后,打开 lib/ 目录下的 main.dart 文件。
将此文件的内容替换为以下内容。
注意:
重新下载dependeces
- import 'package:english_words/english_words.dart';
- import 'package:flutter/material.dart';
- import 'package:provider/provider.dart';
-
- void main() {
- runApp(MyApp());
- }
-
- class MyApp extends StatelessWidget {
- const MyApp({super.key});
-
- @override
- Widget build(BuildContext context) {
- return ChangeNotifierProvider(
- create: (context) => MyAppState(),
- child: MaterialApp(
- title: 'Namer App',
- theme: ThemeData(
- useMaterial3: true,
- colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
- ),
- home: MyHomePage(),
- ),
- );
- }
- }
-
- class MyAppState extends ChangeNotifier {
- var current = WordPair.random();
- }
-
- class MyHomePage extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- var appState = context.watch
(); -
- return Scaffold(
- body: Column(
- children: [
- Text('A random idea:'),
- Text(appState.current.asLowerCase),
- ],
- ),
- );
- }
- }
到目前为止,这 50 行代码是应用的全部。
在 lib/main.dart 的底部,向第一个 Text 对象中的字符串添加一些内容,然后保存文件(使用 Ctrl+S 或 Cmd+S)。例如:
- // ...
-
- return Scaffold(
- body: Column(
- children: [
- Text('A random AWESOME idea:'), // ← Example change.
- Text(appState.current.asLowerCase),
- ],
- ),
- );
-
- // ...
请注意应用会立即发生更改,但随机单词保持不变。这正是 Flutter 广为人知的有状态热重载功能在发挥作用。当您将更改保存到源文件时,系统会触发热重载。

接下来,在 Column 底部添加一个按钮,也就是第二个 Text 实例的正下方。
- // ...
-
- return Scaffold(
- body: Column(
- children: [
- Text('A random AWESOME idea:'),
- Text(appState.current.asLowerCase),
-
- // ↓ Add this.
- ElevatedButton(
- onPressed: () {
- print('button pressed!');
- },
- child: Text('Next'),
- ),
-
- ],
- ),
- );
-
- // ...
当您保存更改时,应用会再次更新:其中会显示一个按钮,当您点击该按钮时,IDE中的调试控制台会显示 button pressed! 消息。

尽管显示调试控制台很有趣,但您希望按钮执行更有意义的操作。不过,在开始之前,请仔细查看 lib/main.dart 中的代码,了解其工作原理。
- // ...
-
- void main() {
- runApp(MyApp());
- }
-
- // ...
在文件的最顶部,您可以找到 main() 函数。目前,该函数只是告知 Flutter 运行 MyApp 中定义的应用。
- // ...
-
- class MyApp extends StatelessWidget {
- const MyApp({super.key});
-
- @override
- Widget build(BuildContext context) {
- return ChangeNotifierProvider(
- create: (context) => MyAppState(),
- child: MaterialApp(
- title: 'Namer App',
- theme: ThemeData(
- useMaterial3: true,
- colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
- ),
- home: MyHomePage(),
- ),
- );
- }
- }
-
- // ...
MyApp 类扩展 StatelessWidget。在构建每一个 Flutter 应用时,widget 都是一个基本要素。如您所见,应用本身也是一个 widget。
注意:我们稍后将详细解释 StatelessWidget(相对于 StatefulWidget)。
MyApp 中的代码设置了整个应用,包括创建应用级状态(稍后会详细介绍)、命名应用、定义视觉主题以及设置“主页” widget,即应用的起点。
- // ...
-
- class MyAppState extends ChangeNotifier {
- var current = WordPair.random();
- }
-
- // ...
接下来,MyAppState 类定义应用的状态。这是您第一次使用 Flutter。因此,在此 Codelab 中,我们让该类保持简单和专注。在 Flutter 中,可以采用许多有效的方法来管理应用状态。其中最容易理解的一种方法就是 ChangeNotifier,也是此应用所采用的方法。
MyAppState 定义应用运行所需的数据。现在,其中仅包含一个变量,即通过随机函数生成当前的随机单词对。您稍后将在其中添加代码。ChangeNotifier,这意味着它可以向其他人通知自己的更改。例如,如果当前单词对发生变化,应用中的一些 widget 需要知晓此变化。ChangeNotifierProvider 创建状态并将其提供给整个应用(参见上面 MyApp 中的代码)。这样一来,应用中的任何 widget 都可以获取状态。- // ...
-
- class MyHomePage extends StatelessWidget {
- @override
- Widget build(BuildContext context) { // ← 1
- var appState = context.watch<MyAppState>(); // ← 2
-
- return Scaffold( // ← 3
- body: Column( // ← 4
- children: [
- Text('A random AWESOME idea:'), // ← 5
- Text(appState.current.asLowerCase), // ← 6
- ElevatedButton(
- onPressed: () {
- print('button pressed!');
- },
- child: Text('Next'),
- ),
- ], // ← 7
- ),
- );
- }
- }
-
- // ...
最后是 MyHomePage,这是您已经修改过的 widget。下面每个带编号的行均映射到上面代码中相应行编号的注释:
build() 方法,每当 widget 的环境发生变化时,系统都会自动调用该方法,以便 widget 始终保持最新状态。MyHomePage 使用 watch 方法跟踪对应用当前状态的更改。build 方法都必须返回一个 widget 或(更常见的)嵌套 widget 树。在本例中,顶层 widget 是 Scaffold。您不会在此 Codelab 中使用 Scaffold,但它是一个有用的 widget。在绝大多数真实的 Flutter 应用中都可以找到该 widget。Column 是 Flutter 中最基础的布局 widget 之一。它接受任意数量的子项并将这些子项从上到下放在一列中。默认情况下,该列会以可视化形式将其子项置于顶部。您很快就会对其进行更改,使该列居中。Text widget。Text widget 接受 appState,并访问该类的唯一成员 current(这是一个 WordPair)。WordPair 提供了一些有用的 getter,例如 asPascalCase 或 asSnakeCase。此处,我们使用了 asLowerCase。但如果您希望选择其他选项,您现在可以对其进行更改。children 是此特定 Column 参数列表的最后一个(也是唯一一个)成员。不过,在一般情况下,使用尾随逗号是一种不错的选择。尾随逗号可大幅减小添加更多成员的必要性,并且还可以在 Dart 的自动格式化程序中作为添加换行符的提示。如需了解详细信息,请参阅代码格式。接下来,您会将按钮关联至状态。
滚动至 MyAppState 并添加 getNext 方法。
- // ...
-
- class MyAppState extends ChangeNotifier {
- var current = WordPair.random();
-
- // ↓ Add this.
- void getNext() {
- current = WordPair.random();
- notifyListeners();
- }
- }
-
- // ...
新的 getNext() 方法为 current 重新分配了新的随机 WordPair。它还调用 notifyListeners() (ChangeNotifier) 的一个方法),以确保向任何通过 watch 方法跟踪 MyAppState 的对象发出通知。
其余要做的就是通过按钮的回调来调用 getNext 方法。
- // ...
-
- ElevatedButton(
- onPressed: () {
- appState.getNext(); // ← This instead of print().
- },
- child: Text('Next'),
- ),
-
- // ...
现在,保存并尝试运行应用。当您每次按下 Next 按钮时,该应用都会生成一个新的随机单词对。

在下一节中,您将改善用户界面的外观。
下图展示了应用的当前外观。
不太好。应用的核心功能(随机生成单词对)应更显眼。毕竟,这是应用为用户提供的主要功能!其他问题还包括,应用的内容不在中心位置,整个应用只有单调的黑色和白色。
本节将通过调整应用设计来解决这些问题。本节的最终目标是实现类似下图的效果:
现在,负责显示当前单词对的代码行大概是这样的:Text(appState.current.asLowerCase)。要改为更复杂的设计,一种行之有效的方式是将此代码行提取到单独的 widget 中。为 UI 的单独逻辑部分使用单独的 widget 是在 Flutter 中管理复杂性的一种重要方法。
Flutter 提供了一个用于提取 widget 的重构帮助程序,但在使用它之前,请确保所提取的代码行仅访问所需的内容。现在,该代码行将访问 appState,但实际上只需知道当前的单词对是什么。
综合考虑以下因素,重写 MyHomePage 的代码,如下所示:
- class MyHomePage extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- var appState = context.watch
(); - var pair = appState.current; // ← Add this.
-
- return Scaffold(
- body: Column(
- children: [
- Text('A random AWESOME idea:'),
- Text(pair.asLowerCase), // ← Change to this.
- ElevatedButton(
- onPressed: () {
- appState.getNext();
- },
- child: Text('Next'),
- ),
- ],
- ),
- );
- }
- }

很好!Text widget 不再引用整个 appState。
现在,您需要调出 Refactor 菜单。在 AS 中,您可以通过以下两种方式之一执行此操作:
Text),然后从下拉菜单中选择 Refactor..., 
在 Refactor 菜单中,选择 Extract Widget。指定一个名称,例如 1,然后点击 Enter 键。
这会在当前文件的末尾自动创建一个新的 BigCard 类。该类应如下所示:

请注意,即便在重构期间,应用也将保持正常运行。
接下来,我们要将这个新的 widget 转变为本节开始部分大胆设想的 UI。
在其中找到 BigCard 类和 build() 方法。
在AS中,光标移动至TEXT然后输入alt+enter

而是选择 Wrap with Padding。这会围绕 Text widget 创建一个新的父 widget,其名称为 Padding。保存后,您会看到随机单词已经有了更宽敞的空间。

下来,我们再进一步。将光标放在 Padding widget 上,调出 Refactor 菜单,然后选择 Wrap with widget...。

这允许您指定父 widget。键入“Card”,然后按下 Enter 键。


为了使卡片更加显眼,请用更丰富的颜色对其进行绘制。保持一致的配色方案始终是一个不想的想法。因此,使用应用的 Theme 来选择颜色。
对 BigCard 的 build() 方法进行以下更改。
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context); // ← Add this.
-
- return Card(
- color: theme.colorScheme.primary, // ← And also this.
- child: Padding(
- padding: const EdgeInsets.all(20),
- child: Text(pair.asLowerCase),
- ),
- );
- }


这两个新代码行完成了很多操作:
Theme.of(context) 请求应用的当前主题。colorScheme 属性相同。配色方案包含多种颜色,其中 primary 最为显眼,用于定义应用的颜色。卡片现在会呈现为应用的 primary 颜色:
您可以更改此颜色以及整个应用的配色方案,方法是向上滚动至 MyApp 并更改其中的 ColorScheme 种子颜色。
提示:Flutter 的 Colors 类可让您方便地访问精选颜色的调色板,例如 Colors.deepOrange 或 Colors.red。不过,您当然可以选择任何颜色。例如,要定义完全不透明的纯绿色,请使用 Color.fromRGBO(0, 255, 0, 1.0)。如果您喜欢使用十六进制数,也可以使用 Color(0xFF00FF00)。

请注意,颜色的动画效果很流畅。这称为隐式动画。许多 Flutter widget 会在值之间平滑地插值,这样 UI 就不仅仅是在状态之间“跳转”。
卡片下方的凸起按钮也会改变颜色。这正是应用级 Theme 相对于硬编码值的强大优势。
卡片还存在一个问题:文字太小,并且在该颜色下很难看清。如需解决此问题,请对 BigCard 的 build() 方法进行以下更改。
- @override
- Widget build(BuildContext context) {
- final theme = Theme.of(context);
- // ↓ Add this.
- final style = theme.textTheme.displayMedium!.copyWith(
- color: theme.colorScheme.onPrimary,
- );
-
- return Card(
- color: theme.colorScheme.primary,
- child: Padding(
- padding: const EdgeInsets.all(20),
- // ↓ Change this line.
- child: Text(pair.asLowerCase, style: style),
- ),
- );
- }
下面详述此项更改:
theme.textTheme,,您可以访问应用的字体主题。此类包括以下成员:bodyMedium(针对中等大小的标准文本)、caption(针对图片的说明)或 headlineLarge(针对大标题)。displayMedium 属性是专用于“展示文本”的大号样式。此处的“展示”一词用于反映版式效果,例如展示字体。displayMedium 的文档指出“展示样式保留用于简短、重要的文本”— 这正是我们的应用场景。displayMedium 属性可以是 null。Dart(您编写此应用所使用的编程语言)采用 null 安全机制,因此不会允许您调用值可能为 null 的对象的方法。不过,在这种情况下,您可以使用 ! 运算符(“bang 运算符”)向 Dart 保证您知道自己在做什么。(在本例中,displayMedium 肯定不是 null。不过,判断这一点的方法超出了此 Codelab 的讨论范围。)displayMedium 上的 copyWith() 会返回文本样式的副本,以及您定义的更改。在本例中,您只是更改文本的颜色。onPrimary 属性定义了一种非常适合在应用的 primary 颜色上使用的颜色。现在,该应用应如下所示:

现在,随机单词对已经呈现出美观的视觉效果,下一步是将其置于应用窗口/屏幕的中间位置。
首先,请记住 BigCard 是 Column 的一部分。默认情况下,各个列会将其子项集中到顶部,但我们可以轻松覆盖此设置。找到 MyHomePage 的 build() 方法,并进行以下更改:
- class MyHomePage extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- var appState = context.watch
(); - var pair = appState.current;
-
- return Scaffold(
- body: Column(
- mainAxisAlignment: MainAxisAlignment.center, // ← Add this.
- children: [
- Text('A random AWESOME idea:'),
- BigCard(pair: pair),
- ElevatedButton(
- onPressed: () {
- appState.getNext();
- },
- child: Text('Next'),
- ),
- ],
- ),
- );
- }
- }

子项已经沿列的横轴居中(换句话说,它们已水平居中)。但是,Column 本身并不在 Scaffold 的中心位置。我们可以使用 Widget Inspector 来验证这一点。
Widget Inspector 超出了此 Codelab 的讨论范围。但您可以看到,当突出显示时,Column 不会占据应用的整个宽度,而是仅占据其子项所需的水平空间。
您可以仅对列进行居中。将光标放在 Column 上,并输入alt+enter 随后选中Wrap with Center。
如果需要,您还可以再对其进行一些调整。
BigCard 上方的 Text widget。一些人认为,界面中不再需要描述性文本 ("A random AWESOME idea:"),因为即使没有该文本,界面也可以发挥应有的作用。而且这样显得更加干净。BigCard 和 ElevatedButton 之间添加一个 SizedBox(height: 10) widget。这样一来,两个 widget 之间就会有更大的空间。SizedBox widget 只是会占用空间,而不会呈现任何内容。它通常用于创建视觉“间隙”。进行一些可选更改后,MyHomePage 现在包含以下代码:
- class MyHomePage extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- var appState = context.watch
(); - var pair = appState.current;
-
- return Scaffold(
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- BigCard(pair: pair),
- SizedBox(height: 10),
- ElevatedButton(
- onPressed: () {
- appState.getNext();
- },
- child: Text('Next'),
- ),
- ],
- ),
- ),
- );
- }
- }
该应用会象是

在下一节中,您将添加收藏(或“喜欢”)生成的单词的功能。
应用现在运行良好,有时甚至会提供一些有趣的单词对。但是,当用户点击 Next 时,每个单词对都会永久消失。最好能通过一种方法来“记住”最佳建议,例如使用“Like”按钮。
滚动至 MyAppState 并添加以下代码:
- class MyAppState extends ChangeNotifier {
- var current = WordPair.random();
-
- void getNext() {
- current = WordPair.random();
- notifyListeners();
- }
-
- // ↓ Add the code below.
- var favorites =
[]; -
- void toggleFavorite() {
- if (favorites.contains(current)) {
- favorites.remove(current);
- } else {
- favorites.add(current);
- }
- notifyListeners();
- }
- }
下面分析各项更改:
MyAppState 中添加了一个名为 favorites 的新属性。此属性使用一个空的列表进行初始化,即 []。[] 。这有助于增强应用的可靠性 — 如果您尝试向应用添加 WordPair 以外的任何内容,Dart 甚至会拒绝运行应用。相应的,您可以使用 favorites 列表,同时知道其中永远不会隐藏任何不需要的对象(如 null)。注意:除了 List(用 [] 表示)以外,Dart 还提供了其他一些集合类型。您可能认为 Set(用 {} 表示)可以更有效地表示收藏夹集合。为了让此 Codelab 保持尽可能简单易懂,我们仍然坚持使用了列表。但如果需要,您可以改为使用 Set。代码不会有太大变化。
toggleFavorite(),它可以从收藏夹列表中删除当前单词对(如果已经存在),或者添加单词对(如果不存在)。在任何一种情况下,代码都会在之后调用 notifyListeners();。完成“业务逻辑”后,接下来继续充实用户界面。如需将“Like”按钮放在“Next”按钮的左侧,我们需要使用 Row。Row widget 是您之前看到的 Column 的水平等效项。
首先,将现有按钮封装在 Row 中。找到 MyHomePage 的 build() 方法,将光标放在 ElevatedButton 上,使用 Ctrl+. 或 Cmd+. 调出 Refactor 菜单,然后选择 Wrap with Row。
保存时,您会注意到 Row 在行为上类似于 Column — 默认情况下,它会将其子项集中在左侧。(Column 会将其子项集中到顶部。)要解决此问题,您可以使用与之前相同的方法,但这次要用到 mainAxisAlignment。不过,出于教学(学习)目的,请使用 mainAxisSize。这会告知 Row 不要占用所有可用的水平空间。
做出以下更改:
-
- class MyHomePage extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- var appState = context.watch
(); - var pair = appState.current;
-
- return Scaffold(
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- BigCard(pair: pair),
- SizedBox(height: 10),
- Row(
- mainAxisSize: MainAxisSize.min, // ← Add this.
- children: [
- ElevatedButton(
- onPressed: () {
- appState.getNext();
- },
- child: Text('Next'),
- ),
- ],
- ),
- ],
- ),
- ),
- );
- }
界面回到了之前的位置。

接下来,添加 Like 按钮并将其关联至 toggleFavorite()。为了考验大家的学习成果,请首先尝试自行完成此任务,而不要看下面的代码块。
接下来,在 MyHomePage 中添加第二个按钮。这次,使用 ElevatedButton.icon() 构造函数创建一个带有图标的按钮。在 build 方法顶部,根据当前单词对是否已在收藏夹中选择适当的图标。另外,请注意再次使用 SizedBox,以便让两个按钮稍微分开。

只不过,用户看不到收藏夹。因此,在下一节中,我们将在应用添加一个完整的独立屏幕!
大多数应用都无法将所有内容放置在一个屏幕中。此特定应用或许可以这样做,但为了实现更好的学习效果,您将为用户的收藏夹创建一个单独的屏幕。为了在两个屏幕之间进行切换,您将实现您的第一个 StatefulWidget。
为了尽快了解这一步的内容,请将 MyHomePage 拆分为 2 个单独的 widget。
全选 MyHomePage 并删除,然后替换为以下代码:
-
- class MyHomePage extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- body: Row(
- children: [
- SafeArea(
- child: NavigationRail(
- extended: false,
- destinations: [
- NavigationRailDestination(
- icon: Icon(Icons.home),
- label: Text('Home'),
- ),
- NavigationRailDestination(
- icon: Icon(Icons.favorite),
- label: Text('Favorites'),
- ),
- ],
- selectedIndex: 0,
- onDestinationSelected: (value) {
- print('selected: $value');
- },
- ),
- ),
- Expanded(
- child: Container(
- color: Theme.of(context).colorScheme.primaryContainer,
- child: GeneratorPage(),
- ),
- ),
- ],
- ),
- );
- }
- }
-
- class GeneratorPage extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- var appState = context.watch
(); - var pair = appState.current;
-
- IconData icon;
- if (appState.favorites.contains(pair)) {
- icon = Icons.favorite;
- } else {
- icon = Icons.favorite_border;
- }
-
- return Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- BigCard(pair: pair),
- SizedBox(height: 10),
- Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- ElevatedButton.icon(
- onPressed: () {
- appState.toggleFavorite();
- },
- icon: Icon(icon),
- label: Text('Like'),
- ),
- SizedBox(width: 10),
- ElevatedButton(
- onPressed: () {
- appState.getNext();
- },
- child: Text('Next'),
- ),
- ],
- ),
- ],
- ),
- );
- }
- }
保存后,您会看到界面的可视效果是正常的,但在功能上无法正常运行。点击侧边导航栏中的 ♥︎(心形符号)后,应用没有任何反应。

检查更改。
MyHomePage 的全部内容均被提取到新的 GeneratorPage widget。在旧版 MyHomePage widget 中,唯一未提取的部分是 Scaffold。MyHomePage 包含一个有两个子项的 Row。第一个是 SafeArea widget,第二个是 Expanded widget。SafeArea 将确保其子项不会被硬件凹口或状态栏遮挡。在此应用中,widget 会将 NavigationRail 封装,以防止导航按钮被遮挡,例如被移动状态栏遮挡。extended: false 行更改为 true。这将显示图标旁边的标签。在接下来的某个步骤中,你将学习如何在应用有足够的水平空间时自动完成此操作。selectedIndex。若选定索引 (selectedIndex) 为零,则会选择第一个目标页面;若选定索引为一,则会选择第二个目标页面,依此类推。目前,它被硬编码为零。onDestinationSelected 的目标页面时会发生什么。现在,应用仅通过 print() 输出所请求的索引值。Row 的第二个子项是 Expanded widget。展开的 widget 在行和列中极具实用性 — 它们可用于呈现以下布局:一些子项仅占用其所需要的空间(在本例中为 NavigationRail),而其他 widget 则尽可能多地占用其余空间(在本例中为 Expanded)。可以将 Expanded widget 视为一种“贪婪的”元素。如果您想要更好地感受此 widget 的作用,请尝试用另一个 Expanded 封装 NavigationRail widget。Expanded widget 会分割两者之间所有可用的水平空间,即使侧边导航栏只需要左侧的一小部分。Expanded widget 内部,有一个指定了颜色的 Container;而在该容器内部,有一个 GeneratorPage。截至目前,MyAppState 涵盖了您的所有状态需求。正是因此,您目前为止编写的所有 widget 都是无状态的。它们不包含任何自己的可变状态。所有 widget 都无法自行更改,而是必须经过 MyAppState。
我们将改变这一状况。
您需要采用某种方法来保存侧边导航栏的 selectedIndex 的值。您还希望能够从 onDestinationSelected 回调中更改此值。
您可以添加 selectedIndex 作为 MyAppState 的另一个属性。它也会发挥作用。但不难想象,如果每个 widget 都将其值存储在其中,应用状态将快速增长到合理范围以外。

某些状态仅与单个 widget 相关,因此应当与该 widget 保持一致。
输入 StatefulWidget,这是一种具有 State 的 widget。首先,将 MyHomePage 转换为有状态 widget。
将光标放在 MyHomePage 的第一行(以 class MyHomePage... 开头的行),然后使alt+enter。接下来,选择 Convert to StatefulWidget。

IDE 为您创建了一个新类 _MyHomePageState。此类扩展 State,因此可以管理其自己的值。(它可以自行改变。)另请注意,旧版无状态 widget 中的 build 方法已移至 _MyHomePageState(而不是保留在 widget 中)。build 方法会一字不差的完成移动,其内部不会发生任何改变。该方法现在只是换了个位置。

_MyHomePageState 开始部分的下划线 (_) 将该类设置为私有类,并由编译器强制执行。如果想要详细了解 Dart 中私有属性以及其他主题,请参阅语言导览。
新的有状态 widget 只需要跟踪一个变量,即 selectedIndex。对 _MyHomePageState 进行以下 3 处更改:
- class _MyHomePageState extends State<MyHomePage> {
-
- var selectedIndex = 0; // ← Add this property.
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- body: Row(
- children: [
- SafeArea(
- child: NavigationRail(
- extended: false,
- destinations: [
- NavigationRailDestination(
- icon: Icon(Icons.home),
- label: Text('Home'),
- ),
- NavigationRailDestination(
- icon: Icon(Icons.favorite),
- label: Text('Favorites'),
- ),
- ],
- selectedIndex: selectedIndex, // ← Change to this.
- onDestinationSelected: (value) {
-
- // ↓ Replace print with this.
- setState(() {
- selectedIndex = value;
- });
-
- },
- ),
- ),
- Expanded(
- child: Container(
- color: Theme.of(context).colorScheme.primaryContainer,
- child: GeneratorPage(),
- ),
- ),
- ],
- ),
- );
- }
- }
下面分析各项更改:
selectedIndex,并将其初始化为 0。NavigationRail 定义中使用此新变量,而不再是像之前那样将其硬编码为 0。onDestinationSelected 回调时,并不是仅仅将新值输出到控制台,而是将其分配到 setState() 调用内部的 selectedIndex。此调用类似于之前使用的 notifyListeners() 方法 — 它会确保界面始终更新为最新状态。
selectedIndex 来确定显示哪一个屏幕。将以下代码放在 _MyHomePageState 的 build 方法的顶部,即 return Scaffold 之前:
-
- Widget page;
- switch (selectedIndex) {
- case 0:
- page = GeneratorPage();
- break;
- case 1:
- page = Placeholder();
- break;
- default:
- throw UnimplementedError('no widget for $selectedIndex');
- }
详细分析这段代码:
Widget 的新变量 page。selectedIndex 中的当前值,switch 语句为 page 分配一个屏幕。FavoritesPage,因此先使用 Placeholder;这是一个便捷易用的 widget,可以在其放置地方绘制一个交叉矩形,以便将界面的该部分标记为未完成。 
selectedIndex 既不是 0 也不是 1 的情况下抛出错误。这有助于防止后续 bug。如果您向侧边导航栏添加了一个新的目标页面而忘记更新此代码,则程序会在开发过程中崩溃(而不是让您猜测程序为何无法正常运行,或者让您将有缺陷的代码发布到生产环境中)。page 现已包含您想要在右侧显示的 widget,您大概可以猜到还需要哪些其他更改。
完成最后一项更改的 _MyHomePageState 如下所示:
-
- class _MyHomePageState extends State<MyHomePage> {
- var selectedIndex = 0;
-
- @override
- Widget build(BuildContext context) {
- Widget page;
- switch (selectedIndex) {
- case 0:
- page = GeneratorPage();
- break;
- case 1:
- page = Placeholder();
- break;
- default:
- throw UnimplementedError('no widget for $selectedIndex');
- }
-
- return Scaffold(
- body: Row(
- children: [
- SafeArea(
- child: NavigationRail(
- extended: false,
- destinations: [
- NavigationRailDestination(
- icon: Icon(Icons.home),
- label: Text('Home'),
- ),
- NavigationRailDestination(
- icon: Icon(Icons.favorite),
- label: Text('Favorites'),
- ),
- ],
- selectedIndex: selectedIndex,
- onDestinationSelected: (value) {
- setState(() {
- selectedIndex = value;
- });
- },
- ),
- ),
- Expanded(
- child: Container(
- color: Theme.of(context).colorScheme.primaryContainer,
- child: page, // ← Here.
- ),
- ),
- ],
- ),
- );
- }
- }
现在,该应用将在 GeneratorPage 与即将成为 Favorites 页面的占位符之间切换。

接下来,为侧边导航栏赋予自适用性。具体来说,让侧边导航栏在有足够空间的情况下自动显示标签(使用 extended: true)。

Flutter 提供了多个 widget,可帮助您为应用赋予自适用性。例如,Wrap 是一个类似于 Row 或 Column 的 widget,当没有足够的垂直或水平空间时,它会自动将子项封装到下一“行”(称为“运行”)中。FittedBox widget 可以自动根据您的规格将其子项放置到可用空间中。
不过,当有足够的空间时,NavigationRail 并不会自动显示标签,因为它无法判断在每个上下文中,什么才算是足够的空间。调用工作应当由您(开发者)来完成。
假设您决定仅当 MyHomePage 的宽度至少为 600 像素时才显示标签。
注意:Flutter 使用逻辑像素作为长度单位。逻辑像素有时也称为与设备无关的像素。无论应用是在分辨率较低的旧款手机上运行,还是在新款“视网膜”设备上运行,8 像素的内边距在视觉上都是一样的。物理显示器每厘米大约有 38 个逻辑像素,相当于每英寸大约有 96 个逻辑像
在本例中,我们将使用的 widget 是 LayoutBuilder。它允许根据可用空间大小来更改 widget 树。
再次在 VS Code 中使用 Flutter 的 Refactor 菜单进行所需的更改。不过,这一次有点复杂:
_MyHomePageState 的 build 方法内部,将光标放在 Scaffold 上。Ctrl+. 键 (Windows/Linux) 或 Cmd+. 键 (Mac) 调出 Refactor 菜单。Builder 的名称修改为 LayoutBuilder。(context) 修改为 (context, constraints)。每当约束发生更改时,系统都会调用 LayoutBuilder 的 builder 回调。比如说,以下场景就会触发这种情况:
MyHomePage 旁边的一些 widget 变大,使 MyHomePage 的约束变小现在,您的代码可以通过查询当前的 constraints 来决定是否显示标签。对 _MyHomePageState 的 build 方法进行以下单行更改:
- class _MyHomePageState extends State<MyHomePage> {
- var selectedIndex = 0;
-
- @override
- Widget build(BuildContext context) {
- Widget page;
- switch (selectedIndex) {
- case 0:
- page = GeneratorPage();
- break;
- case 1:
- page = Placeholder();
- break;
- default:
- throw UnimplementedError('no widget for $selectedIndex');
- }
-
- return LayoutBuilder(builder: (context, constraints) {
- return Scaffold(
- body: Row(
- children: [
- SafeArea(
- child: NavigationRail(
- extended: constraints.maxWidth >= 600, // ← Here.
- destinations: [
- NavigationRailDestination(
- icon: Icon(Icons.home),
- label: Text('Home'),
- ),
- NavigationRailDestination(
- icon: Icon(Icons.favorite),
- label: Text('Favorites'),
- ),
- ],
- selectedIndex: selectedIndex,
- onDestinationSelected: (value) {
- setState(() {
- selectedIndex = value;
- });
- },
- ),
- ),
- Expanded(
- child: Container(
- color: Theme.of(context).colorScheme.primaryContainer,
- child: page,
- ),
- ),
- ],
- ),
- );
- });
- }
- }
现在,您的应用可以响应其环境,例如屏幕尺寸、方向和平台!换句话说,该应用现已具备自适用性!
接下来还有最后一项工作,那就是将 Placeholder 替换为真实的 Favorites 屏幕。下一节将介绍此项操作
还记得我们用来暂时替代 Favorites 页面的 Placeholder widget 吗?
是时候将其替换为真实页面了。
如果您敢于挑战,请尝试自行完成此步骤。您的目标是在新的 FavoritesPage 这一无状态 widget 中显示 favorites 列表,然后显示该 widget,而不是 Placeholder。
下面提供了一些指引:
Column 时,请使用 ListView widget。context.watch() 从任何 widget 访问 MyAppState 实例。ListTile 的 title(通常用于文本)、leading(用于图标或头像)和 onTap(用于交互)等属性。不过,您也可以使用已经掌握的 widget 来实现类似的效果。for 循环。例如,如果 messages 包含一个字符串列表,您可以使用如下代码:另一方面,如果您更熟悉函数式编程,Dart 还支持编写 messages.map((m) => Text(m)).toList() 这样的代码。当然,您始终可以创建一个 widget 列表,并将其强制添加到 build 中。
自行添加 Favorites 页面的好处是,您可以自己做决策,并从中学到更多知识。但其缺点是,您可能会遇到自己无法解决的问题。请记住:不要害怕失败,它是通往成功的必经之路。没有人要求您在一个小时内就掌握 Flutter 开发,这也不现实。
下面提供的只是实现 Favorites 页面的一种方法。其实现方法将(有希望)激发您完善代码、改进界面并为己所用。
新的 FavoritesPage 类如下所示:
- class FavoritesPage extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- var appState = context.watch
(); -
- if (appState.favorites.isEmpty) {
- return Center(
- child: Text('No favorites yet.'),
- );
- }
-
- return ListView(
- children: [
- Padding(
- padding: const EdgeInsets.all(20),
- child: Text('You have '
- '${appState.favorites.length} favorites:'),
- ),
- for (var pair in appState.favorites)
- ListTile(
- leading: Icon(Icons.favorite),
- title: Text(pair.asLowerCase),
- ),
- ],
- );
- }
- }