本笔记基于该教程, 让我们说谢谢 Google 写着感觉和 ArkTS 和 Kotlin 很像 PS:如果你使用 Windows,请将所有 ⌘ (Mac 上的 Command 键)视为 Ctrl 键
Flutter环境配置
- 按照 flutter
brew install flutter
- 安装 VSCode(其他的也行)插件

- 安装 xcode
xcode-select --install
创建一个新项目
- F1 -> flutter new -> Application->填项目名称,选路径
- 替换以下内容
pubspec.yaml
name: 你的项目名称
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
- 替换 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
- 替换
lib/main.dart,经典的面向对象语言
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<MyAppState>();
return Scaffold(
body: Column(
children: [
Text('A random idea:'),
Text(appState.current.asLowerCase),
],
),
);
}
}
运行这个新项目
- 停留在
main.dart上,选择当前的目标运行平台(右下角)
- 我这里选了 Mac,然后点击运行标志,结果报错了在 stackoverflow 找到
xcrun: error: unable to find utility "xcodebuild", not a developer tool or in PATH。如果装了 Xcode APP 版本,进入 Settings->Locations,改一下 command line tools 的路径
- 再运行,还是报错
运行 flutter doctor -v,发现没装cocoapods。运行brew install cocoapods即可 - 再运行,还是一样的报错。在这里找到原因,有人就是因为被 iCloud 文件提供器加了 com.apple.fileprovider.fpfs#P 才一直失败,- 任何打包进 .app 的文件只要带有资源分叉或 Finder 信息,CodeSign 都会拒绝。所以我把项目移出了 iCloud 同步的文件夹,然后清理资源重新运行
# 1) Flutter 清理 + 移除构建输出
flutter clean
rm -rf build/macos
# 2) 删除 Xcode 派生数据
rm -rf ~/Library/Developer/Xcode/DerivedData/*
# 3) 递归清理整个项目树的扩展属性(包含将来要进 .app 的一切)
xattr -rc .
# 4) 若你从网上下过三方二进制/压缩包,顺便去掉隔离标记
xattr -dr com.apple.quarantine .
# 5) 重新预拉取 macOS 引擎构件(避免 SDK 自身产物带脏属性)
flutter precache --macos
# 6) 重新构建
flutter run -d macos
- 这次成功了,弹出了我们的 APP
再试试 iOS 平台,右下角构建平台选择 ios simulator
再打开 lib/main.dart,点击右上角运行标志,等待一会也可以正确显示
热重载
- 我们修改文字,并按 ⌘+S,可看到程序中的文字发生了变化(注意 Web 平台不支持)

添加一个按钮
我们继续修改,修改以下内容
// ...
return Scaffold(
body: Column(
children: [
Text('Hello World'),
Text(appState.current.asLowerCase),
// ↓ Add this.
ElevatedButton(
onPressed: () {
print('button pressed!');
},
child: Text('Next'),
),
],
),
);
// ...
保存更改,可以看到出现了一个按钮,可以点击
学习 lib/main.dart
1. 程序入口
// ...
void main() {
runApp(MyApp());
}
// ...
入口函数 main() 运行一个 App,这个 App 是 MyApp()
2. 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 类型的(一种 Widget)。构建 Flutter 时,widget 是基本要素。这里定义了 MyApp 这个 widget 所有东西,有状态,名字,主题,主页
3. 状态管理
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
// ...
- MyAppState 是我们这个应用的状态,一种管理状态的方法是
ChangeNotifier。 现在我们的状态只有一个变量,就是current,它是随机一个英文单词对 ChangeNotifier,一种状态类,意思是状态变化时通知 widget- 怎么通知呢?使用
ChangeNotifierProvider,提供给整个 App。这样所有的该 App 中的 Widget 都可以订阅这个状态
4. 组件 MyHomePage
也是我们这个 App 的主页
// ...
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
),
);
}
}
// ...
- 如同 MyApp(因为即使是完整的 App 也是一个组件,组件里面套组件),每个 widget 都要有一个
build()。当 widget的环境变化时,该方法自动调用,然后使 widget 的内容产生变化。有点像 Vue 开发中的生命周期OnUpdated() - 在 build 中,通过
context.watch()监控状态 build()最终返回的是一个 Widget(更准确的说,嵌套的 Widget 树)。而顶层的 Widget 有一点区别,我们为其赋予一个具体的实现Scaffold(也是一种 Widget)Column是一种 Widget 的布局,将 children 中的内容从上到下放在一列Text()也是一种 widget- 这个 Text 接受了状态中存储的内容。我们打开
current的定义WordPair.random(),可以知道random()实际上返回了一个 WordPair类型。Socurrentis also aWordPair.WordPairhas keyasLowerCase. It is the String of two string, then use functiontoLowerCase(), which comes fromString
late final String asString = '$first$second';
late final String asLowerCase = asString.toLowerCase();
- Here we use a lot trailing commas, it’s a good idea, because most time it’s not the last widget
添加行为
添加关于数据的操作,一定是在状态中添加
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
// ↓ Add this.
void getNext() {
current = WordPair.random();
notifyListeners();
}
}
// ...
有了对于数据的更新,我们只需要通过按钮回调即可
// ...
ElevatedButton(
onPressed: () {
appState.getNext(); // ← This instead of print().
},
child: Text('Next'),
),
// ...
修改外观
首先,我们先修改一下主页对状态的读取,让职责更分离一些
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
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 的外观更符合我们的要求,我们其实可以直接在 Text 中传入各种参数。但是这样可维护性很差。通常的做法是,把这个 Text() 提取为单独的 Widget,然后添加各种 UI 逻辑
- 右击需要重构的代码段(本例为 Text),或者光标放置在上面然后 ⌘+. 然后选择 Extract Widget, 输入
BigCard
- 可以看到新建了一个 BigCard 类。接下来,我们为其中的 Text 添加一个 Padding。放在 Text 上,同样的操作,但点击 Wrap with Padding
- 我们可以修改 Padding 的值,为该 BigCard 增加一些内边距
- 可以看到 Padding 其实也是一个 widget,并不是常规的"属性"或 CSS 值
主题和样式
- 我们把 Padding 再包裹一下,用 Card。
Card是一种预设样式,这样会出现一个卡片样式 - 修改一下这个 Card 的样式,添加 theme,并使用 theme 的颜色。这样可以保持一致的配色方案
// ...
@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 请求当前的主题,我们调用了其中
colorScheme的主题色primary
配色方案
如果想修改配色方案,我们来到总 App 中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(),
),
);
}
}
Colors类提供了很多预设颜色,如果我们希望使用自己的 RGB 或 16 进制,使用``Color.fromRGBO(0, 255, 0, 1.0) 或 Color(0xFF00FF00)`
文本主题
修改文本的颜色和大小,我们来修改 Text
// ...
@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),
),
);
}
// ...
我们定义了一个新的 style,它是文字主题textTheme。这允许我们访问该文字主题中包含的各种的文字预设主题,比如bodyMedium(用于中等大小的标准文本)、caption(针对图片的说明)或headlineLarge(用于大标题)
这里面我们用的是displayMedium,在 Flutter 中用于一种《版式效果》,比如我们展示一个最简单的关键词,就会用到这个
但是我们需要对其进行自己的修改,这时候就要调用它的copyWith(),意在返回一个该样式的副本,但使用了我们自己的修改。此处我们修改了它的颜色,使用了onPrimary,字面意思就是"适合在主题色上展示的颜色"
因此,现在文字会显得很突出
此外还有一个!值得注意。Dart 是一门 null 安全的语言,不允许调用空对象。但是使用!可以绕过这种检查。可能你会觉得多次依据,因为此处displayMedium一定有值。但假设:
final minimal = ThemeData(
// 不使用默认 Typography,自己给一个非常精简的 textTheme
textTheme: const TextTheme( // 只给了少数几个
bodyMedium: TextStyle(fontSize: 14),
),
);
可以看到minimal主题的textTheme 中只有一个bodyMedium,而不是我们现在用的常见Material3主题:
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.lightBlue),
),
home: MyHomePage(),
),
);
}
}
进一步自定义文本主题
- 将鼠标放在
copyWith()的括号内任意处,按下 ⌘+⇧+Space,可以看到可以更改的属性列表 - 对于
Card()同样适用,按下后可以看到一个elevation,修改它的值以扩大Card的阴影
添加喜欢功能
首先,如果我们希望存储我们喜欢的单词,就要在状态中实现它
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
void getNext() {
current = WordPair.random();
notifyListeners();
}
// ↓ Add the code below.
var favorites = <WordPair>[];
void toggleFavorite() {
if (favorites.contains(current)) {
favorites.remove(current);
} else {
favorites.add(current);
}
notifyListeners();
}
}
// ...
注意所有的状态变化函数,结尾都要调用notifyListeners(),这样可以使那些 context.watch<MyAppState>()来获取这个函数对于 state 的更改
然后来到我们的主界面,添加一个新按钮。这需要添加一个Row,如果你了解前端开发,这会很容易理解
我们在 Column 中添加mainAxisAlignment,意在使其子项不是集中在顶部,而是纵向的中间(毕竟 column 是列,列是纵向的)。选中我们的 Button,然后 ⌘+.,选择 Wrap with Row
然后,在肉中添加 mainAxisSize,但这次我们为了学习,先使用 MainAxisSize
// ...
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Hello Word'),
BigCard(pair: pair),
Row(
mainAxisSize: MainAxisSize.min, //<-- add this one
children: [
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
// ...
这样做后并应用更改,你会发现一切都居中了。如果你好奇把 min 改为 max,会发现按钮们跑到最左侧
然后我们需要将 state 中的函数应用起来,真正的将按钮关联到数据上。首先,在 Row 的子项添加一个按钮,并在onPressed属性中使用toggleFavorite()
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Hello Word'),
BigCard(pair: pair),
Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: () {
appState.toggleFavorite();
},
child: Text('Like'),
),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
保存后你应当能看到这个按钮了。然后再添加一个好看的图标。
- 这个图标能够根据当前 WordPair 是否在
favorites[]中来更改外观。 - 此外,可以看到 ElevatedButton 支持一种 icon 方法,可以传入
icon以显示 Sizebox()用于添加间隔。在 BigCard 和按钮所在行之间增加高度的间隔,在按钮和按钮之间增加宽度的间隔
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
// ↓ Add this.
IconData icon;
if (appState.favorites.contains(pair)) {
icon = Icons.favorite;
} else {
icon = Icons.favorite_border;
}
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
// ↓ And this.
ElevatedButton.icon(
onPressed: () {
appState.toggleFavorite();
},
icon: Icon(icon),
label: Text('Like'),
),
SizedBox(width: 10), //<-- and this.
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
}
}
// ...
添加侧边导航栏
收藏了我们的 WordPair,但是再也找不回来了。这时候,我们需要再开一个页面,并提供一个路由(也就是导航栏)
移除我们的MyHomePage,我们将将其拆分为两个 Widget,替换为以下代码
// ...
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<MyAppState>();
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'),
),
],
),
],
),
);
}
}
// ...
保存后,原有的功能正常,但侧边栏没有反应。与以前的代码比较,你可以发现原有的HomePage 具有的组件都在 GeneratorPage中。在现在的MyHomePage中,一行有两个内容,第一个是 SafeArea,第二个是 Expanded,他们依然都是 Widget
SafeArea及其导航
SafeArea确保其子项不会被硬件凹口或者状态栏遮挡(比如 iPhone 的刘海)。用它来封装NavigationRail,能够防止导航按钮被遮挡- 如果将
NavigationRail中的extended改为 true,你就能看到标签。我们后续学习允许应用在空间充足时自动调整这一点 onDestinationSelected有点类似onPressed,在选择目标页面时调用其中的操作
Expanded及其容器
Row的第二个子项Expanded,用于这种布局:一些子项仅占用所需空间(NavigationRail),而其他子项尽可能多的占用剩余空间(Expanded)。如果你把NavigationRail也用Expanded包裹起来,而不是SafeArea,它看上去是这样:
- 因此
Expanded是一个贪婪的组件。在其内部,我们有一个Container,为其指定了颜色和包裹的页面
无状态 widget 和有状态 widget
目前为止,我们写的都是 StatelessWidget,他们没有自己的状态,必须使用MyAppState。这样有局限性,虽然我们可以把所有页面的所有值都存在一个state中,但很快这个可维护性就很差了。如果你写过 React 或 Vue,将很快理解我在说什么,这是一种组件化的设计思想
比如当前我们希望不同页面都具有自己的 state,比如 selectedIndex,页面的索引值以实现导航,就需要用到StatefulWidget
将光标放在 MyHomePage 的第一行(以 class MyHomePage... 开头的行),然后使用 或 ⌘ +. 调出 Refactor 菜单。接下来,选择 Convert to StatefulWidget
IDE 为您创建了一个新类 _MyHomePageState。此类基于 State<MyHomePage>实现,是一个独立的类,有自己的值。另请注意,旧版无状态 widget 中的 build 方法已移至 _MyHomePageState(而不是保留在 widget 中)。build 方法会一字不差的完成移动,其内部不会发生任何改变。该方法现在只是换了个位置。
-MyHomePageState 开始的下划线将该类设置为私有类,也就是不能被其他类之外的部分引用
为 Widget 设置 state
当前,我们的 Widget 只需要管理一个值,就是selectedIndex,也就是当前选中的页面的索引。这样做的原因是现在的HomePage实际上并不是一个 Page,它是对于所有页面的一个集合,并通过导航实现页面间的切换。因此,使selectedIndex成为它管理范围的一个值再好不过了
// ...
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(),
),
),
],
),
);
}
}
// ...
现在点击切换,我们就会保存选中的值了。保存查看点击的效果
你可能会好奇为什么组件知道我们的 0,1 代表着页面的索引。实际上这是 Flutter 的 API 规定好的。对于
NavigationRail这个组件,onDestinationSelected就是接收一个 int 参数,并用于 destinations 切换
使用 state 中的 index
建立 selectedIndex 和 不同 page 的关联
我们首先要建立 selectedIndex 和 不同 page 的关联。在_MyHomePageState 的 build 方法的顶部:
// ...
@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');
}
// ...
- 这段代码声明了一个类型为
Widget的新变量page。 - 然后,根据
selectedIndex中的当前值,switch 语句为page分配一个屏幕。 - 目前还没有
FavoritesPage,因此先使用Placeholder;这是一个便捷易用的 widget,可以在其放置地方绘制一个交叉矩形,以便将界面的该部分标记为未完成。

- 通过应用快速失败原则,switch 语句还将确保在
selectedIndex既不是 0 也不是 1 的情况下抛出错误。这有助于防止后续 bug。如果您向侧边导航栏添加了一个新的目标页面而忘记更新此代码,则程序会在开发过程中崩溃(而不是让您猜测程序为何无法正常运行,或者让您将有缺陷的代码发布到生产环境中)
使右侧容器显示页面
我们在前文提到了Expanded组件,在我们的 app 中,它其中的 Container 是右侧的主区域。因此我们为Container增加子组件 page
// ...
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page, // ← Here.
),
),
// ...
现在应用更改,你就能看到变化
为侧边导航自适应
这也是我们前文提到的功能。NavigationRail具有extended值,它会让侧边栏显示标签,但我们希望空间较小时,它能自动隐藏
Flutter 提供了很多具有这种能力的 widget,例如:
Wrap类似于 Row 或 Column,垂直或水平空间不足时,它将自动将项目放至下一列/行Fittedbox可根据规格将子项放至可用空间(不限制列/行)LayoutBuilder可以根据可用空间大小来修改 widget 树
不过值得注意的是,程序并不知道可用空间的大小。我们需要自己定义足够的空间的像素
假设我规定仅当
MyHomePage的宽度至少为 600 像素时才显示标签(flutter 中的像素是逻辑像素,即视觉上的像素,这与不同的屏幕种类无关。通常每厘米有 38 个逻辑像素,即使你用 oled 有很多物理像素,它就是视觉上的 38 个)
我们使用 LayoutBuilder,使用 ⌘+. 进行包裹
- 在
_MyHomePageState的build方法内部,将光标放在Scaffold上。 - 使用
Ctrl+.键 (Windows/Linux) 或Cmd+.键 (Mac) 调出 Refactor 菜单。 - 选择 Wrap with Builder 并按下 Enter 键。
- 将新添加的
Builder的名称修改为LayoutBuilder。 - 将回调参数列表从
(context)修改为(context, constraints)。
每当约束发生更改时,系统都会调用 LayoutBuilder的builder回调。比如说,以下场景就会触发这种情况:
- 用户调整应用窗口的大小
- 用户将手机从人像模式旋转到横屏模式,或从横屏模式旋转到人像模式
MyHomePage旁边的一些 widget 变大,使MyHomePage的约束变小- 其他还有很多,不再一一列举
我们可以通过监控当前的 constraints 来决定是否显示标签。对 _MyHomePageState 的 build 方法进行以下单行更改:
// ...
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'),
),
],
// ...
再次保存,查看一下结果
添加一个新页面
我们复习一下我们学过的内容即可,我推荐你自己动手做一个。以下是我的代码
记得替换index对应的PlaceHolder()
class FavoritesPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var favorites = appState.favorites;
return ListView(
children: [
for (WordPair fav in favorites) ...[
ListCard(fav: fav),
SizedBox(height: 5),
],
],
);
}
}
class ListCard extends StatelessWidget {
const ListCard({
super.key,
required this.fav,
});
final WordPair fav;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Icon(Icons.favorite, color: theme.colorScheme.onPrimary),
SizedBox(width: 10),
Text(fav.asLowerCase, style: style),
],
),
),
);
}
}
