编写第一个 Flutter 应用

如果你更喜欢由讲师介绍的 codelab 版本,请查看以下的 workshop:

The app that you'll be building

这是一个指引你完成第一个 Flutter 应用的手把手操作教程(我们也称之为是 codelab)。我们将会着手创建一个简单的 Flutter 应用,无需 Dart 语言、移动端开发、桌面端开发或 Web 开发的经验,只需你具备面向对象语言开发基础即可(如变量,循环和条件语句)。

完整的教程分为两部分,本页面是第一部分的内容,你可以在这里查看 第二部分 的内容。 (Codelabs 里的第一部分内容与本页内容相同)。

第一部分的内容概览

你将完成一个简单的应用,功能是:为一个创业公司生成建议的公司名称。用户可以选择和取消选择的名称、保存喜欢的名称。该代码一次生成十个名称,当用户滚动时,会生成新一批名称。

页面上方的这个 GIF 可以引导你预览本 codelab 做完之后的应用效果图。

任何一个 Flutter 的项目都可以编译为 web 应用。你可以在 IDE 中打开 devices 选择器,或者是在命令行中输入 flutter devices,这样你就可以看到 Chrome 以及 Web server 选项卡。 Chrome 设备将会自动打开 Chrome。 Web server 则会运行一个服务程式托管应用,这样你就可以在任意浏览器加载它。在开发时请使用 Chrome 进行调试,以使用 DevTools。当你想在其他浏览器测试时,请使用 web server。更多详细信息请查看 使用 Flutter 构建 web 应用,以及 编写你的第一个 Flutter web 应用程序

同时,Flutter 应用也可以编译到桌面端。你会在 IDE 的 设备 列表内或运行 flutter devices 时看到其列举了你的操作系统,例如 Windows (desktop),想了解更多关于构建桌面端应用的信息,参考 编写一个 Flutter 桌面应用

第一步:创建初始化工程

按照 这个指南 中所描述的步骤,创建一个简单的、基于模板的 Flutter 工程,然后将项目命名为 startup_namer (而不是 myapp),接下来你将会修改这个工程来完成最终的 App。

在这个示例中,你将主要编辑 Dart 代码所在的 lib/main.dart 文件,

  1. 删除 lib/main.dart 中的所有代码,然后替换为下面的代码,它将在屏幕的中心显示「Hello World」。

    lib/main.dart
    // Copyright 2018 The Flutter team. All rights reserved.
    // Use of this source code is governed by a BSD-style license that can be
    // found in the LICENSE file.
    
    import 'package:flutter/material.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Welcome to Flutter',
          home: Scaffold(
            appBar: AppBar(
              title: const Text('Welcome to Flutter'),
            ),
            body: const Center(
              child: Text('Hello World'),
            ),
          ),
        );
      }
    }
  2. 运行 你的工程项目,根据不同的操作系统,你会看到如下运行结果界面:

    Hello world app on Windows
    Windows
    Hello world app on iOS
    iOS

观察和分析

  • 本示例创建了一个具有 Material Design 风格的应用, Material 是一种移动端和网页端通用的视觉设计语言, Flutter 提供了丰富的 Material 风格的 widgets。在 pubspec.yaml 文件的 flutter 部分选择加入 uses-material-design: true 会是一个明智之举,通过这个可以让您使用更多 Material 的特性,比如其预定义好的 图标 集。

  • 该应用程序继承了 StatelessWidget,这将会使应用本身也成为一个 widget。在 Flutter 中,几乎所有都是 widget,包括对齐 (alignment)、填充 (padding) 和布局 (layout)。

  • Scaffold 是 Material 库中提供的一个 widget,它提供了默认的导航栏、标题和包含主屏幕 widget 树的 body 属性。 widget 树可以很复杂。

  • 一个 widget 的主要工作是提供一个 build() 方法来描述如何根据其他较低级别的 widgets 来显示自己。

  • 本示例中的 body 的 widget 树中包含了一个 Center widget, Center widget 又包含一个 Text 子 widget, Center widget 可以将其子 widget 树对齐到屏幕中心。

第二步:使用外部 package

在这一步中,你将开始使用一个名为 english_words 的开源软件包,其中包含数千个最常用的英文单词以及一些实用功能。

你可以在 pub.dev 上找到 english_words package 以及其他许多开源的 package。

pubspec.yaml 文件管理着 Flutter 工程中的所有资源和依赖。

  1. 通过下面的方式将 english_words 这个 package 加入你的工程里:

    在你的 IDE 中,将 english_words: ^4.0.0 加到 cupertino_icons 1.0.4 后面,然后保存文件。

    文件保存后就会触发依赖的获取,这等同于执行下面的命令:

$ `flutter pub add english_words`

输出的结果会类似下面这些:

Resolving dependencies...
These packages are no longer being depended on:
+ english_words 4.0.0
Downloading english_words 4.0.0...

Changed 1 dependency!

依赖关系的获取也会自动生成 pubspec.lock 文件,这个文件包含所有加入项目的 package 和版本号信息。

pubspec.yaml 文件管理着 Flutter 工程中的所有资源和依赖。在这个文件中,现在你会观察到 english_words 这个依赖已经添加:

{step1_base → step2_use_package}/pubspec.yaml
@@ -25,4 +25,5 @@
25
25
  dependencies:
26
26
  flutter:
27
27
  sdk: flutter
28
28
  cupertino_icons: ^1.0.2
29
+ english_words: ^4.0.0
  1. 在 Android Studio 的编辑器视图中查看 pubspec.yaml 文件时,点击 Pub get 会将依赖包安装到你的项目。你应该会在控制台中看到以下内容:

$ `flutter pub get`

会大约输出下面类似的内容:

Running "flutter pub get" in startup_namer...
Process finished with exit code 0
  1. lib/main.dart 中引入,如下所示:

    lib/main.dart
    import 'package:english_words/english_words.dart';
    import 'package:flutter/material.dart';

    在你输入时,Android Studio会为你提供有关库导入的建议。然后它将呈现灰色的导入字符串,让你知道导入的库截至目前尚未被使用。

  2. 接下来,我们使用 English words 包生成文本来替换字符串”Hello World”:

    {step1_base → step2_use_package}/lib/main.dart
    @@ -2,6 +2,7 @@
    2
    2
      // Use of this source code is governed by a BSD-style license that can be
    3
    3
      // found in the LICENSE file.
    4
    + import 'package:english_words/english_words.dart';
    4
    5
      import 'package:flutter/material.dart';
    5
    6
      void main() {
    @@ -13,14 +14,15 @@
    13
    14
      @override
    14
    15
      Widget build(BuildContext context) {
    16
    + final wordPair = WordPair.random();
    15
    17
      return MaterialApp(
    16
    18
      title: 'Welcome to Flutter',
    17
    19
      home: Scaffold(
    18
    20
      appBar: AppBar(
    19
    21
      title: const Text('Welcome to Flutter'),
    20
    22
      ),
    21
    - body: const Center(
    22
    - child: Text('Hello World'),
    23
    + body: Center(
    24
    + child: Text(wordPair.asPascalCase),
    23
    25
      ),
    24
    26
      ),
    25
    27
      );
  3. 如果应用程序正在运行,请使用热重载按钮 offline_bolt 更新正在运行的应用程序。每次单击热重载或保存项目时,都会在正在运行的应用程序中随机选择不同的单词对。这是因为单词对是在 build 方法内部生成的。每次 MaterialApp 需要渲染时或者在 Flutter Inspector 中切换平台时 build 都会运行。

    App at completion of second step on Windows
    Windows
    App at completion of second step on iOS
    iOS

遇到问题?

如果你的应用程序运行不正常,请查找是否有拼写错误。如果需要通过 Flutter 的 debug 工具,可以查看 开发者工具 页面来查看 debug 和 profile 的工具。如果需要,使用下面链接中的代码来对比更正。

第三步:添加一个 Stateful widget

状态的 widgets 是不可变的,这意味着它们的属性不能改变 —— 所有的值都是 final。

状态的 widgets 也是不可变的,但其持有的状态可能在 widget 生命周期中发生变化,实现一个有状态的 widget 至少需要两个类: 1)一个 StatefulWidget 类;2)一个 State 类,StatefulWidget 类本身是不变的,但是 State 类在 widget 生命周期中始终存在。

在这一步,你将添加一个有状态的 widget —— RandomWords,它会创建自己的状态类 —— _RandomWordsState,然后你需要将 RandomWords 内嵌到已有的无状态的 MyApp widget。

  1. 创建有状态 widget 的样板代码。
    lib/main.dart 中,将光标置于所有代码之后,输入 回车 几次另起新行。在 IDE 中,输入 stful,编辑器就会提示您是否要创建一个 Stateful widget。按回车键表示接受建议,随后就会出现两个类的样板代码,光标也会被定位在输入有状态 widget 的名称处。

  2. 输入 RandomWords 作为有状态 widget 的名称。
    RandomWords widget 的主要作用就是创建其对应的 State 类。

    输入 RandomWords 作为有有状态 widget 的名称后, IDE 会自动更新其对应的 State 类,并将其命名为 _RandomWordsState。默认情况下,State 类的名称带有下划线前缀。 Dart 语言中,给标识符加上下划线前缀可以 增强隐私性,并且这也是针对 State 对象推荐的最佳实践写法。

    IDE 也会自动将状态类继承自 State<RandomWords>,这表示专门用于 RandomWords 的通用 State 类。该应用程序的大多数逻辑都位于此处—它维护 RandomWords widget 的状态。该类会保存生成的单词对的列表,该列表随用户滚动而无限增长,在本实验的第 2 部分中,用户可以通过点击心形图标,添加或删除列表中收藏的单词对。

    这两个类现在都如下所示:

    class RandomWords extends StatefulWidget {
        const RandomWords({super.key});
    
        @override
        State<RandomWords> createState() => _RandomWordsState();
      }
    
      class _RandomWordsState extends State<RandomWords> {
        @override
        Widget build(BuildContext context) {
          return Container();
        }
      }
  3. 更新 _RandomWordsState 中的 build() 方法:

    lib/main.dart (_RandomWordsState)
    class _RandomWordsState extends State<RandomWords> {
        @override
        Widget build(BuildContext context) {
          final wordPair = WordPair.random();
          return Text(wordPair.asPascalCase);
        }
      }
  4. 通过以下差异所示的更改,删除 MyApp 中单词生成的代码:

    {step2_use_package → step3_stateful_widget}/lib/main.dart
    @@ -14,16 +14,15 @@
    14
    14
      @override
    15
    15
      Widget build(BuildContext context) {
    16
    - final wordPair = WordPair.random();
    17
    16
      return MaterialApp(
    18
    17
      title: 'Welcome to Flutter',
    19
    18
      home: Scaffold(
    20
    19
      appBar: AppBar(
    21
    20
      title: const Text('Welcome to Flutter'),
    22
    21
      ),
    23
    - body: Center(
    24
    - child: Text(wordPair.asPascalCase),
    22
    + body: const Center(
    23
    + child: RandomWords(),
    25
    24
      ),
    26
    25
      ),
    27
    26
      );
    28
    27
      }
  5. 重启应用。应用应该像之前一样运行,每次热重载或保存应用程序时都会显示一个单词对。

  6. 遇到问题?

    如果你的应用程序运行不正常,请查找是否有拼写错误。如果需要通过 Flutter 的 debug 工具,可以查看 开发者工具 页面来查看 debug 和 profile 的工具。如果需要,使用下面链接中的代码来对比更正。

第四步:创建一个无限滚动的 ListView

在该步骤中,您会拓展 _RandomWordsState 以生成并显示单词对列表。随着用户滚动,列表(显示在 ListView widget 中)将无限增长。 ListViewbuilder 工厂构造函数使您可以按需延迟构建列表视图。

  1. _RandomWordsState 类中添加一个 _suggestions 列表以保存建议的单词对,同时,添加一个 _biggerFont 变量来增大字体大小。

    lib/main.dart
    class _RandomWordsState extends State<RandomWords> {
      final _suggestions = <WordPair>[];
      final _biggerFont = const TextStyle(fontSize: 18);
      // ···
    }
  2. 接下来,我们将向 _RandomWordsState 类添加一个 _buildSuggestions() 方法,此方法构建显示建议单词对的 ListView

    ListView 类提供了一个名为 itemBuilder 的 builder 属性,这是一个工厂匿名回调函数,接受两个参数 BuildContext 和行迭代器 i。迭代器从 0 开始,每调用一次该函数 i 就会自增,每次建议的单词对都会让其递增两次,一次是 ListTile,另一次是 Divider。它用于创建一个在用户滚动时候无限增长的列表。

  3. 使用 ListView.builder 构造函数,从 _RandomWordsState 类的 build 方法中返回一个 ListView widget。

    lib/main.dart (itemBuilder)
    return ListView.builder(
      padding: const EdgeInsets.all(16.0),
      itemBuilder: /*1*/ (context, i) {
        if (i.isOdd) return const Divider(); /*2*/
    
        final index = i ~/ 2; /*3*/
        if (index >= _suggestions.length) {
          _suggestions.addAll(generateWordPairs().take(10)); /*4*/
        }
        return Text(_suggestions[index].asPascalCase);
      },
    );
    1. 对于每个建议的单词对都会调用一次 itemBuilder,然后将单词对添加到 ListTile 行中。在偶数行,该函数会为单词对添加一个 ListTile row,在奇数行,该函数会添加一个分割线的 widget,来分隔相邻的词对。注意,在小屏幕上,分割线可能较难辨别。

    2. ListView 里的每一行之前,添加一个 1 像素高的分隔线 widget。

    3. 语法 i ~/ 2 表示 i 除以 2,但返回值是整型(向下取整),比如 i 为:1, 2, 3, 4, 5 时,结果为 0, 1, 1, 2, 2,这个可以计算出 ListView 中减去分隔线后的实际单词对数量。

    4. 如果是建议列表中最后一个单词对,接着再生成 10 个单词对,然后添加到建议列表。

    ListView.builder 的构造函数会为每个单词对创建并显示一个 Text widget,下一步里,你将可以为每个单词对返回一个 ListTile widget,这可以让每一行的显示更漂亮。

  4. _RandomWordsState 类的 ListView.builder 里的 itemBuilder 属性体里,将 Text 替换为 ListTile widget:

    lib/main.dart (listTile)
    return ListTile(
      title: Text(
        _suggestions[index].asPascalCase,
        style: _biggerFont,
      ),
    );

    ListTile 是包含了文本以及前后位图标 widget 的一行。

  5. 更新 _RandomWordsStatebuild() 方法以使用 _buildSuggestions(),而不是直接调用单词生成库,代码更改后如下:

    lib/main.dart (build)
    @override
    Widget build(BuildContext context) {
      return ListView.builder(
        padding: const EdgeInsets.all(16.0),
        itemBuilder: /*1*/ (context, i) {
          if (i.isOdd) return const Divider(); /*2*/
    
          final index = i ~/ 2; /*3*/
          if (index >= _suggestions.length) {
            _suggestions.addAll(generateWordPairs().take(10)); /*4*/
          }
          return ListTile(
            title: Text(
              _suggestions[index].asPascalCase,
              style: _biggerFont,
            ),
          );
        },
      );
    }
  6. 更新 MyAppbuild() 方法,修改 title 的值来改变标题,修改 home 的值为 RandomWords widget。

    {step3_stateful_widget → step4_infinite_list}/lib/main.dart
    @@ -14,12 +14,12 @@
    14
    14
      @override
    15
    15
      Widget build(BuildContext context) {
    16
    16
      return MaterialApp(
    17
    - title: 'Welcome to Flutter',
    17
    + title: 'Startup Name Generator',
    18
    18
      home: Scaffold(
    19
    19
      appBar: AppBar(
    20
    - title: const Text('Welcome to Flutter'),
    20
    + title: const Text('Startup Name Generator'),
    21
    21
      ),
    22
    22
      body: const Center(
    23
    23
      child: RandomWords(),
    24
    24
      ),
    @@ -27,18 +27,36 @@
    27
    27
      );
    28
    28
      }
    29
    29
      }
    30
    - class RandomWords extends StatefulWidget {
    31
    - const RandomWords({super.key});
    30
    + class _RandomWordsState extends State<RandomWords> {
    31
    + final _suggestions = <WordPair>[];
    32
    + final _biggerFont = const TextStyle(fontSize: 18);
    32
    33
      @override
    33
    - State<RandomWords> createState() => _RandomWordsState();
    34
    + Widget build(BuildContext context) {
    35
    + return ListView.builder(
    36
    + padding: const EdgeInsets.all(16.0),
    37
    + itemBuilder: /*1*/ (context, i) {
    38
    + if (i.isOdd) return const Divider(); /*2*/
    39
    +
    40
    + final index = i ~/ 2; /*3*/
    41
    + if (index >= _suggestions.length) {
    42
    + _suggestions.addAll(generateWordPairs().take(10)); /*4*/
    43
    + }
    44
    + return ListTile(
    45
    + title: Text(
    46
    + _suggestions[index].asPascalCase,
    47
    + style: _biggerFont,
    48
    + ),
    49
    + );
    50
    + },
    51
    + );
    52
    + }
    34
    53
      }
    35
    - class _RandomWordsState extends State<RandomWords> {
    54
    + class RandomWords extends StatefulWidget {
    55
    + const RandomWords({super.key});
    56
    +
    36
    57
      @override
    37
    - Widget build(BuildContext context) {
    38
    - final wordPair = WordPair.random();
    39
    - return Text(wordPair.asPascalCase);
    40
    - }
    58
    + State<RandomWords> createState() => _RandomWordsState();
    41
    59
      }
  7. 重新启动你的项目工程应用,你应该看到一个单词对列表。尽可能地向下滚动,你将继续看到新的单词对。

    App at completion of fourth step on Windows
    Windows
    App at completion of fourth step on iOS
    iOS

遇到问题?

如果你的应用程序运行不正常,请查找是否有拼写错误。如果需要通过 Flutter 的 debug 工具,可以查看 开发者工具 页面来查看 debug 和 profile 的工具。如果需要,使用下面链接中的代码来对比更正。

以 profile 模式运行

截止目前文档所示内容,你的应用应该运行在调试 (debug) 模式中,这个模式意味着在更大的性能开销下实现了更快速的开发效率,比如热重载功能的启用,因此你可能要面临较差质量的动画效果。当你准备分析应用性能或要打包发布的时候,你可能需要 Flutter 的 profile 或者 release 构建,相关文档,请查阅文档: Flutter 的构建模式选择

下一步

The app from part 2
The app from part 2

祝贺你!

你已经完成了一个可以同时运行在 iOS、Android、Windows 和 Web 平台的 Flutter 应用!同时学习了如下内容:

  • 从零开始创建了一个 Flutter 应用;

  • 编写 Dart 代码;

  • 使用外部的第三方库(package);

  • 在开发过程中试用了热重载 (hot reload);

  • 实现了一个有状态的 widget;

  • 创建了一个懒加载的,无限滚动的列表。

如果你想继续扩展你的应用,在这里进行 第二部分,你将会从以下方面修改你的应用:

  • 为应用添加交互功能,一个能点击的小心心,来保存喜欢的公司名字;

  • 为应用添加一个新的页面(Route),查看收藏列表;

  • 修改应用的主题,变成一个白色系的应用。