编写第一个 Flutter App (二)

原文地址:
https://codelabs.flutter-io.cn/codelabs/first-flutter-app-pt2-cn/index.html#0
https://codelabs.flutter-io.cn

1. 介绍

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

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

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

  • Flutter 可以在 Android 和 iOS 系统里自动适应不同的 UI 体系;
  • 使用热重载(hot reload)加速你的开发进程;
  • 在 stateful widget 上添加交互;
  • 导航到第二个页面;
  • 修改应用的主题。

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

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

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

2. 设定 Flutter 开发环境

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

提醒: 中国的开发者们,请先看一下这篇文档,查看是否需要对网络环境进行特别设置。

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

3. 创建初始化工程

如果你已经实践了第一部分:编写你的第一个 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

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

4. 向列表里添加图标

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

添加一个 _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

遇到问题了?

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

5. 添加交互

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

为了做到这个,我们在 _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); 
        } 
      });
    },               // ... 一直到这里
  );
}

提示: 在 Flutter 的响应式风格的框架中,调用 setState() 会为 State 对象触发 build() 方法,从而导致对 UI 的更新

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

Android iOS

遇到问题了?

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

6. 导航到新页面

在这一步中,您将添加一个显示收藏夹内容的新页面(在 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(),
    );
  }
  ...
}

提示: 某些 widget 属性需要单个 widget(child),而其它一些属性,如 action,需要一组widgets(children),用方括号 [] 表示。

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

遇到问题了?

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

7. 使用 Themes 修改 UI

这一部分,我们将会一起修改应用的主题。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 类提供了许多可以使用的颜色常量, 你可以使用热重载来快速简单地尝试、实验。

遇到问题了?

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

8. 完成啦!

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

写了 Dart 代码
使用热重载加速了开发进程
实现了一个 stateful widget,为你的应用加入了交互功能
创建了一个新的页面(route),为主页和这个新页面的跳转加入了逻辑
学会了如何使用 themes 修改应用的 UI

9. 扩展阅读

了解更多 Flutter SDK:

资源清单:

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

感谢

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容