Flutter 是 Google 用以帮助开发者在 iOS 和 Android 两个平台开发高质量原生 UI 的移动 SDK。Flutter 兼容现有的代码,免费且开源,在全球开发者中广泛被使用。

在这个 codelab 里,你将会着手创建一个简单的 Flutter 应用,无需 Dart 语言和移动开发语言经验,只需你具备面向对象语言开发基础(如变量,循环和条件语句)即可。

在第一部分,你可以了解到:

在本 codelab 的第二部分,你还将学到添加交互,修改应用的主题,为应用添加一个新的页面(在 Flutter,我们称之为 route

在第一部分,你将可以制作出:

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

下面这个 GIF 可以引导你预览本 codelab 做完之后的应用效果图:

你拥有怎样的移动应用开发经验?

我从未开发过移动应用 我只开发过基于移动网页的应用 我只开发过 Android 应用 我只开发过 iOS 应用 我同时开发 Android 和 iOS 应用 我开发移动 Web,Android 和 iOS 应用

你需要安装两部分来完成本次实验,Flutter 的 SDK编辑器(editor),这个 codelab 里,我们以 Android Studio 作为编辑器(editor),但你可以用个人更顺手的编辑器。

你可以通过如下任何设备完成本 codelab:

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

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

替换 lib/main.dart

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

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: const Text('Welcome to Flutter'),
        ),
        body: const Center(
          child: const Text('Hello World'),
        ),
      ),
    );
  }
}

运行你的工程项目,根据不同的操作系统,你会看到如下运行结果界面:

Android

iOS

观察和分析

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

你可以 在 pub.dartlang.org 上找到 english_words 软件包以及其他许多开源软件包。

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

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^0.1.0
  english_words: ^3.1.0   # 新增了这一行

在Android Studio 的编辑器视图中查看 pubspec 时,单击右上角的 Packages get,这会将依赖包安装到您的项目。您可以在控制台中看到以下内容:

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

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

import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';  // 新增了这一行

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

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

我们需要进行如下更改:

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

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final wordPair = new WordPair.random(); // 新增了这一行
    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('Welcome to Flutter'),
        ),
        body: new Center(    // 这里把之前的 "const" 换成了 "new".
          //child: const Text('Hello World'),   // 我们不用这样的方式生成文字了
          child: new Text(wordPair.asPascalCase),  // 这是新的文字生成方式
        ),
      ),
    );
  }
}

如果你没有把 Center 前面的修饰词从 const 改成 new 的话,系统就会报错,因为这个时候它的子对象已经不是常量,那就不能再用 const 了,所以这里 CenterText 都需要使用 new 创建新的实例。

如果应用程序正在运行,请使用热重载按钮 () 更新正在运行的应用程序。每次单击热重载或保存项目时,都会在正在运行的应用程序中随机选择不同的单词对。 这是因为单词对是在 build 方法内部生成的。每次 MaterialApp 需要渲染时或者在 Flutter Inspector 中切换平台时 build 都会运行.

Android

iOS

遇到问题?

如果您的应用程序运行不正常,请查找是否有拼写错误。如果需要,使用下面链接中的代码来对比更正。

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

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

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

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

class RandomWordsState extends State<RandomWords> {
  // TODO Add build method
}

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

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

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

class RandomWords extends StatefulWidget {
  @override
  RandomWordsState createState() => new RandomWordsState();
}

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

build 方法添加到 RandomWordState 中,如下所示:

class RandomWordsState extends State<RandomWords> {
  @override                                  // 新增代码片段 - 开始 ... 
  Widget build(BuildContext context) {
    final WordPair wordPair = new WordPair.random();
    return new Text(wordPair.asPascalCase);
  }                                          // ... 新增的代码片段 - 结束
}

如下所示,删除 MyApp 里生成文字的代码:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final WordPair wordPair = new WordPair.random();  // 删掉本行
    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('Welcome to Flutter'),
        ),
        body: new Center(
          //child: new Text(wordPair.asPascalCase), // 修改本行内容 
          child: new RandomWords(),                 // 修改成本行代码
        ),
      ),
    );
  }
}

热重载(Hot reload)当前的工程,应用应该像之前一样运行,每次热重载或保存应用程序时都会显示一个单词对。

遇到问题?

如果您的应用程序运行不正常,可以使用下面链接中的代码来对比更正。

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

向 RandomWordsState 类中添加一个 _suggestions 列表以保存建议的单词对,同时,添加一个 biggerFont 变量来增大字体大小 Also, add a _biggerFont variable for making the font size larger.

class RandomWordsState extends State<RandomWords> {
  // 添加如下两行
  final List<WordPair> _suggestions = <WordPair>[];
  final TextStyle _biggerFont = const TextStyle(fontSize: 18.0); 
  ...
}

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

ListView 类提供了一个 builder 属性,itemBuilder 值是一个匿名回调函数, 接受两个参数- BuildContext 和行迭代器 i。迭代器从 0 开始, 每调用一次该函数,i 就会自增 1,对于每个建议的单词对都会执行一次。该模型允许建议的单词对列表在用户滚动时无限增长。

RandomWordsState 类添加 _buildSuggestions() 函数,内容如下:

  Widget _buildSuggestions() {
    return new ListView.builder(
      padding: const EdgeInsets.all(16.0),

      // 对于每个建议的单词对都会调用一次 itemBuilder,
      // 然后将单词对添加到 ListTile 行中
      // 在偶数行,该函数会为单词对添加一个 ListTile row.
      // 在奇数行,该函数会添加一个分割线的 widget,来分隔相邻的词对。
      // 注意,在小屏幕上,分割线看起来可能比较吃力。

      itemBuilder: (BuildContext _context, int i) {
        // 在每一列之前,添加一个1像素高的分隔线widget
        if (i.isOdd) {
          return new Divider();
        }

        // 语法 "i ~/ 2" 表示i除以2,但返回值是整形(向下取整)
        // 比如 i 为:1, 2, 3, 4, 5 时,结果为 0, 1, 1, 2, 2,
        // 这可以计算出 ListView 中减去分隔线后的实际单词对数量
        final int index = i ~/ 2;
        // 如果是建议列表中最后一个单词对
        if (index >= _suggestions.length) {
        // ...接着再生成10个单词对,然后添加到建议列表
          _suggestions.addAll(generateWordPairs().take(10));
        }
        return _buildRow(_suggestions[index]);
      }
    );
  }

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

RandomWordsState 中添加 _buildRow 函数 :

  Widget _buildRow(WordPair pair) {
    return new ListTile(
      title: new Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
    );
  }

更新 RandomWordsStatebuild 方法以使用 _buildSuggestions(),而不是直接调用单词生成库,代码更改后如下:(使用 Scaffold 类实现基础的 Material Design 布局)

  @override
  Widget build(BuildContext context) {
    //final wordPair = new WordPair.random(); // 删掉 ... 
    //return new Text(wordPair.asPascalCase); // ... 这两行

    return new Scaffold (                   // 代码从这里... 
      appBar: new AppBar(
        title: new Text('Startup Name Generator'),
      ),
      body: _buildSuggestions(),
    );                                      // ... 添加到这里
  }

build() 方法添加到 MyApp 类中,如下所示:

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Startup Name Generator',
      home: new RandomWords(),
    );
  }

重新启动你的项目工程应用,你应该看到一个单词对列表。尽可能地向下滚动,你将继续看到新的单词对。

Android

iOS

遇到问题?

如果你的应用没有正常运行,你可以使用一下链接中的代码对比更正。

祝贺你!

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

第二部分结束之后,你的应用应该看起来是这个样子的:

了解更多 Flutter SDK:

阅读更多

资源清单:

请在我们的邮件列表与我们保持联系,我们期待听到你的反馈!

感谢

特别感谢来自 FlutterChina.club 的 Du Wen 提供的翻译作为基础。

感谢来自社区的葛佳恒、Lu Cheng 的翻译。

感谢 hushicai, fuyunling 对本文的翻译提出指正。