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

在本 codelab 里,你将在一个基础的 Flutter 应用里加入交互功能,同时创建第二个页面(Flutter 里称之为 route)用户可以进入这个页面。最终,你将可以修改应用的主题(配色)。这是"创建你的第一个 Flutter 应用"第一篇的"续集",如果你希望从本篇开始也没问题,我们提供了初始化工程代码。

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

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

完成一个简单的移动应用程序,功能是:为一个创业公司生成建议的名称。用户可以选择和取消选择的名称、保存(收藏)喜欢的名称。该代码一次生成十个名称,当用户滚动时,会生成一新批名称。用户可以点击导航栏右边的列表图标,以打开到仅列出收藏名称的新页面(route)。

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

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

1. 我从未开发过移动应用 2. 我只开发过基于移动网页的应用 3. 我只开发过 Android 应用 4. 我只开发过 iOS 应用 5. 我同时开发 Android 和 iOS 应用 6. 我开发移动 Web,Android 和 iOS 应用

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

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

如果你已经实践了第一部分:编写你的第一个 Flutter App [1/2],则可以直接跳过本篇,直接进行下一部分。

如果你尚未开始,别怕,请照着下面的步骤来一遍:

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

删除这个文件里的所有内容 lib/main.dart,替换为这个文件的内容,这块代码实现了一个瀑布流似的文字列表,文字列表是一些初创公司的名字。

更新 pubspec.yaml 文件的内容,加入 English Words 这个 package:

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^0.1.0
  english_words: ^3.1.0    // 加入这一行...

这个 English Words package 的会自动生成一些随机的英文单词,我们这里假定这些是用于初创公司名字的。

在 Android Studio 的编辑器视图修改 pubspec 文件的时候,点击右上角的 Packages get,这将帮助你下载这些 package 到你的项目里,终端(console)里应该可以看到如下的内容:

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

运行你的工程,随意下滑查看这个列表,这里都是给你提供的初创公司名字喔!

在这部分,我们将为每一行添加一个心形的(收藏)图标,下一步你将能够为这个图标加入点击收藏的功能。

添加一个 _saved Set(集合)到 RandomWordsState,这个集合存储用户喜欢(收藏)的单词对。 在这里,Set 比 List 更合适,因为 Set 中不允许重复的值。

class RandomWordsState extends State<RandomWords> {
  final List<WordPair> _suggestions = <WordPair>[];
  final Set<WordPair> _saved = new Set<WordPair>();   // 新增本行
  final TextStyle _biggerFont = const TextStyle(fontSize: 18.0);
  ...
}

_buildRow 方法中添加 alreadySaved 来检查确保单词对还没有添加到收藏夹中。

Widget _buildRow(WordPair pair) {
  final bool alreadySaved = _saved.contains(pair);  // 新增本行
  ...
}

同时在 _buildRow() 中, 添加一个心形 ❤️图标到 ListTiles以启用收藏功能。接下来,你就可以给心形 ❤️图标添加交互能力了。

向列表添加图标,如下所示:

Widget _buildRow(WordPair pair) {
  final bool alreadySaved = _saved.contains(pair);
  return new ListTile(
    title: new Text(
      pair.asPascalCase,
      style: _biggerFont,
    ),
    trailing: new Icon(   // 新增代码开始 ...
      alreadySaved ? Icons.favorite : Icons.favorite_border,
      color: alreadySaved ? Colors.red : null,
    ),                    // ... 新增代码结束
  );
}

热重载应用,你现在可以在每一行看到心形 ❤️图标️,但它们还没有交互。

Android

iOS

遇到问题了?

如果您的应用没有正常运行,请查看下面链接处的代码,对比更正。

在这部分,我们将为刚刚的心形 ❤️图标增加交互,当用户点击列表中的条目,切换其"收藏"状态,并将该词对添加到或移除出"收藏夹"。

为了做到这个,我们在 _buildRow 中让心形 ❤️图标变得可以点击。如果单词条目已经添加到收藏夹中, 再次点击它将其从收藏夹中删除。当心形 ❤️图标被点击时,函数调用 setState() 通知框架状态已经改变。

增加 onTap 方法,如下所示:

Widget _buildRow(WordPair pair) {
  final alreadySaved = _saved.contains(pair);
  return new ListTile(
    title: new Text(
      pair.asPascalCase,
      style: _biggerFont,
    ),
    trailing: new Icon(
      alreadySaved ? Icons.favorite : Icons.favorite_border,
      color: alreadySaved ? Colors.red : null,
    ),
    onTap: () {      // 增加如下 9 行代码...
      setState(() {
        if (alreadySaved) {
          _saved.remove(pair);
        } else { 
          _saved.add(pair); 
        } 
      });
    },               // ... 一直到这里
  );
}

热重载应用,你就可以点击任何一行测试收藏或取消收藏功能,你的点击同时自带 Material Design 里的水波动画特效。

Android

iOS

遇到问题了?

如果您的应用没有正常运行,请查看下面链接处的代码,对比更正。

在这一步中,您将添加一个显示收藏夹内容的新页面(在 Flutter 中称为路由[route])。您将学习如何在主路由和新路由之间导航(切换页面)。

在 Flutter 中,导航器管理应用程序的路由栈。将路由推入(push)到导航器的栈中,将会显示更新为该路由页面。 从导航器的栈中弹出(pop)路由,将显示返回到前一个路由。

接下来,我们在 RandomWordsState 的 build 方法中为 AppBar 添加一个列表图标。当用户点击列表图标时,包含收藏夹的新路由页面入栈显示。

将该图标及其相应的操作添加到 build 方法中:

class RandomWordsState extends State<RandomWords> {
  ...
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Startup Name Generator'),
        actions: <Widget>[      // 新增代码开始 ...
          new IconButton(icon: const Icon(Icons.list), onPressed: _pushSaved),
        ],                      // ... 代码新增结束
      ),
      body: _buildSuggestions(),
    );
  }
  ...
}

RandomWordsState 这个类里添加 _pushSaved() 方法:

class RandomWordsState extends State<RandomWords> {
  ...
  // 新增代码开始
  void _pushSaved() {
  }
  // 新增代码结束 
}

热重载应用,列表图标()将会出现在导航栏中。现在点击它不会有任何反应,因为 _pushSaved 函数还是空的。

接下来,(当用户点击导航栏中的列表图标时)我们会建立一个路由并将其推入到导航管理器栈中。此操作会切换页面以显示新路由,新页面的内容会在 MaterialPageRoute 的 builder 属性中构建,builder 是一个匿名函数。

添加 Navigator.push 调用,这会使路由入栈(以后路由入栈均指推入到导航管理器的栈)

void _pushSaved() {
  Navigator.of(context).push(
  );
}

接下来,添加 MaterialPageRoute 及其 builder。 现在,添加生成 ListTile 行的代码,ListTile 的 divideTiles() 方法在每个 ListTile 之间添加 1 像素的分割线。 该 divided 变量持有最终的列表项,并通过 toList()方法非常方便的转换成列表显示。

添加如下所示的代码:

void _pushSaved() {
  Navigator.of(context).push(
    new MaterialPageRoute<void>(   // 新增如下20行代码 ...
      builder: (BuildContext context) {
        final Iterable<ListTile> tiles = _saved.map(
          (WordPair pair) {
            return new ListTile(
              title: new Text(
                pair.asPascalCase,
                style: _biggerFont,
              ),
            );
          },
        );
        final List<Widget> divided = ListTile
          .divideTiles(
            context: context,
            tiles: tiles,
          )
          .toList();
      },
    ),                           // ... 新增代码结束
  );
}

builder 返回一个 Scaffold,其中包含名为"Saved Suggestions"的新路由的应用栏。新路由的body 由包含 ListTiles 行的 ListView 组成;每行之间通过一个分隔线分隔。

添加水平分隔符,如下代码所示:

void _pushSaved() {
  Navigator.of(context).push(
    new MaterialPageRoute<void>(
      builder: (BuildContext context) {
        final Iterable<ListTile> tiles = _saved.map(
          (WordPair pair) {
            return new ListTile(
              title: new Text(
                pair.asPascalCase,
                style: _biggerFont,
              ),
            );
          },
        );
        final List<Widget> divided = ListTile
          .divideTiles(
            context: context,
            tiles: tiles,
          )
              .toList();

        return new Scaffold(         // 新增 6 行代码开始 ...
          appBar: new AppBar(
            title: const Text('Saved Suggestions'),
          ),
          body: new ListView(children: divided),
        );                           // ... 新增代码段结束.
      },
    ),
  );
}

热重载应用程序,点击列表项收藏一些项,点击列表图标(),在新的 route(路由)页面中显示收藏的内容。Navigator(导航器)会在应用栏中自动添加一个"返回"按钮,无需调用Navigator.pop,点击后退按钮就会返回到主页路由。

iOS - Main route

iOS - Saved suggestions route

遇到问题了?

如果您的应用没有正常运行,请查看下面链接处的代码,对比更正。

这一部分,我们将会一起修改应用的主题。Flutter 里我们使用 theme 来控制你应用的外观和风格,你可以使用默认主题,该主题取决于物理设备或模拟器,也可以自定义主题以适应您的品牌。

你可以通过配置 ThemeData 类轻松更改应用程序的主题,目前我们的应用程序使用默认主题,下面将更改 primaryColor 颜色为白色。

在 MyApp 这个类里修改颜色:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Startup Name Generator',
      theme: new ThemeData(          // 新增代码开始... 
        primaryColor: Colors.white,
      ),                             // ... 代码新增结束
      home: new RandomWords(),
    );
  }
}

热重载应用。 你会发现,整个背景将会变为白色,包括 app bar(应用栏)。

一个小练习,你可以看一下 ThemeData 的文档,添加其他属性来更多改变 UI 样式。Material library 中的 Colors 类提供了许多可以使用的颜色常量, 你可以使用热重载来快速简单地尝试、实验。

Android

iOS

遇到问题了?

如果您的应用没有正常运行,请查看下面链接处的代码,对比更正。

真棒,你完成了一个可运行在 Android 和 iOS 系统上的、包含交互的 Flutter 应用,在这个 codelab 里,你已经做了下面的事情:

了解更多 Flutter SDK:

资源清单:

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

感谢

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

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