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

news2024/11/25 16:37:09

依照上一篇中我们新建的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

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

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

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),
        ],
      ),
    );
  }
}

到目前为止,这 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});

  @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,即应用的起点。

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 {
  @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。下面每个带编号的行均映射到上面代码中相应行编号的注释:

  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 {
  @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 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

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

lib/main.dart

  @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<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 {
  @override
  Widget 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 {
  @override
  Widget 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 {
  @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'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

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

检查更改。

  • 首先,请注意 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.

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

下面分析各项更改:

  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;

  @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 菜单进行所需的更改。不过,这一次有点复杂:

  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;

  @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 屏幕。下一节将介绍此项操作

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 {
      @override
      Widget 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.coloradmin.cn/o/1004004.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot整合Easy-ES实现对ES操作

请确保已有可用的ES&#xff0c;若没有&#xff0c;请移步&#xff1a;Docker安装部署ElasticSearch&#xff08;ES&#xff09; 新建SpringBoot项目 这里是用的springboot版本是2.6.0 引入依赖 <!-- 排除springboot中内置的es依赖,以防和easy-es中的依赖冲突--><…

0基础学习VR全景平台篇 第99篇:百度地图如何上传全景图

蛙色平台现已打通VR全景入驻百度地图全流程&#xff0c;百度全景分为免费版和付费版两种&#xff0c;其中付费支持配置作品音乐、场景漫游热点、联系电话、描述信息。 百度地图上传案例 免费版 付费版 一、百度地图上传流程 1、进入蛙色VR账号后台 &#xff08;1&#xff…

Fiddler抓取HTTPS 详解

对于想抓取HTTPS的测试初学者来说&#xff0c;常用的工具就是fiddler。 但是初学时&#xff0c;大家对于fiddler如何抓取HTTPS难免走歪路&#xff0c;也许你一步步按着网上的帖子成功了&#xff0c;这自然是极好的。 但也有可能没那么幸运&#xff0c;这时候你就会很抓狂。 …

华为云云服务器云耀L实例评测 | 华为云云服务器实例新品全面解析

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

分享一下在微信商城上怎么可以快速实现分销功能

微信商城上的分销功能是一种吸引更多用户和提升销售的方式&#xff0c;通过搭建一个以分销为主的平台&#xff0c;商家可以借助用户的力量来推广自己的产品或服务。下面将介绍在微信商城上如何实现分销功能&#xff0c;包括分销模式的选择、开发流程和推广方法。 一、选择适合的…

BUG定位分析方法

作为测试人员&#xff0c;接触最多的就是bug&#xff0c;怎样才能体现出测试人的专业性&#xff1f;能够精准的定位并分析bug一定是你的加分项。 在什么地方干了什么产生了什么结果&#xff0c;和期望的结果不一致&#xff0c;那么这就是一个bug。人人都能找出bug&#xff0c;…

【SpringMVC】JSR 303与拦截器注释使用

目录 一、JSR 303 1.1 JSR 303介绍 1.2 为什么要使用JSR-303 1.3 常用注解 1.4 使用示例 1.4.1 导入JSR303依赖 1.4.2 配置校验规则 1.4.3 编写方法校验 1.4.4 编写前端 1.4.5 测试 ​编辑 1.5 Validated与Valid区别 二、拦截器&#xff08;interceptor&#xff09…

SpringBoot2.0入门(详细文档)

文章目录 Springboot是什么Springboot2.x依赖环境和版本新特性说明为什么学习Springboot从springboot优点来看从未来发展的趋势来看 开发环境Spring Boot开发环境搭建和项目启动jdk 的配置Spring Boot 工程的构建maven配置IDEA 快速构建maven 创建工程常用注解 完整代码 Spring…

延迟win11的更新

自从升级到win11之后&#xff0c;发现更新插件的频率高得有点过分,每隔几天就是一次. 我看网上有人能把更新时间延迟几十万年,所以我心动了! 我试了一下:成功延迟到2099年 创建一个文本文件&#xff0c;命名为&#xff1a;“stopupdate.reg”&#xff0c;然后用记事本或者代码…

SpringMVC之入门:springmcx工作流程,springmvc的入门,静态资源处理器

springmvc工作流程springmvc的入门静态资源处理 1.springmvc工作流程 自定义mvc流程&#xff1a;1.客户端浏览器发送请求url http://localhost:8080/mvc/book.action?methodNamelist 2.被中央控制器dispatchServlet接收 *.action 将url处理&#xff0c;截取得到 *(/book) 3.通…

水循环原理VR实景教学课件开发

日本核污水排海让人们越来越重视海洋大气层水循环的安全&#xff0c;水循环是一个周而复始、循环往复的动态过程&#xff0c;为了将水循环过程以形象、生动地形式展示出来&#xff0c;水循环VR全景动态演示逐渐受到大家青睐。 传统的水循环教育方式通常是通过图片、动画或实地考…

python机器人编程——用python实现一个写字机器人

目录 一、前言二、整体框架2.1 系统构成2.2 硬件介绍2.2.1主要组成部分2.2.2机械结构2.2.3驱动及控制主板PS电机驱动原理简介: 2.2.4其余部分 2.3 机器人python程序框架2.3.1通信服务模块2.3.2消息处理模块2.3.3轨迹解析模块2.3.4机械臂逆解模块2.3.5写字板模块 三、机械臂的建…

我们如何在工作与生活中找到平衡点?

找到工作与生活中的平衡点是每个人都必须面对的问题。以下是一些建议&#xff0c;可以帮助你在工作和生活之间找到平衡&#xff1a; 制定时间表&#xff1a;确保你有足够的时间来处理工作和生活中的各种任务。为工作、学习和个人生活设定优先级&#xff0c;并确保时间分配合理…

VMware Explore | 联想与VMware扩大合作带来生成式AI和多云解决方案

*带有 VMware Cloud 的全新联想 ThinkSystem 生成式 AI 解决方案&#xff0c;采用 NVIDIA 加速计算和软件&#xff0c;提供专为实现下一代 AI 工作负载而打造的 GPU 密集型平台。 联合创新实验室为商业中端市场和企业提供即用型混合多云解决方案。 全新 Lenovo TruScale Hybr…

C++ - 搜索二叉树

二叉搜索树的概念 二叉搜索树&#xff0c;又称二叉排序树。它具有以下性质&#xff1a; 若它的左子树不为空&#xff0c;则左子树上所有节点的值都小于根节点的值。若它的右子树不为空&#xff0c;则右子树上所有节点的值都大于根节点的值。它的左右子树也分别为二叉搜索树。 …

核心实验16_端口镜像_ENSP

项目场景&#xff1a; 核心实验16_端口镜像_ENSP 实搭拓扑图&#xff1a; 具体操作&#xff1a; 交换机: [garliccc]observe-port 1 interface GigabitEthernet 0/0/3 /设置0/0/3为观察口 [garliccc]int g0/0/2 [garliccc-GigabitEthernet0/0/2]port-mirroring to observe-po…

Java后端简历指南(应届)

⭐简单说两句⭐ 作者&#xff1a;后端小知识 CSDN个人主页&#xff1a;后端小知识 &#x1f50e;GZH&#xff1a;后端小知识 &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; Java后端简历指南&#xff08;应届&#xff09; 文章目录 Java后端简…

人离自动断电设备的功能要求

人离开自动断电石家庄光大远通电气有限公司用电器待机能耗往往是一种不易被发现的“隐藏的浪费”&#xff0c; 如果将一户家庭的空调、洗衣机、电视、微波炉、电饭煲五类电器进行计算&#xff0c;待机功率在12W到15W&#xff0c;待机能耗0.2度到0.33度电。每年能耗73度到124.45…

Nginx 中 location 和 proxy_pass 斜杠/ 问题

location 的斜杠问题比较好理解&#xff0c;不带斜杠的是模糊匹配。例如&#xff1a; location /doc 可以匹配 /doc/index.html&#xff0c;也可以匹配 /docs/index.html。 location /doc/ 强烈建议使用这种 只能匹配 /doc/index.html&#xff0c;不能匹配 /docs/index…

Python入门 | 如何判断多个条件

入门教程、案例源码、学习资料、读者群 请访问&#xff1a; python666.cn 大家好&#xff0c;欢迎来到 Crossin的编程教室 &#xff01; 之前我们已经了解了如何在 Python 中进行条件判断&#xff08;《是真是假&#xff1f;》&#xff09;&#xff0c;以及根据判断的结果执行不…