Fultter学习日志(2)-构建第一个flutter应用

依照上一篇中我们新建的flutter应用

让我们更改pubspec.yaml中的内容为

name: namer_app
description: A new Flutter project.publish_to: 'none' # Remove this line if you wish to publish to pub.devversion: 0.0.1+1environment:sdk: '>=2.19.4 <4.0.0'dependencies:flutter:sdk: flutterenglish_words: ^4.0.0provider: ^6.0.0dev_dependencies:flutter_test:sdk: flutterflutter_lints: ^2.0.0flutter:uses-material-design: true

pubspec.yaml 文件指定与您的应用相关的基本信息,例如其当前版本、依赖项以及其随附的资源。

注意:如果您为应用指定的名称不是 namer_app,则需要对第一行进行相应的更改

接下来,在项目中打开另一个配置文件 analysis_options.yaml

将其内容替换为以下内容:

analysis_options.yaml

include: package:flutter_lints/flutter.yamllinter:rules:prefer_const_constructors: falseprefer_final_fields: falseuse_key_in_widget_constructors: falseprefer_const_literals_to_create_immutables: falseprefer_const_constructors_in_immutables: falseavoid_print: false

此文件决定了 Flutter 在分析代码时的严格程度。由于这是您第一次使用 Flutter,您可以让分析器不用太严格。此后,您可以随时进行调整。事实上,在邻近发布实际正式版应用的阶段,您几乎肯定会希望分析器更加严格。

最后,打开 lib/ 目录下的 main.dart 文件。

将此文件的内容替换为以下内容。

注意:

重新下载dependeces

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});@overrideWidget 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 {@overrideWidget build(BuildContext context) {var appState = context.watch<MyAppState>();return Scaffold(body: Column(children: [Text('A random idea:'),Text(appState.current.asLowerCase),],),);}
}

到目前为止,这 50 行代码是应用的全部。

添加按钮

第一次热重载

在 lib/main.dart 的底部,向第一个 Text 对象中的字符串添加一些内容,然后保存文件(使用 Ctrl+S 或 Cmd+S)。例如:

lib/main.dart

// ...return Scaffold(body: Column(children: [Text('A random AWESOME idea:'),  // ← Example change.Text(appState.current.asLowerCase),],),);// ...

请注意应用会立即发生更改,但随机单词保持不变。这正是 Flutter 广为人知的有状态热重载功能在发挥作用。当您将更改保存到源文件时,系统会触发热重载。

添加按钮

接下来,在 Column 底部添加一个按钮,也就是第二个 Text 实例的正下方。

lib/main.dart

// ...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! 消息。


 

5 分钟 Flutter 速成课程

尽管显示调试控制台很有趣,但您希望按钮执行更有意义的操作。不过,在开始之前,请仔细查看 lib/main.dart 中的代码,了解其工作原理。

lib/main.dart

// ...void main() {runApp(MyApp());
}// ...

在文件的最顶部,您可以找到 main() 函数。目前,该函数只是告知 Flutter 运行 MyApp 中定义的应用。

lib/main.dart

// ...class MyApp extends StatelessWidget {const MyApp({super.key});@overrideWidget 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,即应用的起点。

lib/main.dart

// ...class MyAppState extends ChangeNotifier {var current = WordPair.random();
}// ...

接下来,MyAppState 类定义应用的状态。这是您第一次使用 Flutter。因此,在此 Codelab 中,我们让该类保持简单和专注。在 Flutter 中,可以采用许多有效的方法来管理应用状态。其中最容易理解的一种方法就是 ChangeNotifier,也是此应用所采用的方法。

  • MyAppState 定义应用运行所需的数据。现在,其中仅包含一个变量,即通过随机函数生成当前的随机单词对。您稍后将在其中添加代码。
  • 状态类扩展 ChangeNotifier,这意味着它可以向其他人通知自己的更改。例如,如果当前单词对发生变化,应用中的一些 widget 需要知晓此变化。
  • 使用 ChangeNotifierProvider 创建状态并将其提供给整个应用(参见上面 MyApp 中的代码)。这样一来,应用中的任何 widget 都可以获取状态。

lib/main.dart

// ...class MyHomePage extends StatelessWidget {@overrideWidget build(BuildContext context) {           // ← 1var appState = context.watch<MyAppState>();  // ← 2return Scaffold(                             // ← 3body: Column(                              // ← 4children: [Text('A random AWESOME idea:'),        // ← 5Text(appState.current.asLowerCase),    // ← 6ElevatedButton(onPressed: () {print('button pressed!');},child: Text('Next'),),],                                       // ← 7),);}
}// ...

最后是 MyHomePage,这是您已经修改过的 widget。下面每个带编号的行均映射到上面代码中相应行编号的注释:

  1. 每个 widget 均定义了一个 build() 方法,每当 widget 的环境发生变化时,系统都会自动调用该方法,以便 widget 始终保持最新状态。
  2. MyHomePage 使用 watch 方法跟踪对应用当前状态的更改。
  3. 每个 build 方法都必须返回一个 widget 或(更常见的)嵌套 widget 树。在本例中,顶层 widget 是 Scaffold。您不会在此 Codelab 中使用 Scaffold,但它是一个有用的 widget。在绝大多数真实的 Flutter 应用中都可以找到该 widget。
  4. Column 是 Flutter 中最基础的布局 widget 之一。它接受任意数量的子项并将这些子项从上到下放在一列中。默认情况下,该列会以可视化形式将其子项置于顶部。您很快就会对其进行更改,使该列居中。
  5. 您在第一步中更改了此 Text widget。
  6. 第二个 Text widget 接受 appState,并访问该类的唯一成员 current(这是一个 WordPair)。WordPair 提供了一些有用的 getter,例如 asPascalCase 或 asSnakeCase。此处,我们使用了 asLowerCase。但如果您希望选择其他选项,您现在可以对其进行更改。
  7. 请注意,Flutter 代码大量使用了尾随逗号。此处并不需要这种特殊的逗号,因为 children 是此特定 Column 参数列表的最后一个(也是唯一一个)成员。不过,在一般情况下,使用尾随逗号是一种不错的选择。尾随逗号可大幅减小添加更多成员的必要性,并且还可以在 Dart 的自动格式化程序中作为添加换行符的提示。如需了解详细信息,请参阅代码格式。

接下来,您会将按钮关联至状态。

您的第一个行为

滚动至 MyAppState 并添加 getNext 方法。

lib/main.dart

// ...class MyAppState extends ChangeNotifier {var current = WordPair.random();// ↓ Add this.void getNext() {current = WordPair.random();notifyListeners();}
}// ...

新的 getNext() 方法为 current 重新分配了新的随机 WordPair。它还调用 notifyListeners() (ChangeNotifier) 的一个方法),以确保向任何通过 watch 方法跟踪 MyAppState 的对象发出通知。

其余要做的就是通过按钮的回调来调用 getNext 方法。

lib/main.dart

// ...ElevatedButton(onPressed: () {appState.getNext();  // ← This instead of print().},child: Text('Next'),),// ...

现在,保存并尝试运行应用。当您每次按下 Next 按钮时,该应用都会生成一个新的随机单词对。

在下一节中,您将改善用户界面的外观。

5. 改善应用外观

下图展示了应用的当前外观。

不太好。应用的核心功能(随机生成单词对)应更显眼。毕竟,这是应用为用户提供的主要功能!其他问题还包括,应用的内容不在中心位置,整个应用只有单调的黑色和白色。

本节将通过调整应用设计来解决这些问题。本节的最终目标是实现类似下图的效果:

提取 widget

现在,负责显示当前单词对的代码行大概是这样的:Text(appState.current.asLowerCase)。要改为更复杂的设计,一种行之有效的方式是将此代码行提取到单独的 widget 中。为 UI 的单独逻辑部分使用单独的 widget 是在 Flutter 中管理复杂性的一种重要方法。

Flutter 提供了一个用于提取 widget 的重构帮助程序,但在使用它之前,请确保所提取的代码行仅访问所需的内容。现在,该代码行将访问 appState,但实际上只需知道当前的单词对是什么。

综合考虑以下因素,重写 MyHomePage 的代码,如下所示:

class MyHomePage extends StatelessWidget {@overrideWidget 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 widget 不再引用整个 appState

现在,您需要调出 Refactor 菜单。在 AS 中,您可以通过以下两种方式之一执行此操作:

  1. 右键点击要重构的代码段(在本例中为 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() 方法进行以下更改。

lib/main.dart

 @overrideWidget 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() 方法进行以下更改。

lib/main.dart

  @overrideWidget 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 {@overrideWidget build(BuildContext context) {var appState = context.watch<MyAppState>();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 {@overrideWidget build(BuildContext context) {var appState = context.watch<MyAppState>();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'),),],),),);}
}

该应用会象是

在下一节中,您将添加收藏(或“喜欢”)生成的单词的功能。

6. 添加功能

应用现在运行良好,有时甚至会提供一些有趣的单词对。但是,当用户点击 Next 时,每个单词对都会永久消失。最好能通过一种方法来“记住”最佳建议,例如使用“Like”按钮。

添加业务逻辑

滚动至 MyAppState 并添加以下代码:

lib/main.dart

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();}
}

下面分析各项更改:

  • 您在 MyAppState 中添加了一个名为 favorites 的新属性。此属性使用一个空的列表进行初始化,即 []
  • 您还使用 generics 指定该列表只能包含单词对:<WordPair>[]。这有助于增强应用的可靠性 — 如果您尝试向应用添加 WordPair 以外的任何内容,Dart 甚至会拒绝运行应用。相应的,您可以使用 favorites 列表,同时知道其中永远不会隐藏任何不需要的对象(如 null)。

注意:除了 List(用 [] 表示)以外,Dart 还提供了其他一些集合类型。您可能认为 Set(用 {} 表示)可以更有效地表示收藏夹集合。为了让此 Codelab 保持尽可能简单易懂,我们仍然坚持使用了列表。但如果需要,您可以改为使用 Set。代码不会有太大变化。

  • 您还添加了一个新方法 toggleFavorite(),它可以从收藏夹列表中删除当前单词对(如果已经存在),或者添加单词对(如果不存在)。在任何一种情况下,代码都会在之后调用 notifyListeners();

添加按钮

完成“业务逻辑”后,接下来继续充实用户界面。如需将“Like”按钮放在“Next”按钮的左侧,我们需要使用 RowRow widget 是您之前看到的 Column 的水平等效项。

首先,将现有按钮封装在 Row 中。找到 MyHomePage 的 build() 方法,将光标放在 ElevatedButton 上,使用 Ctrl+. 或 Cmd+. 调出 Refactor 菜单,然后选择 Wrap with Row

保存时,您会注意到 Row 在行为上类似于 Column — 默认情况下,它会将其子项集中在左侧。(Column 会将其子项集中到顶部。)要解决此问题,您可以使用与之前相同的方法,但这次要用到 mainAxisAlignment。不过,出于教学(学习)目的,请使用 mainAxisSize。这会告知 Row 不要占用所有可用的水平空间。

做出以下更改:

lib/main.dart


class MyHomePage extends StatelessWidget {@overrideWidget build(BuildContext context) {var appState = context.watch<MyAppState>();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,以便让两个按钮稍微分开。

只不过,用户看不到收藏夹。因此,在下一节中,我们将在应用添加一个完整的独立屏幕!

7. 添加侧边导航栏

大多数应用都无法将所有内容放置在一个屏幕中。此特定应用或许可以这样做,但为了实现更好的学习效果,您将为用户的收藏夹创建一个单独的屏幕。为了在两个屏幕之间进行切换,您将实现您的第一个 StatefulWidget

为了尽快了解这一步的内容,请将 MyHomePage 拆分为 2 个单独的 widget。

全选 MyHomePage 并删除,然后替换为以下代码:


class MyHomePage extends StatelessWidget {@overrideWidget 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 {@overrideWidget 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'),),],),],),);}
}

保存后,您会看到界面的可视效果是正常的,但在功能上无法正常运行。点击侧边导航栏中的 ♥︎(心形符号)后,应用没有任何反应。

检查更改。

  • 首先,请注意 MyHomePage 的全部内容均被提取到新的 GeneratorPage widget。在旧版 MyHomePage widget 中,唯一未提取的部分是 Scaffold
  • 新的 MyHomePage 包含一个有两个子项的 Row。第一个是 SafeArea widget,第二个是 Expanded widget。
  • SafeArea 将确保其子项不会被硬件凹口或状态栏遮挡。在此应用中,widget 会将 NavigationRail 封装,以防止导航按钮被遮挡,例如被移动状态栏遮挡。
  • 您可以将 NavigationRail 中的 extended: false 行更改为 true。这将显示图标旁边的标签。在接下来的某个步骤中,你将学习如何在应用有足够的水平空间时自动完成此操作。
  • 侧边导航栏有两个目标页面(Home 和 Favorites),两者都有各自的图标和标签。侧边导航栏还定义了当前的 selectedIndex。若选定索引 (selectedIndex) 为零,则会选择第一个目标页面;若选定索引为一,则会选择第二个目标页面,依此类推。目前,它被硬编码为零。
  • 侧边导航栏还定义了当用户选择其中一个具有 onDestinationSelected 的目标页面时会发生什么。现在,应用仅通过 print() 输出所请求的索引值。
  • Row 的第二个子项是 Expanded widget。展开的 widget 在行和列中极具实用性 — 它们可用于呈现以下布局:一些子项仅占用其所需要的空间(在本例中为 NavigationRail),而其他 widget 则尽可能多地占用其余空间(在本例中为 Expanded)。可以将 Expanded widget 视为一种“贪婪的”元素。如果您想要更好地感受此 widget 的作用,请尝试用另一个 Expanded 封装 NavigationRail widget。
  • 两个 Expanded widget 会分割两者之间所有可用的水平空间,即使侧边导航栏只需要左侧的一小部分。
  • 在 Expanded widget 内部,有一个指定了颜色的 Container;而在该容器内部,有一个 GeneratorPage

无状态 widget 与有状态 widget

截至目前,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 中私有属性以及其他主题,请参阅语言导览。

setState

新的有状态 widget 只需要跟踪一个变量,即 selectedIndex。对 _MyHomePageState 进行以下 3 处更改:

class _MyHomePageState extends State<MyHomePage> {var selectedIndex = 0;     // ← Add this property.@overrideWidget 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(),),),],),);}
}

下面分析各项更改:

  1. 您引入了一个新变量 selectedIndex,并将其初始化为 0
  2. 您在 NavigationRail 定义中使用此新变量,而不再是像之前那样将其硬编码为 0
  3. 当调用 onDestinationSelected 回调时,并不是仅仅将新值输出到控制台,而是将其分配到 setState() 调用内部的 selectedIndex。此调用类似于之前使用的 notifyListeners() 方法 — 它会确保界面始终更新为最新状态。
  4. 侧边导航栏现在会响应用户交互。但右侧的展开区域仍然保持不变。这是因为代码并未使用 selectedIndex 来确定显示哪一个屏幕。

使用 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');
}

详细分析这段代码:

  1. 这段代码声明了一个类型为 Widget 的新变量 page
  2. 然后,根据 selectedIndex 中的当前值,switch 语句为 page 分配一个屏幕。
  3. 目前还没有 FavoritesPage,因此先使用 Placeholder;这是一个便捷易用的 widget,可以在其放置地方绘制一个交叉矩形,以便将界面的该部分标记为未完成。

              

  1. 通过应用快速失败原则,switch 语句还将确保在 selectedIndex 既不是 0 也不是 1 的情况下抛出错误。这有助于防止后续 bug。如果您向侧边导航栏添加了一个新的目标页面而忘记更新此代码,则程序会在开发过程中崩溃(而不是让您猜测程序为何无法正常运行,或者让您将有缺陷的代码发布到生产环境中)。

page 现已包含您想要在右侧显示的 widget,您大概可以猜到还需要哪些其他更改。

完成最后一项更改的 _MyHomePageState 如下所示:


class _MyHomePageState extends State<MyHomePage> {var selectedIndex = 0;@overrideWidget 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 菜单进行所需的更改。不过,这一次有点复杂:

  1. 在 _MyHomePageState 的 build 方法内部,将光标放在 Scaffold 上。
  2. 使用 Ctrl+. 键 (Windows/Linux) 或 Cmd+. 键 (Mac) 调出 Refactor 菜单。
  3. 选择 Wrap with Builder 并按下 Enter 键。
  4. 将新添加的 Builder 的名称修改为 LayoutBuilder
  5. 将回调参数列表从 (context) 修改为 (context, constraints)

每当约束发生更改时,系统都会调用 LayoutBuilder 的 builder 回调。比如说,以下场景就会触发这种情况:

  • 用户调整应用窗口的大小
  • 用户将手机从人像模式旋转到横屏模式,或从横屏模式旋转到人像模式
  • MyHomePage 旁边的一些 widget 变大,使 MyHomePage 的约束变小
  • 其他还有很多,不再一一列举

现在,您的代码可以通过查询当前的 constraints 来决定是否显示标签。对 _MyHomePageState 的 build 方法进行以下单行更改:

class _MyHomePageState extends State<MyHomePage> {var selectedIndex = 0;@overrideWidget 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 屏幕。下一节将介绍此项操作

8. 添加新页面

还记得我们用来暂时替代 Favorites 页面的 Placeholder widget 吗?

是时候将其替换为真实页面了。

如果您敢于挑战,请尝试自行完成此步骤。您的目标是在新的 FavoritesPage 这一无状态 widget 中显示 favorites 列表,然后显示该 widget,而不是 Placeholder

下面提供了一些指引:

  • 如果想要一个可滚动的 Column 时,请使用 ListView widget。
  • 请记住,使用 context.watch<MyAppState>() 从任何 widget 访问 MyAppState 实例。
  • 如果您还想尝试新的 widget,可以使用 ListTile 的 title(通常用于文本)、leading(用于图标或头像)和 onTap(用于交互)等属性。不过,您也可以使用已经掌握的 widget 来实现类似的效果。
  • Dart 允许在集合字面量内部使用 for 循环。例如,如果 messages 包含一个字符串列表,您可以使用如下代码:
  • 另一方面,如果您更熟悉函数式编程,Dart 还支持编写 messages.map((m) => Text(m)).toList() 这样的代码。当然,您始终可以创建一个 widget 列表,并将其强制添加到 build 中。

    自行添加 Favorites 页面的好处是,您可以自己做决策,并从中学到更多知识。但其缺点是,您可能会遇到自己无法解决的问题。请记住:不要害怕失败,它是通往成功的必经之路。没有人要求您在一个小时内就掌握 Flutter 开发,这也不现实。

  • 下面提供的只是实现 Favorites 页面的一种方法。其实现方法将(有希望)激发您完善代码、改进界面并为己所用。

    新的 FavoritesPage 类如下所示:

  • class FavoritesPage extends StatelessWidget {@overrideWidget build(BuildContext context) {var appState = context.watch<MyAppState>();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),),],);}
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/130519.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SQL 注入漏洞攻击

文章目录 1. 介绍2. 无密码登录3. 无用户名无密码登录4. 合并表获取用户名密码 1. 介绍 假设你用自己的用户名和密码登录了一个付费网站&#xff0c;网站服务器就会查询一下你是不是 VIP 用户&#xff0c;而用户数据都是放在数据库中的&#xff0c;服务器通常都会向数据库进行查…

最新IDE流行度最新排名(每月更新)

2023年09月IDE流行度最新排名 顶级IDE排名是通过分析在谷歌上搜索IDE下载页面的频率而创建的 一个IDE被搜索的次数越多&#xff0c;这个IDE就被认为越受欢迎。原始数据来自谷歌Trends 如果您相信集体智慧&#xff0c;Top IDE索引可以帮助您决定在软件开发项目中使用哪个IDE …

【初阶C语言】操作符2---表达式求值

前言&#xff1a;本节重点介绍操作符的使用&#xff0c;如&#xff0c;优先级高低、类型转换等 一、逻辑操作符 前言&#xff1a;逻辑操作符包括逻辑与&#xff08;&&&#xff09;和逻辑或&#xff08;||&#xff09;&#xff0c;操作对象&#xff1a;两个 1.逻辑与&…

一文了解数据科学Notebook

编者按&#xff1a; 主要介绍什么是Notebook&#xff0c;Notebook在数据科学领域的应用的重要性与优势&#xff0c;以及数据科学家/算法团队在选择Notebook时需考虑哪些关键因素。同时&#xff0c;基于Notebook的筛选考量维度&#xff0c;对常见的Notebook进初步对比分析&#…

2023-9-11 台阶-Nim游戏

题目链接&#xff1a;台阶-Nim游戏 #include <iostream> #include <algorithm>using namespace std;int main() {int n;cin >> n;int res 0;for(int i 1;i < n; i){int x;cin >> x;if(i % 2) res ^ x; }if(res) cout << "Yes" &l…

MyBatis-Plus深入 —— 条件构造器与插件管理

前言 在前面的文章中&#xff0c;荔枝梳理了一个MyBatis-Plus的基本使用、配置和通用Service接口&#xff0c;我们发现在MyBatis-Plus的辅助增强下我们不再需要通过配置xml文件中的sql语句来实现基本的sql操作了&#xff0c;不愧是最佳搭档&#xff01;在这篇文章中&#xff0c…

Linux:工具(vim,gcc/g++,make/Makefile,yum,git,gdb)

目录 ---工具功能 1. vim 1.1 vim的模式 1.2 vim常见指令 2. gcc/g 2.1 预备知识 2.2 gcc的使用 3.make,Makefile make.Makefile的使用 4.yum --yum三板斧 5.git --git三板斧 --Linux下提交代码到远程仓库 6.gdb 6.1 gdb的常用指令 学习目标&#xff1a; 1.知道…

[构建自己的 Vue 组件库] 小尾巴 UI 组件库

文章归档于&#xff1a;https://www.yuque.com/u27599042/row3c6 组件库地址 npm&#xff1a;https://www.npmjs.com/package/xwb-ui?activeTabreadme小尾巴 UI 组件库源码 gitee&#xff1a;https://gitee.com/tongchaowei/xwb-ui小尾巴 UI 组件库测试代码 gitee&#xff1a…

2023年世界机器人大会回顾

1、前记&#xff1a; 本次记录是我自己去世界机器人博览会参观的一些感受&#xff0c;所有回顾为个人感兴趣部分的机器人产品分享。整个参观下来最大的感受就是科学技术、特别是机器人技术和人工智能毫无疑问地、广泛的应用在我们日常生活的方方面面&#xff0c;在安全巡检、特…

Vue 报错error:0308010C:digital envelope routines::unsupported 解决方案(三种)

新换的电脑&#xff0c;系统装的win11&#xff0c;node也是18的版本。 跑了一下老项目&#xff0c;我用的是HbuilderX&#xff0c;点击运行和发行时&#xff0c;都会报错&#xff1a; Error: error:0308010C:digital envelope routines::unsupported 出现这个错误是因为 node.j…

数学建模B多波束测线问题B

数学建模多波束测线问题 完整思路和代码请私信~~~~ 1.问题重述&#xff1a; 单波束测深是一种利用声波在水中传播的技术来测量水深的方法。它通过测量从船上发送声波到声波返回所用的时间来计算水深。然而&#xff0c;由于它是在单一点上连续测量的&#xff0c;因此数据在航…

从 算力云 零开始部署ChatGLM2-6B 教程

硬件最低需求&#xff0c;显存13G以上 基本环境&#xff1a; 1.autodl-tmp 目录下 git clone https://github.com/THUDM/ChatGLM2-6B.git然后使用 pip 安装依赖&#xff1a; pip install -r requirements.txtpip 使用pip 阿里的 再执行git clone之前&#xff0c;要先在命令行…

【笔试强训选择题】Day40.习题(错题)解析

作者简介&#xff1a;大家好&#xff0c;我是未央&#xff1b; 博客首页&#xff1a;未央.303 系列专栏&#xff1a;笔试强训选择题 每日一句&#xff1a;人的一生&#xff0c;可以有所作为的时机只有一次&#xff0c;那就是现在&#xff01;&#xff01;&#xff01; 文章目录…

Unity Animation、Animator 的使用(超详细)

文章目录 1. 添加动画2. Animation2.1 制作界面2.2 制作好的 Animation 动画2.3 添加和使用事件 3. Animator3.1 制作界面3.2 一些参数解释3.3 动画参数 4. Animator中相关类、属性、API4.1 类4.2 属性4.3 API4.4 几个关键方法 5. 动画播放和暂停控制 1. 添加动画 选中待提添加…

【赠书活动】考研备考书单推荐

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

javaweb04-vue基础

话不多说&#xff0c;参考官网地址Vue官网集成Vue应用。 一、Vue快速入门 &#xff08;1&#xff09;新建HTML页面&#xff0c;引入Vue.js 我这里用的是CDN方式 <script src"https://unpkg.com/vue3/dist/vue.global.js"></script> &#xff08;2&am…

UMA 2 - Unity Multipurpose Avatar☀️四.UMA人物部位的默认颜色和自定义(共享)颜色

文章目录 🟥 人物颜色介绍1️⃣ 使用默认颜色2️⃣ 使用自定义颜色🟧 UMA自定义颜色的作用🟨 自定义颜色还可作为共享颜色🟥 人物颜色介绍 UMA不同部位的颜色分为默认的内置颜色和我们新定义的颜色. 1️⃣ 使用默认颜色 比如不勾选UseSharedColor时,使用的眼睛的默认…

javaee springMVC的简单使用 jsp页面在webapp和web-inf目录下的区别

项目结构 依赖文件 <?xml version"1.0" encoding"UTF-8"?><project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/…

搭建自己的OCR服务,第二步:PaddleOCR环境安装

PaddleOCR环境安装&#xff0c;遇到了很多问题&#xff0c;根据系统不同问题也不同&#xff0c;不要盲目看别人的教程&#xff0c;有的教程也过时了&#xff0c;根据实际情况自己调整。 我这边目前是使用windows 10系统CPU python 3.7 搭建。 熟悉OCR的人应该知道&#xff0…

人工智能基础-趋势-架构

在过去的几周里&#xff0c;我花了一些时间来了解生成式人工智能基础设施的前景。在这篇文章中&#xff0c;我的目标是清晰概述关键组成部分、新兴趋势&#xff0c;并重点介绍推动创新的早期行业参与者。我将解释基础模型、计算、框架、计算、编排和矢量数据库、微调、标签、合…