创建你的第一个Flutter APP
本文指引你创建你的第一个Flutter APP。如果你熟悉面向对象编程和基本的编程的概念,
如变量,循环,条件等,那你就能完全理解本文。你不需要关于 Dart 和手机编程方面的经验。
- Step 1: 创建并启动Flutter APP
- Step 2: 使用扩展包
- Step 3: 添加一个带状态的部件
- Step 4: 创建一个无限滚动的ListView
- Step 5: 加入交互元素
- Step 6: 导航到新窗口
- Step 7: 使用全新的主题
- 圆满结束
APP功能
您将实现一个简单的移动应用程序,为初创公司推荐名称,用户可以选择和取消选择名称,然后保存最好的名称,代码一次生成10个名称,
当用户滚动界面时,会生成一组新的名称。用户可以点击顶栏右上角的列表图标,会转换到只显示红心名称的界面。
下面的最终效果图显示了APP的最终功能
将要学习的内容:
– Flutter APP的基础结构
– 查找并使用包来扩展功能
– 使用热加载来缩小开发周期
– 如何实现一个带有状态的组件
– 如何创建一个无限的、延迟加载的列表
– 如何创建并导航到另一个界面
– 如何使用 Themes 改变应用程序的外观
将要使用的东西:
– Flutter SDK: 包含Flutter的引擎,框架,组件,工具,和 Dart SDK。它的版本需要 v0.1.4 或更高
– Android Studio IDE: 使用它来写代码,但是也可以使用其他IDE,或者直接使用命令行工具
– IDE 插件: Flutter和Dart插件必须安装到IDE中,除了Android Studio之外,这两个插件也可以在VS Code和IntelliJ IDEs中
关于如何搭建环境,请参阅Flutter Installation and Setup
Step 1: 创建并启动Flutter APP
通过参阅Getting Started with your first Flutter app来创建一个简单的,模板化的APP,
项目命名为 startup_namer (代替 myapp)。通过修改这个简单的APP来完善你的APP。
在开发APP时,你主要修改的是 lib/main.dart 中的代码,这个文件中是 Dart 语言的代码。
注:当写APP的代码时,缩进会变得扭曲,你可以使用 Flutter 工具来自动修复这个问题:
– 在 Android Studio / IntelliJ IDEA 中可以在 Dart 文件上点击右键,然后选择
Reformat Code with dartfmt– 在 VS Code 中,点击右键并选择
Format Document– 在终端中,运行 flutter format 命令
1, 替换 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'),
),
),
);
}
}
2, 运行这个APP,你会看到以下界面:
观察
- 这个示例创建一个 Material (质感) APP,Material是一种在Web和手机上,标准的视觉设计语言,
Flutter 提供了丰富的 Material 组件。 - 主方法使用了胖箭头符号(=>), 它是只有一行代码的函数和方法的简写形式。
- MyApp 继承了 StatelessWidget ,使得它自己成为了一个小组件,在Flutter中, 几乎所有东西都是一个组件,包括对其,填充和布局。
- Material 库中的 Scaffold 组件,提供了一个默认的 bar (顶栏),title,和 body 属性,Scaffold 为主屏幕保存了一组组件树。组件树可能会非常复杂。
- 组件的主要共组是提供一个 build() 方法,这个方法描述了如何显示其他小组件
- 这个例子中的组件树包含 Center 组件,Center组件包含一个 Text 子组件, Center 组件将其子组件居中对齐。
Step 2: 使用扩展包
在这一步中,将要使用一个扩展包,名为 english_words , 它包含了几千个最常用的英语单词和一些实用功能。
可以在 pub.dartlang.org中查找 english_words 包,当然也可以在这里查找其他包。
1, pubspec 文件管理 Flutter App的资源,在 pubspec.yaml 文件中添加 english_words(3.1.0或更高的版本) 到依赖列表中,新的局部代码如下所示:
dependencies: flutter: sdk: flutter cupertino_icons: ^0.1.0 english_words: ^3.1.0
2, 在Android Studio的编辑视图中打开 pubspec 文件,然后点击右上角的 Packages get, 这个操作会把包加入你的项目中,你应该从控制台中看到以下内容:
flutter packages get
Running "flutter packages get" in startup_namer...
Process finished with exit code 0
3, 在 lib/main.dart 中, 引入 english_words, 如下所示:
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
当你键入代码时,Android Studio会给你一些导入库的建议,键入完成后,编辑器会呈现灰色的字符串来提示你导入的库未使用(到目前为止)。
4, 使用 english_words 包生成的字符串来代替”Hello World”
注:“Pascal case” (又名“upper camel 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, 每次你点击了热加载按钮,或者保存了文件,你都会看到在运行的应用程序中随机选择的一个单词对。
这是因为单词对时在 build() 方法中随机生成的。
报错?
如果APP没有正确运行,认真寻找错误。如若必要,参照下列链接中的代码:
– pubspec.yaml (pubspec.yaml 文件不会再改变了)
– lib/main.dart
Step 3: 添加一个带状态的部件
Stateless 组件是不可变的,这意味着它们的属性不能改变——所有的值都是final的。
Stateful 组件可以保存状态,它在其生命周期内可以改变状态,实现了 stateful 的组件至少需要两个类:StatefulWidget 与 State 。
StatefulWidget 创建一个 State 的实例,StatefulWidget 本身是不可变的,但是 State 是可变的,并且存在生命周期。
在这一步中,你将创建一个有状态的组件: RandomWords , 然后,它创建了他的状态类: RandomWordsState , 状态类最终保存了喜欢的单词对。
1, 在 main.dart 中添加一个状态组件 RandomWords , 它可以写在MyApp类外的任何地方,但是推荐把它写在文件底部,RandomWords 除了创建它自己的状态部件外,没有其他多余的功能:
class RandomWords extends StatefulWidget {
@override
createState() => new RandomWordsState();
}
2, 创建 RandomWordsState 类,App中的大部分代码都在这个类中,它保存了 RandomWords 类的状态,它会保存生成的单词对,
当用户滚动屏幕时,它保存的单词对会无限增加,同时在用户添加和删除单词对时,它也会切换心型图标的颜色。
你将一步一步地构建这个类,下面,首先创建一个最基本的类:
class RandomWordsState extends State<RandomWords> {}
3, 创建完成之后,IDE会提示这个类没有 build() 方法,下一步,将要写一个基本的 build() 方法,将之前 MyApp 中随机生成单词对的代码移动到这个方法中:
class RandomWordsState extends State<RandomWords> {
@override
Widget build(BuildContext context) {
final wordPair = new WordPair.random();
return new Text(wordPair.asPascalCase);
}
}
4, 删除 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(wordPair.asPascalCase), // 删除这一行 child: new RandomWords(), // 添加这一行 ),
),
);
}
}
重启APP,这时若使用热加载,会看到以下警告:
Reloading...
Not all changed program elements ran during view reassembly; consider
restarting.
这可能是误报,但是它会自动重启来确保你的改变会映射到UI中。
APP应该像之前一样,在每次热加载和保存项目之后显示一个随机的单词对。
报错?
如果APP没有正确运行,认真寻找错误。如若必要,参照下列链接中的代码:
– lib/main.dart
Step 4: 创建一个无限滚动的ListView
在这一步中,将要扩展 RandomWordsState ,让它显示单词对的列表,当用户滚动时,显示在 ListView 组件中的列表会无限增长。
ListView组件的 builder
工厂构造器允许你根据需要,创建一个惰性加载的列表。
1, 在 RandomWordsState 中添加一个 _suggestions
列表,这个列表保存了建议的单词对。这个变量以一个下划线开头,在 Dart 中,以下划线开头的变量是私有的。
另外,添加一个 _biggerFont
变量保存字体大小:
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _biggerFont = const TextStyle(fontSize: 18.0);
//... }
2, 在 RandomWordsState 中添加一个 _buildSuggestions()
方法,这个方法构造了显示单词对列表的 ListView 。
ListView 提供了一个构造属性 itemBuilder
, 这个属性需要一个匿名函数作为其回调。这个匿名函数有两个参数— BuildContext
和迭代行数 i
,迭代从 0 开始,
每次对一个单词对执行回调后都会加一。这种模式允许单词对列表随着用户滚动屏幕而增长。
_buildSuggestions()的代码如下所示:
class RandomWordsState extends State<RandomWords> {
...
Widget _buildSuggestions() {
return new ListView.builder(
padding: const EdgeInsets.all(16.0),
// itemBuilder 在每次生成一个单词对时被回调
// 每一行都是用 ListTile 代表
// 对于偶数行,这个回调函数都添加一个 ListTile 组件来保存单词对
// 对于奇数行,这个回调函数会添加一个 Divider 组件来在视觉上分割单词对。
// 注意,在小设备上看到分割物可能会非常困难
itemBuilder: (context, i) {
// 在 ListView 组件的每行之前,先添加一个像素高度的分割。
if (i.isOdd) return new Divider();
// 这个"i ~/ 2"的表达式将i 除以 2,然后会返回一个整数结果。
// 例: 1, 2, 3, 4, 5 会变成 0, 1, 1, 2, 2。
// 这个表达式会计算 ListView 中单词对的真实数量
final index = i ~/ 2;
// 如果到达了单词对列表的结尾处...
if (index >= _suggestions.length) {
// ...然后生成10个单词对到建议的名称列表中。
_suggestions.addAll(generateWordPairs().take(10));
}
return _buildRow(_suggestions[index]);
}
);
}
}
3, 在每次生成单词对后,_buildSuggestions 会调用 _buildRow。这个函数使用 ListTile 组件显示每一个新的单词对,这使得每一行更加好看。
在 RandomWordsState 中添加 _buildRow:
class RandomWordsState extends State<RandomWords> {
...
Widget _buildRow(WordPair pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
}
}
4, 使用 _buildSuggestions()
修改 RandomWordsState 的build() 方法, 不再使用直接生成单词的方式,如下所示:
class RandomWordsState extends State<RandomWords> {
...
@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(),
);
}
...
}
修改 MyApp 的 build() 方法,移除 Scaffold 和 AppBar 实例,
MyApp 将由 RandomWordsState 来管理,这使得用户可以更容易地在APP顶栏中更换界面,因为用户可以在接下来的步骤中从一个屏幕导航到另一个屏幕。
用下列的build() 方法替换调原来的方法:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
home: new RandomWords(),
);
}
}
重启APP,你会看到单词对列表,向下滚动,你会继续看到新的单词配对
报错?
如果APP没有正确运行,认真寻找错误。如若必要,参照下列链接中的代码:
– lib/main.dart
Step 5: 加入交互元素
在这一步中,会为每一行加入可点击的心形图标,当用户点击列表中的一个条目时,会切换它的“收藏”状态,这个单词对会被添加或从收藏夹中删除。
1, 在 RandomWordsState 中添加名为 _save 的 Set 集合,这个集合保存了用户喜欢的单词对,这里,Set 要优于 List,因为Set不允许出现重复元素。
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _saved = new Set<WordPair>();
final _biggerFont = const TextStyle(fontSize: 18.0);
...
}
2, 在 _buildRow 方法中添加 alreadySaved 字段用来判断单词对是否已经加入了收藏夹中。
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
...
}
3, 在 _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,
),
);
}
4, 重启APP, 你应该能看到每一行都会有一个心形图标了,但是现在还没有办法与其交互。
5, 让心形图标可以变得点击。如果一个单词对已经保存在收藏夹中了,再一次点击它,会把单词对从收藏夹中删除,
当图标被点击后,会调用 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没有正确运行,认真寻找错误。如若必要,参照下列链接中的代码:
– lib/main.dart
Step 6: 导航到新窗口
在这一步中,将要添加一个新界面(在Flutter中,叫 route
),这个新界面显示最爱的单词对,你将学会如何在home界面和新界面之间来回导航。
在Flutter中,导航器管理包含 route 的堆栈。添加一个 route 在导航栈上,新界面将通过该 route 进入。从导航栈中弹出一条 route,将回到原来的界面。
1, 在 RandomWordsState 类中 build() 方法的 AppBar 上添加一个列表图标,当用户单击列表图标时,一个包含收藏项的新 route 被推送到导航器中。
注: 有些组件的子组件是单个组件 (child)。另一些组件,像 action 的子组件是一个组件数组 (children),数组用一对方括号表示([])。
将图标及其对应的动作添加到 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: new Icon(Icons.list), onPressed: _pushSaved),
],
),
body: _buildSuggestions(),
);
}
...
}
2, 在 RandomWordsState 中添加 _pushSaved() 方法:
class RandomWordsState extends State<RandomWords> {
...
void _pushSaved() {
}
}
热加载APP之后,会看到列表图标出现在顶栏中,现在点击它不会发生任何事情,因为 _pushSaved() 方法是空的。
3, 当用户点击APP顶栏上的列表图标时,创建一个 route ,并将它添加到导航栈中,这个动作会使APP显示一个新界面。
新界面的内容由 MaterialPageRoute 的 builder
属性构建,这个属性应该是一个匿名函数。
添加一条新路径的代码:
void _pushSaved() {
Navigator.of(context).push();
}
4, 添加 MaterialPageRoute 和它的 builder 属性,然后添加生成ListTile行的代码。
ListTile 的 divideTiles() 方法在每行之间添加了一个水平的间距。divided
变量保存了最后一行,最后由 toList() 函数转换为列表:
void _pushSaved() {
Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) {
final tiles = _saved.map(
(pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
final divided = ListTile
.divideTiles(
context: context,
tiles: tiles,
)
.toList();
},
),
);
}
5, builder 返回一个 Scaffold 组件,为包含一个新界面的顶栏,名为 “Saved Suggestions.”。新界面的 body 属性是包含若干 ListTiles 的 ListView ,每一行使用分割线分割。
void _pushSaved() {
Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) {
final tiles = _saved.map(
(pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
final divided = ListTile
.divideTiles(
context: context,
tiles: tiles,
)
.toList();
return new Scaffold(
appBar: new AppBar(
title: new Text('Saved Suggestions'),
),
body: new ListView(children: divided),
);
},
),
);
}
6, APP热加载之后,选择一些最喜欢的,然后点击顶栏中的列表图标,新界面是一个收藏夹,注意,导航加入了一个”Back”的按钮在顶栏中,这里,不需要显式地调用 Navigator.pop,点击后退按钮会返回到主界面。
报错?
如果APP没有正确运行,认真寻找错误。如若必要,参照下列链接中的代码:
– lib/main.dart
Step 7: 使用全新的主题
这一步,将更换APP的主题,主题控制着APP的外观,你可以使用默认的主题,默认主题由你的物理设备或模拟器决定,你可以使用自定义的主题来改变APP的外观。
1, 通过配置 ThemeData 类,可以很容易的更改APP的主题,下面,将主题颜色改为白色:
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(),
);
}
}
2, 热重载应用。注意到整个背景是白色的,甚至包括导航栏。
3, 留给读者一个练习:ThemeData 类可以改变UI的多个方面,
Material 库中的 Colors 类提供了很多可以使用的颜色常量,热重载可以让颜色测试变得更快。
报错?
如果APP没有正确运行,认真寻找错误。如若必要,参照下列链接中的代码:
– lib/main.dart
圆满结束
你已经写完了能够运行在IOS和安卓两个平台上的交互式 Flutter APP, 在这篇文章中,你学会了:
- 从头创建一个 Flutter APP
- 编写 Dart 代码
- 利用第三方库
- 使用热加载来提高开发效率
- 创建带状态的组件,为APP增加了交互性
- 创建了一个延迟加载的、无限滚动的列表,并显示了一个列表视图和列表框
- 创建了一条 route ,并添加了在主route和新route之间移动的逻辑
- 了解了如何使用主题来改变应用程序UI的外观