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

2023-10-26

依照上一篇中我们新建的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),
              ),
          ],
        );
      }
    }

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Fultter学习日志(2)-构建第一个flutter应用 的相关文章

随机推荐

  • cmake简洁教程 - 第一篇

    由于cmake内容较多 篇幅较长 为了不让人疲倦 分成了多篇博客 全部博客链接如下 cmake简洁教程 第一篇 YZF Kevin的博客 CSDN博客 cmake简洁教程 第二篇 YZF Kevin的博客 CSDN博客 cmake简洁教程
  • mt19937 随机数

    https blog csdn net real myth article details 53893854 https blog csdn net calmreason article details 72655060 https blo
  • 数字图像处理-python基于opencv代码实现 反转变换、对数变换和幂律(伽马)变换

    本文主要介绍对 数字图像处理 第三章书中示例图片实现 反转变换 对数变换以及伽马变换的代码 若要获取更多数字图像处理 python 深度学习 机器学习 计算机视觉等高清PDF以及 更多有意思的 分享 可搜一搜 微信公共号 分享猿 免费获取资
  • typescript基础之object和Object

    TypeScript 的 object 和 Object 是两种不同的类型 它们的区别和用途如下 object 类型是 TypeScript 2 2 引入的新类型 它表示非原始对象 也就是除了 number string boolean s
  • 实时时钟电路DS1302的原理及应用

    2006 05 11 10 10 39 实时时钟电路DS1302的原理及应用
  • 使用windeployqt.exe打包QT工程,windows系统可执行程序

    前言 因为自己打包qt程序遇到点问题 提示0xc000007b错误 发现是因为打包工具和工程编译工具不对应导致 于是为了记录打包方法 有了此篇文章 记录使用windeployqt exe打包qt工程在windows系统的可执行文件 一 确定
  • adb install 命令参数

    adb install 6个参数描述 t 允许测试包 l 锁定该应用程序 s 把应用程序安装到sd卡上 g 为应用程序授予所有运行时的权限 r 替换已存在的应用程序 也就是说强制安装 d 允许进行将见状 也就是安装的比手机上带的版本低
  • activiti-serviceTask(服务任务)

    Activiti服务任务 serviceTask Activiti服务任务 serviceTask 作者 邓家海 都有一段沉默的时间 等待厚积薄发 应用场景 当客户有这么一个需求 下一个任务我需要自动执行一些操作 并且这个节点不需要任何的人
  • 一文让你深刻理解异步请求池-DNS解析与实现

    一 DNS概念简述 DNS Domain Name Service 域名解析服务 工作在应用层 是互联网的一项服务 它作为将域名和IP地址相互映射的一个分布式数据库 能够使人更方便地访问互联网 DNS监听在TCP和UDP端口53 FQDN
  • SpringMVC系列(十)(处理静态资源)和...

  • 通俗理解泰勒公式

    本博客只用于自身学习 如有错误 虚心求教 在维基百科上的解释 在数学中 泰勒公式 英语 Taylor s Formula 是一个用函数在某点的信息描述其附近取值的公式 这个公式来自于微积分的泰勒定理 Taylor s theorem 泰勒定
  • 计算方法——C语言实现——迭代法求解线性方程组

    最近在上计算方法这门课 要求是用MATLAB做练习题 但是我觉得C语言也很棒棒啊 题目 和直接法不同 迭代法是一种逐次逼近的方法 将复杂问题简单化 求比较大的方程组时一般都不会用直接法 迭代法有好几种 这里使用了Jacobi迭代与Gauss
  • 8.4收官之战非农蓄力能否引爆黄金单边行情?

    近期有哪些消息面影响黄金走势 黄金多空该如何研判 黄金消息面解析 周五 8月4日 亚洲时段 现货黄金在近三周低位窄幅震荡 目前交投于1937 60美元 盎司附近 美联储7月决策符合预期 如期加息25个基点 虽然美国通胀增速放缓 但仍高于美联
  • Git 大文件push失败

    目录 1 下载并安装Git Large File Storage命令行扩展 2 配置lfs跟踪的文件 3 commit 并push到远程仓库 由于git有push文件的大小限制 100MB 因此如果push操作中右超过100MB的文件 就会
  • 抽签小程序(C语言随机数),C# 抽签小程序

    设计背景 设置一个Excel名单表 对名单进行随机抽取 设计思路 使用Timer定时器 运行定时器进行名单随机滚动 停止定时器获得抽签结果 相关技术 随机数 Excel读取 导出 XML文档读写 相关类库 C1 C1Excel Excel操
  • 《深入浅出话数据结构》系列之什么是B树、B+树?为什么二叉查找树不行?

    本文将为大家介绍B树和B 树 首先介绍了B树的应用场景 为什么需要B树 然后介绍了B树的查询和插入过程 最后谈了B 树针对B树的改进 在谈B树之前 先说一下B树所针对的应用场景 那么B树是用来做什么的呢 B树是一种为辅助存储设计的一种数据结
  • 达梦DCA认证培训和考试

    本人有幸参加了达梦DCA认证培训并参加了认证考试 培训内容包括 第一天 国产数据库现状及未来 DM8企业版安装 创建数据库及数据库实例管理 DM8体系结构 第二天 表空间管理 用户管理 DMSQL 第三天 模式对象管理 备份还原 配置作业
  • 数据结构课程设计 最小生成树,拓扑排序以及最短路径

    通信网络的架设问题 问题描述 若要在n 10 个城市之间建设通信网络 只需要架设n 1条线路即可 如何以最低的经济代价建设这个通信网 是一个网的最小生成树问题 基本要求 1 利用二种方法 Prim算法和克鲁斯卡尔 Kruskual 生成网中
  • 阿里Java后端电话面试

    生平第一次面试 还是阿里 非常紧张 因为是校招 所以面的比较简单 都是我简历上说熟悉的东西 回答的不是很理想 面试官说我广度还行 深度差的比较多 面试官 你好同学 我是蚂蚁金服的 现在方便面试吗 我 方便方便 面试官 请简单介绍一下自己 这
  • Fultter学习日志(2)-构建第一个flutter应用

    依照上一篇中我们新建的flutter应用 让我们更改pubspec yaml中的内容为 name namer app description A new Flutter project publish to none Remove this