编写第一个Flutter App(翻译)
博客搬迁至https://blog.wangjiegulu.com
RSS订阅:https://blog.wangjiegulu.com/feed.xml
以下代码 Github 地址:https://github.com/wangjiegulu/flutter_test_01
编写你的第一个Flutter App
这个你创建第一个Flutter app的指南。如果你熟悉面向对象的代码,基本的编程概念,比如变量,循环,和条件,你就可以完成本教程。你不需要之前有Dart或者手机的编程经验。
- 第1步:创建启动 Flutter app
- 第2步:使用外部包
- 第3步:增加一个 Stateful widget
- 第4步:创建一个无限滚动的 ListView
- 第5步:增加交互
- 第6步:导航到一个新的页面
- 第7步:使用 Theme 来改变 UI
- 干得不错!
你将构建什么
你将要实现一个简单的手机 app,为一个初创公司去生成一些推荐的名字。用户可以选择和取消选择这些名字,并保存最好的一些名字。代码一次生成10个名字。当用户滚动时,新的一批名字就会被生成。用户可以点击 app bar 右上角的按钮进入一个新的页面来仅展示被喜欢的名字。
Gif 动图展示了 app 完成之后的运行效果。
你将学到什么
- Flutter app 的基础结构。
- 查询和使用包来扩展特性。
- 使用热重载来实现快速的开发周期。
- 怎么去实现一个 stateful widget 。
- 怎么去创建一个无限,懒加载的列表。
- 怎么去创建和导航到第二个页面。
- 怎么去使用 Theme 来改变 app 的外观。
你将使用什么
- Flutter SDK:Flutter SDK 包括 Flutter 的引擎,framework, widget ,工具和 Dart SDK。这个 codelab 需要 v0.1.4 或者更新。
- Android Studio IDE:这个 codelab 具备 Android Studio IDE,但是你也可以使用其它的 IDE,或者使用命令行工作。
- 你的 IDE 插件:你的 IDE 上面必须分别安装 Flutter 和 Dart 插件。除了 Android Studio,Flutter 和 Dart 插件在 VS Code 和 IntelliJ IDE。
根据 开始你的第一个 Flutter app 的介绍,创建一个简单,模版的 Flutter app。给项目取名为 startup_namer (替换掉 myapp)。您将修改这个 app 来创建完成的 app。
在这个 codelab 中,你主要编辑 dart 代码存放处的 lib/main.dart。
提示:当复制代码到你的 app 中,缩进可能会歪斜。你可以使用 Flutter 工具来自动修正它们:
- Android Studio / IntelliJ IDEA: 在 dart 代码上右键并选择 Reformat Code with dartfmt。
- VS Code: 右键并选择 Format Document。
- Ternimal: 运行 flutter format
。
-
替换 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: new Text('Welcome to Flutter'), ), body: new Center( child: new Text('Hello World'), ), ), ); } }
-
运行App,你将会看到如下的屏幕
观察
- 这个例子创建了一个 Material app。Material 是手机和 web 上的标准的设计语言。Flutter 提供了丰富的 Material widget 。
- main 方法制定了一个宽箭头(
=>
)标志,这是一行函数或者方法的简写。 - App 继承了 StatelessWidget,这使得 app 本身称为了一个 widget。在 Flutter 中,几乎所有一切都是 widget,包括 alignment, padding, 和 layout。
- Material 库中的 Scaffold,提供了一个默认的 app bar,title,和一个 body 属性,它持有了主页面的 widget 树。widget 的子树可能相当复杂。
- Widget 的主要的工作是提供一个
build()
方法,它描述了如何根据其他较低级别的 widget 显示 widget。 - 这个例子中的 widget 树的构成是一个中心的 widget 包含了一个文本的子 child widget。中心 widget 将它的 widget 子树对齐到屏幕的中心。
## 第2步:使用外部包
在这一步,我将使用一个名为 english_words 的开源包,它包含了几千个最常用的英文单词和常用的工具方法。
在 pub.dartlang.org,你可以找到 english_words,以及很多其它的开源包。
-
pubspec 文件为 Flutter app 管理 assets。在 pubspec.yaml,增加 english_words (3.1.0或者更高) 到依赖列表。新增行在下面已被高亮:
dependencies: flutter: sdk: flutter cupertino_icons: ^0.1.0 english_words: ^3.1.0
-
在 Android Studio’s editor 视图查看 pubspec,点击右上角的 Packages get。这会把包拉取到你的项目中。你会在控制台上看到以下信息:
flutter packages get Running "flutter packages get" in startup_namer... Process finished with exit code 0
-
在 lib/main.dart,增加一个
english_words
的导入,就如高亮展示的那样:import 'package:flutter/material.dart'; import 'package:english_words/english_words.dart';
由于你的输入,Android Studio 针对库会给你一些导入的建议。然后将导入字符串呈现为灰色,让你知道倒入的库你没有使用它(目前为止)。
-
使用 English words 包生成文本,用来替换掉之前的 "Hello World" 字符串。
提示:"Pascal case" (也称为 “大驼峰式命名法”),表示字符串中的每个单词,包括第一个单词,首字母大写。所以,“uppercamelcase” 就变成 “UpperCamelCase”。
做以下改变,如下面高亮处:
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( //child: new Text('Hello World'), // Replace the highlighted text... child: new Text(wordPair.asPascalCase), // With this highlighted text. ), ), ); } }
-
如果 app 正在运行中,使用热重载按钮()来更新运行中的 app。每一次你点击了热重载,或者保存了项目,你将会看见不同的词对,它在运行的 app 中是随机的。这是因为词对在 build 方法中被生成。在每次 MaterialApp 需要渲染或者在 Flutter Inspector 中切换平台的时候。
问题?
如果你的 app 没有正确运行,排查错误。如果需要,请使用以下链接的代码来追踪。
- pubspec.yaml (pubspec.yaml 不会再次修改了。)
- lib/main.dart
## 第3步:增加一个 Stateful widget
Stateless widget 是不可改变的,意味着它们的属性不能被修改 —— 所有值都是 final 的。
Stateful widgets 维护了状态,它可能会在 widget 的生命周期内被修改。实现一个 statful widget 需要两个类:1)一个 StatefulWidget 类,用来创建一个实例 2)一个 State 类。StatefulWidget 类本身是不可变的,但 State 类在整个 widget 的生命周期中保持不变。
在这一步中,你将会增加一个 stateful widget,RandomWords,增加它的 State class,RandomWordsState。State 类中将最终维护这个 widget 中推荐喜欢的词对。
-
增加 stateful RandomWords widget 到你的 main.dart 中。它可以被放在任何地方,甚至 MyApp 之外,但是这里的解决方案放在了文件的底部。RandomWords widget 除了创建它的 State 类没有什么特别的。
class RandomWords extends StatefulWidget { @override createState() => new RandomWordsState(); }
-
增加 RandomWordsState 类。app 的大部分代码将会写在这个类中,它维护拉这个 widget 中的 state。这个类会保存生成的词对,会被用户无限滚动,用户通过列表切换中的心图标来添加或删除它们。
你将逐步编写这个类。作为开始,通过以下高亮的文本来创建一个最小的 class:
class RandomWordsState extends State<RandomWords> { }
-
在增加了 state class 之后,IDE 警告这个类缺少一个 build 方法。然后,你将增加一个基本的 build 方法通过从 MyApp 转移生成词对的代码到 RandomWordsState 来生成词对:
class RandomWordsState extends State<RandomWords> { @override Widget build(BuildContext context) { final wordPair = new WordPair.random(); return new Text(wordPair.asPascalCase); } }
-
通过以下高亮改变,从 MyApp 中移除生成词对的代码:
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { final wordPair = new WordPair.random(); // Delete this line 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), // Change the highlighted text to... child: new RandomWords(), // ... this highlighted text ), ), ); } }
重启 app,如果你尝试去热重载,你可能会看到一个警告:
Reloading...
Not all changed program elements ran during view reassembly; consider
restarting.
这可能是误报,但考虑重新启动以确保你的更改反映在 app UI 中。
app 应该会跟以前一样,每次你热重载或者保存的时候展示一个词对。
问题?
如果你的 app 没有正确运行,排查错误。如果需要,请使用以下链接的代码来追踪。
## 第4步:创建一个无限滚动的 ListView
在这一步,你将扩展 RandomWordsState 来生成和展示一个列表的词对。当用户滚动时,展示在 ListView widget 的列表会无限滚动。ListView 的 builder
factory 构造方法允许你根据需要实现懒加载。
-
在 RandomWordsState 类中增加一个
_suggestions
list 来保存推荐的词对。注意变量以下划线(_
)开头。在 Dart 语言中,以下划线作为前缀标志代表私有。也增加一个
biggerFont
变量来使字体变大。class RandomWordsState extends State<RandomWords> { final _suggestions = <WordPair>[]; final _biggerFont = const TextStyle(fontSize: 18.0); ... }
-
在 RandomWordsState 类中增加一个
_buildSuggestions()
方法。这个方法构建展示词对的 ListView。ListView 类提供了一个 builder 属性,
itemBuilder
,以匿名方法的方式指定一个工厂构造器和回调方法。两个参数会被传入到方法中 —— BuildContext,和行迭代器,i
。迭代器从0开始,每一次方法被调用时递增,每个推荐词对配对一次。这个模型允许在用户滚动时推荐列表无限滚动。增加以下高亮行:
class RandomWordsState extends State<RandomWords> { ... Widget _buildSuggestions() { return new ListView.builder( padding: const EdgeInsets.all(16.0), // 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 may be difficult // to see on smaller devices. itemBuilder: (context, i) { // Add a one-pixel-high divider widget before each row in theListView. if (i.isOdd) return new Divider(); // The syntax "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. final index = i ~/ 2; // If you've reached the end of the available word pairings... if (index >= _suggestions.length) { // ...then generate 10 more and add them to the suggestions list. _suggestions.addAll(generateWordPairs().take(10)); } return _buildRow(_suggestions[index]); } ); } }
-
_buildSuggestions
方法在每个词配对时调用。这个方法在一个 ListTile 中展示一个新的配对,在下一步中它允许你在行中增加交互。在
RandomWordsState
中增加一个_buildRow
方法:class RandomWordsState extends State<RandomWords> { ... Widget _buildRow(WordPair pair) { return new ListTile( title: new Text( pair.asPascalCase, style: _biggerFont, ), ); } }
-
使用
_buildSuggestions()
来更新 RandomWordsState 的 build 方法,而不是直接调用生成词对的库。修改以下高亮改变:class RandomWordsState extends State<RandomWords> { ... @override Widget build(BuildContext context) { final wordPair = new WordPair.random(); // Delete these two lines. Return new Text(wordPair.asPascalCase); return new Scaffold ( appBar: new AppBar( title: new Text('Startup Name Generator'), ), body: _buildSuggestions(), ); } ... }
-
更新 MyApp 的 build 方法。在 MyApp 中移除 Scaffold 和 AppBar 实例。这些应该由 RandomWordsState 去管理,这让在下一步中导航到另一个页面时修改 app bar 的名字更简单。
用下面高亮的 build 方法替换原生的方法:
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Startup Name Generator', home: new RandomWords(), ); } }
重启 app,你将看到一个词对列表。按你想要的去滚动列表,你会看到新的词对。
问题?
如果你的 app 没有正确运行,排查错误。如果需要,请使用以下链接的代码来追踪。
## 第5步:增加交互
在这一步,你将在没行增加一个可点击的心型图标。当用户点击 list 中的每行时,切换它的 “喜欢” 状态,这会触发词对在保存的集合中增加或者删除。
-
在 RandomWordsState 中增加一个
_saved
集合。这个集合存储了用户喜欢了的词对。集合首选 List,因为正确的实现是 Set 不允许重复的条目。class RandomWordsState extends State<RandomWords> { final _suggestions = <WordPair>[]; final _saved = new Set<WordPair>(); final _biggerFont = const TextStyle(fontSize: 18.0); ... }
-
在
_buildRow
方法中,增加一个alreadySaved
检查来确保词对是否已经添加到喜欢集合中了。Widget _buildRow(WordPair pair) { final alreadySaved = _saved.contains(pair); ... }
-
在
_buildRow()
,增加一个心型的图标到 ListTile 来启用喜欢状态。稍后,你会在这个心型图标上增加一个交互。增加以下高亮:
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, ), ); }
-
重启 app,现在你会看到每行都有心型图标,但是它们还不能交互。
-
在
_buildRow
方法中让心形图标可点击。如果一个词对已经被添加到喜欢集合,再次点击会从喜欢集合中删除。当心形图标被点击,调用setState()
方法来通知系统状态被改变。增加高亮行:
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: () { setState(() { if (alreadySaved) { _saved.remove(pair); } else { _saved.add(pair); } }); }, ); }
提示:在 Flutter 响应式风格框架中,调用 setState() 触发 State 对象的 build() 方法的调用,结果更新在 UI 中。
热重载 app,你应该会看到点击任意行来喜欢,取消喜欢条目。注意,点击一行会生成从心型图标发出的隐式墨迹飞溅动画。
问题?
如果你的 app 没有正确运行,排查错误。如果需要,请使用以下链接的代码来追踪。