编写第一个 Flutter 应用

The app that you'll be building

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

This is a guide to creating your first Flutter app. If you are familiar with object-oriented code and basic programming concepts such as variables, loops, and conditionals, you can complete this tutorial. You don’t need previous experience with Dart or mobile programming.

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

This guide is part 1 of a two-part codelab. You can find part 2 on Google Developers. Part 1 can also be found on Google Developers.

第一部分的内容概览

What you’ll build in part 1

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

You’ll implement a simple mobile app that generates proposed names for a startup company. The user can select and unselect names, saving the best ones. The code lazily generates names. As the user scrolls, more names are generated. There is no limit to how far a user can scroll.

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

The animated GIF shows how the app works at the completion of part 1.

第一步:创建初始化工程

Step 1: Create the starter Flutter app

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

Create a simple, templated Flutter app, using the instructions in Getting Started with your first Flutter app. Name the project startup_namer (instead of myapp).

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

In this codelab, you’ll mostly be editing lib/main.dart, where the Dart code lives.

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

    Replace the contents of lib/main.dart.
    Delete all of the code from lib/main.dart. Replace with the following code, which displays “Hello World” in the center of the screen.

    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(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Welcome to Flutter',
          home: Scaffold(
            appBar: AppBar(
              title: Text('Welcome to Flutter'),
            ),
            body: Center(
              child: Text('Hello World'),
            ),
          ),
        );
      }
    }
  2. 运行 你的工程项目,根据不同的操作系统,你会看到如下运行结果界面:

    Run the app in the way your IDE describes. You should see either Android or iOS output, depending on your device.

    Hello world app on Android
    Android
    Hello world app on iOS
    iOS

观察和分析

Observations

  • 本示例创建了一个具有 Material Design 风格的应用, Material 是一种移动端和网页端通用的视觉设计语言, Flutter 提供了丰富的 Material 风格的 widgets。

    This example creates a Material app. Material is a visual design language that is standard on mobile and the web. Flutter offers a rich set of Material widgets.

  • 主函数(main)使用了 (=>) 符号,这是 Dart 中单行函数或方法的简写。

    The main() method uses arrow (=>) notation. Use arrow notation for one-line functions or methods.

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

    The app extends StatelessWidget which makes the app itself a widget. In Flutter, almost everything is a widget, including alignment, padding, and layout.

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

    The Scaffold widget, from the Material library, provides a default app bar, title, and a body property that holds the widget tree for the home screen. The widget subtree can be quite complex.

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

    A widget’s main job is to provide a build() method that describes how to display the widget in terms of other, lower level widgets.

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

    The body for this example consists of a Center widget containing a Text child widget. The Center widget aligns its widget subtree to the center of the screen.

第二步:使用外部 package

Step 2: Use an external package

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

In this step, you’ll start using an open-source package named english_words, which contains a few thousand of the most used English words plus some utility functions.

你可以在 Pub site 上找到 english_words 软件包以及其他许多开源软件包。

You can find the english_words package, as well as many other open source packages, on the Pub site.

  1. pubspec 文件管理 Flutter 应用程序的 assets(资源,如图片、package等)。在pubspec.yaml 中,将 english_words(3.1.0或更高版本)添加到依赖项列表,如下面高亮显示的行:

    The pubspec file manages the assets and dependencies for a Flutter app. In pubspec.yaml, add english_words (3.1.0 or higher) to the dependencies list:

    {step1_base → step2_use_package}/pubspec.yaml
    @@ -5,4 +5,5 @@
    5
    5
    dependencies:
    6
    6
    flutter:
    7
    7
    sdk: flutter
    8
    8
    cupertino_icons: ^0.1.2
    9
    + english_words: ^3.1.0
  2. 在 Android Studio 的编辑器视图中查看 pubspec 时,单击右上角的 Packages get,这会将依赖包安装到你的项目。你可以在控制台中看到以下内容:

    While viewing the pubspec in Android Studio’s editor view, click Packages get. This pulls the package into your project. You should see the following in the console:

    $ flutter pub get
    Running "flutter pub get" in startup_namer...
    Process finished with exit code 0
    

    在执行 Packages get 命令的时候,同时会自动生成一个名为 pubspec.lock 的文件,这里包含了你依赖 packages 的名称和版本。

    Performing Packages get also auto-generates the pubspec.lock file with a list of all packages pulled into the project and their version numbers.

  3. lib/main.dart 中引入,如下所示:

    In lib/main.dart, import the new package:

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

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

    As you type, Android Studio gives you suggestions for libraries to import. It then renders the import string in gray, letting you know that the imported library is unused (so far).

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

    Use the English words package to generate the text instead of using the string “Hello World”:

    {step1_base → step2_use_package}/lib/main.dart
    @@ -9,6 +10,7 @@
    9
    10
    class MyApp extends StatelessWidget {
    10
    11
    @override
    11
    12
    Widget build(BuildContext context) {
    13
    + final wordPair = WordPair.random();
    12
    14
    return MaterialApp(
    13
    15
    title: 'Welcome to Flutter',
    14
    16
    home: Scaffold(
    @@ -16,7 +18,7 @@
    16
    18
    title: Text('Welcome to Flutter'),
    17
    19
    ),
    18
    20
    body: Center(
    19
    - child: Text('Hello World'),
    21
    + child: Text(wordPair.asPascalCase),
    20
    22
    ),
    21
    23
    ),
    22
    24
    );
  5. 如果应用程序正在运行,请使用热重载按钮 offline_bolt 更新正在运行的应用程序。每次单击热重载或保存项目时,都会在正在运行的应用程序中随机选择不同的单词对。这是因为单词对是在 build 方法内部生成的。每次 MaterialApp 需要渲染时或者在 Flutter Inspector 中切换平台时 build 都会运行。

    If the app is running, hot reload to update the running app. Each time you click hot reload, or save the project, you should see a different word pair, chosen at random, in the running app. This is because the word pairing is generated inside the build method, which is run each time the MaterialApp requires rendering, or when toggling the Platform in Flutter Inspector.

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

遇到问题?

Problems?

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

If your app is not running correctly, look for typos. If you want to try some of Flutter’s debugging tools, check out the DevTools suite of debugging and profiling tools. If needed, use the code at the following links to get back on track.

第三步:添加一个 Stateful widget

Step 3: Add a Stateful widget

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

Stateless widgets are immutable, meaning that their properties can’t change—all values are final.

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

Stateful widgets maintain state that might change during the lifetime of the widget. Implementing a stateful widget requires at least two classes: 1) a StatefulWidget class that creates an instance of 2) a State class. The StatefulWidget class is, itself, immutable, but the State class persists over the lifetime of the widget.

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

In this step, you’ll add a stateful widget, RandomWords, which creates its State class, RandomWordsState. You’ll then use RandomWords as a child inside the existing MyApp stateless widget.

  1. 创建一个最简的 state 类,这个类可以在任意地方创建而不一定非要在 MyApp 里,我们的示例代码是放在 MyApp 类的最下面了:

    Create a minimal state class. Add the following to the bottom of main.dart:

    lib/main.dart (RandomWordsState)
    class RandomWordsState extends State<RandomWords> {
      // TODO Add build() method
    }

    注意一下 State<RandomWords> 的声明。这表明我们在使用专门用于 RandomWordsState 泛型类。应用的大部分逻辑和状态都在这里 —— 它会维护 RandomWords widget 的状态。这个类会保存代码生成的单词对,这个单词对列表会随着用户滑动而无限增长,另外还会保存用户喜爱的单词对(第二部分),也即当用户点击爱心图标的时候会从喜爱的列表中添加或者移除当前单词对。

    Notice the declaration State<RandomWords>. This indicates that we’re using the generic State class specialized for use with RandomWords. Most of the app’s logic and state resides here—it maintains the state for the RandomWords widget. This class saves the generated word pairs, which grows infinitely as the user scrolls, and favorite word pairs (in part 2), as the user adds or removes them from the list by toggling the heart icon.

    RandomWordsState 依赖 RandomWords,我们接下来会创建这个类。

    RandomWordsState depends on the RandomWords class. You’ll add that next.

  2. 添加有状态的 RandomWords widget 到 main.dartRandomWords widget 除了创建 State 类之外几乎没有其他任何东西:

    Add the stateful RandomWords widget to main.dart. The RandomWords widget does little else beside creating its State class:

    lib/main.dart (RandomWords)
    class RandomWords extends StatefulWidget {
      @override
      RandomWordsState createState() => RandomWordsState();
    }

    在添加状态类后,IDE 会提示该类缺少 build 方法。接下来,你将添加一个基本的 build 方法,该方法通过将生成单词对的代码从 MyApp 移动到 RandomWordsState 来生成单词对。

    After adding the state class, the IDE complains that the class is missing a build method. Next, you’ll add a basic build method that generates the word pairs by moving the word generation code from MyApp to RandomWordsState.

  3. build() 方法添加到 RandomWordState 中,如下所示:

    Add the build() method to RandomWordsState:

    lib/main.dart (RandomWordsState)
    class RandomWordsState extends State<RandomWords> {
      @override
      Widget build(BuildContext context) {
        final wordPair = WordPair.random();
        return Text(wordPair.asPascalCase);
      }
    }
  4. 如下所示,删除 MyApp 里生成文字的代码:

    Remove the word generation code from MyApp by making the changes shown in the following diff:

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

    Restart the app. The app should behave as before, displaying a word pairing each time you hot reload or save the app.

遇到问题?

Problems?

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

If your app is not running correctly, look for typos. If you want to try some of Flutter’s debugging tools, check out the DevTools suite of debugging and profiling tools. If needed, use the code at the following link to get back on track.

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

Step 4: Create an infinite scrolling ListView

在这一步中,你将扩展(继承)RandomWordsState 类,以生成并显示单词对列表。当用户滚动时,ListView 中显示的列表将无限增长。 ListViewbuilder 工厂构造函数允许你按需建立一个懒加载的列表视图。

In this step, you’ll expand RandomWordsState to generate and display a list of word pairings. As the user scrolls, the list displayed in a ListView widget, grows infinitely. ListView’s builder factory constructor allows you to build a list view lazily, on demand.

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

    Add a _suggestions list to the RandomWordsState class for saving suggested word pairings. Also, add a _biggerFont variable for making the font size larger.

    lib/main.dart
    class RandomWordsState extends State<RandomWords> {
      final _suggestions = <WordPair>[];
      final _biggerFont = const TextStyle(fontSize: 18.0);
      // ···
    }

    接下来,我们将向 RandomWordsState 类添加一个 _buildSuggestions() 方法,此方法构建显示建议单词对的 ListView

    Next, you’ll add a _buildSuggestions() function to the RandomWordsState class. This method builds the ListView that displays the suggested word pairing.

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

    The ListView class provides a builder property, itemBuilder, that’s a factory builder and callback function specified as an anonymous function. Two parameters are passed to the function—the BuildContext, and the row iterator, i. The iterator begins at 0 and increments each time the function is called. It increments twice for every suggested word pairing: once for the ListTile, and once for the Divider. This model allows the suggested list to grow infinitely as the user scrolls.

  2. RandomWordsState 类添加 _buildSuggestions() 方法,内容如下:

    Add a _buildSuggestions() function to the RandomWordsState class:

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

      The itemBuilder callback is called once per suggested word pairing, and places each suggestion into a ListTile row. For even rows, the function adds a ListTile row for the word pairing. For odd rows, the function adds a Divider widget to visually separate the entries. Note that the divider might be difficult to see on smaller devices.

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

      Add a one-pixel-high divider widget before each row in the ListView.

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

      The expression i ~/ 2 divides i by 2 and returns an integer result. For example: 1, 2, 3, 4, 5 becomes 0, 1, 1, 2, 2. This calculates the actual number of word pairings in the ListView, minus the divider widgets.

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

      If you’ve reached the end of the available word pairings, then generate 10 more and add them to the suggestions list.

    对于每一个单词对,_buildSuggestions() 都会调用一次 _buildRow()。这个函数在 ListTile 中显示每个新词对,这使你在下一步中可以生成更漂亮的显示行,详见本 codelab 的第二部分。

    The _buildSuggestions() function calls _buildRow() once per word pair. This function displays each new pair in a ListTile, which allows you to make the rows more attractive in the next step.

  3. RandomWordsState 中添加 _buildRow() 函数 :

    Add a _buildRow() function to RandomWordsState:

    lib/main.dart (_buildRow)
    Widget _buildRow(WordPair pair) {
      return ListTile(
        title: Text(
          pair.asPascalCase,
          style: _biggerFont,
        ),
      );
    }
  4. 更新 RandomWordsState 的 build 方法以使用 _buildSuggestions(),而不是直接调用单词生成库,代码更改后如下:(使用 Scaffold 类实现基础的 Material Design 布局)

    In the RandomWordsState class, update the build() method to use _buildSuggestions(), rather than directly calling the word generation library. (Scaffold implements the basic Material Design visual layout.) Replace the method body with the highlighted code:

    lib/main.dart (build)
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Startup Name Generator'),
        ),
        body: _buildSuggestions(),
      );
    }
  5. 更新 MyAppbuild() 方法,修改 title 的值来改变标题,修改 home 的值为 RandomWords widget。

    In the MyApp class, update the build() method by changing the title, and changing the home to be a RandomWords widget:

    {step3_stateful_widget → step4_infinite_list}/lib/main.dart
    @@ -10,15 +10,8 @@
    10
    10
    class MyApp extends StatelessWidget {
    11
    11
    @override
    12
    12
    Widget build(BuildContext context) {
    13
    13
    return MaterialApp(
    14
    - title: 'Welcome to Flutter',
    15
    - home: Scaffold(
    14
    + title: 'Startup Name Generator',
    15
    + home: RandomWords(),
    16
    - appBar: AppBar(
    17
    - title: Text('Welcome to Flutter'),
    18
    - ),
    19
    - body: Center(
    20
    - child: RandomWords(),
    21
    - ),
    22
    - ),
    23
    16
    );
    24
    17
    }
  6. 重新启动你的项目工程应用,你应该看到一个单词对列表。尽可能地向下滚动,你将继续看到新的单词对。

    Restart the app. You should see a list of word pairings no matter how far you scroll.

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

遇到问题?

Problems?

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

If your app is not running correctly, look for typos. If you want to try some of Flutter’s debugging tools, check out the DevTools suite of debugging and profiling tools. If needed, use the code at the following link to get back on track.

以 profile 模式运行

Profile or release runs

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

The app you’ve run so far is built in debug mode, which allows faster development (for example, by supporting hot reload) at a big performance cost. Therefore, janky animations are expected in such mode. Once you are ready to analyze performance or release your app, you’ll want to use Flutter’s profile or release builds. For more information, see Flutter’s build modes.

下一步

Next steps

The app from part 2
The app from part 2

祝贺你!

Congratulations!

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

You’ve written an interactive Flutter app that runs on both iOS and Android. In this codelab, you’ve:

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

    Created a Flutter app from the ground up.

  • 编写 Dart 代码;

    Written Dart code.

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

    Leveraged an external, third-party library.

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

    Used hot reload for a faster development cycle.

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

    Implemented a stateful widget.

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

    Created a lazily loaded, infinite scrolling list.

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

If you would like to extend this app, proceed to part 2 on the Google Developers Codelabs site, where you add the following functionality:

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

    Implement interactivity by adding a clickable heart icon to save favorite pairings.

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

    Implement navigation to a new route by adding a new screen containing the saved favorites.

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

    Modify the theme color, making an all-white app.