Flutter学习(二):创建一个交互式的Flutter应用程序
Flutter学习(二):创建一个交互式的Flutter应用程序
本文将会完成 Flutter 官网的一个练习例子来作为 Flutter 开发学习的入门hello world。
主要知识点:
- Flutter应用程序的基本结构.
- 查找和使用packages来扩展功能.
- 使用热重载加快开发周期.
- 如何实现有状态的widget.
- 如何创建一个无限的、延迟加载的列表.
- 如何创建并导航到第二个页面.
- 如何使用主题更改应用程序的外观.
具体的例子要求是:为一个创业公司生成建议的名称。用户可以选择和取消选择的名称、保存(收藏)喜欢的名称。该代码一次生成十个名称,当用户滚动时,会生成一新批名称。用户可以点击导航栏右边的列表图标,以打开到仅列出收藏名称的新页面。
完成效果:
新建项目
项目使用IDEA来创建,具体新建项目的步骤请查看Flutter学习(一):开发环境搭建
项目创建结束之后,打开 lib/main.dart便是项目的主要逻辑代码所在地。
默认创建的项目中包含系统自动生成的一系列代码,这里我们将其统统删除。开始自己亲手创建一个应用程序。
导入所需要的第三方依赖包
由于本项目需要生成大量的单词列表,而单词自然不能靠自己去定义。一来浪费时间,二来也不能实现无限瀑布流的效果。
于是使用一个名为 english_words的开源软件包 ,其中包含数千个最常用的英文单词以及一些实用功能.
打开 pubspec.yaml文件,在 dependencies中添加english_words包的依赖引用。
项目依赖添加
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.0
# 添加常用的英文单词包
english_words: ^3.1.0
获取项目所需依赖
之后点击右上角的 packages get flutter-2-packages-get
之后项目便会下载所需要的包并自动加载到项目中去
flutter packages get
Running "flutter packages get" in startup_namer...
Process finished with exit code 0
导入所需依赖
在 lib/main.dart 中, 引入 english_words 包和系统所需的界面包 Material,Material是一种标准的移动端和 web端的视觉设计语言。 Flutter 提供了一套丰富的 Material widgets。
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
编写项目主体框架
首先项目肯定是要有一个主要的界面来作为主界面。在 Flutte中一个界面称之为 router(界面),一个大型的项目便是许许多多的路由在相互跳转。
在Flutter中,导航器管理应用程序的路由栈。界面之间的跳转其实是路由栈的 压栈和 出栈操作。
主路由
首先创建项目的主路由:
// => 为flutter中单行函数或方法的简写
void main() => runApp(new MyApp());
这里,主路由使用 runApp方法来启动路由。主要的逻辑代码我们将其封装到了MyApp类中。在当前文件中直接创建MyApp类。
class MyApp extends StatelessWidget{
// 每次执行该界面是便会执行一遍build方法
@override
Widget build(BuildContext context) {
// TODO: implement build
}
}
MyApp 类继承自 StatelessWidget,Statelesswidget 是不可变的, 这意味着其中的属性不能改变,所有的值都是最终的,相当于是一个静态界面。
继承了StatelessWidget之后 MyApp 类也是一个 widget。
在Flutter中,大多数东西都是 widget,包括对齐(alignment)、填充(padding)和布局(layout)。widget 的主要工作是提供一个 build()方法来描述如何根据其他较低级别的widget来显示自己。
MaterialApp
在 StatelessWidget 的 build 方法中必须返回一个MaterialApp来作为路由的主要显示
class MyApp extends StatelessWidget{
// 每次执行该界面是便会执行一遍build方法
@override
Widget build(BuildContext context) {
// TODO: implement build
return new MaterialApp(
);
}
}
MaterialApp中包含 title、home、theme等部分。title是当前页面显示的标题,一般在界面顶部显示。而home则是界面的显示主体。theme而是当前应用的主题设置。
在 MaterialApp 中进行简单的设置
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'),
),
),
);
}
}
在home中创建了一个 Scaffold,Scaffold 是 Material library 中提供的一个widget, 它提供了默认的导航栏、标题和包含主屏幕widget树的body属性
接下来运行应用程序,显示界面如下: hello world
显示单词列表
由于想要显示的单词列表界面需要进行改变,并且还需响应相应的点击事件。因此需要创建一个 RandomWords类来继承 StatefulWidget。
StatefulWidget 和 StatelessWidget 是 Flutter中的两种Widget,主要的区别是:
Stateless widgets 是不可变的, 这意味着它们的属性不能改变,所有的值都是最终的.
Stateful widgets 持有的状态可能在widget生命周期中发生变化. 实现一个 stateful widget 至少需要两个类:
- 一个 StatefulWidget类。
- 一个 State类。
StatefulWidget类本身是不变的,但是 State 类在 widget 生命周期中始终存在.
创建StatefulWidget
class RandomWords extends StatefulWidget{
@override
createState() => new RandomWordState();
}
在 RandomWords 中创建了其 State 类 RandomWordsState。State 类将最终为 widget 维护建议的和喜欢的单词对。
创建RandomWordState
RandomWordState 继承自 State,作为一个 State,其中需要实现的便是 build 方法。
class RandomWordState extends State<RandomWords>{
/**
* 用来生成单词对的主要运行函数
*/
@override
Widget build(BuildContext context) {
// TODO: implement build
);
}
}
在 build 方法中便可以创建一个无限滚动 ListView来进行列表显示。
首先需要显示列表的话便需要一个存储元素的列表,dart中使用下划线来将变量声明为私有的。
// 保存单词对
final _suggestions = <WordPair>[];
// 显示字体效果
final _biggerFont = const TextStyle(fontSize: 18.0);
其次在 RandomWordsState 类添加一个 _buildSuggestions() 函数. 此方法构建显示建议单词对的ListView。
/**
* 用来构建显示单词对的list
* 当用户滚动时,ListView中显示的列表将无限增长。
* ListView的builder工厂构造函数允许按需建立一个懒加载的列表视图。
*/
Widget _bulidSuggestions(){
return new ListView.builder(
padding: const EdgeInsets.all(16.0),
// 对于每个建议的单词对都会调用一次itemBuilder,然后将单词对添加到ListTile行中
// 在偶数行,该函数会为单词对添加一个ListTile row.
// 在奇数行,该行书湖添加一个分割线widget,来分隔相邻的词对。
// 注意,在小屏幕上,分割线看起来可能比较吃力。
itemBuilder: (context,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 index = i~/2;
// 如果是建议列表中最后一个单词对
if(index >= _suggestions.length){
// ...接着再生成10个单词对,然后添加到建议列表
_suggestions.addAll(generateWordPairs().take(10));
}
// 对于每一个单词对,_buildSuggestions函数都会调用一次_buildRow。
// 这个函数在ListTile中显示每个新词对,这使您在下一步中可以生成更漂亮的显示行
return _buildRow(_suggestions[index]);
}
);
}
接下来创建对应的 _buildRow 方法来显示每一行的数据信息
Widget _buildRow(WordPair pair) {
// 显示列表标题
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
}
接下来便可以在 RandomWordState 类中的 build方法中调用 _bulidSuggestions() 方法来显示单词对列表
@override
Widget build(BuildContext context) {
return new Scaffold (
appBar: new AppBar(
title: new Text('Startup Name Generator'),
),
body: _buildSuggestions(),
);
}
...
在返回的 Scaffold widget中的 body中调用 _buildSuggestions()方法来在当前界面主体内容中生成单词列表。
最后还需要更新 MyApp 的 build 方法。
从 MyApp 中删除 Scaffold 和 AppBar 实例。 然后使用RandomWordsState来作为路由的主要显示widget。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
home: new RandomWords(),
);
}
}
重新启动应用程序。项目运行结果如下: 项目运行结果
给单词列表添加交互
目前已经能够显示一个单词列表瀑布流,但是每一个单词列表还不能够对其进行操作。按照之前的要求,还需要对显示的单词列表添加收藏功能。那么便得在显示的单词列表的后面添加一个心形 ❤️ 图标,当点击该图标的时候便将该单词保存到收藏集合中去。这里之所以使用 集合作为存储容器是因为集合可以保证存入的变量内容不重复。
创建集合来存储收藏的单词对
在 RandomWordState类中添加私有变量 _saved来存储所收藏的单词对。
final _saved = new Set<WordPair>();
显示心形 ❤️ 图标
因为所要交互的是显示的集合中的每一个行,因此在 RandomWordState类中的 _buildRow方法添加对应的显示图标。
在 ListTile 中对于的标题 title 之后添加图标:
// 显示图标
trailing: new Icon(
// 判断是否已经收藏来显示不同的图标
alreadSaved ? Icons.favorite : Icons.favorite_border,
color: alreadSaved ? Colors.red : null,
),
这里使用 alreadSaved来表示当前的单词对是否已经包含在集合中,在 RandomWordState类第一行的位置进行判断:
// 判断当前元素是否已经包含在集合中
final alreadSaved = _saved.contains(suggestion);
显示玩收藏图标之后,还需要对其配置对应的点击事件,在后面编辑 onTap():
// 设置点击事件处理
onTap: (){
// 当心形❤图标被点击时,函数调用setState()通知框架状态已经改变。
// 在Flutter的响应式风格的框架中,
// 调用setState() 会为State对象触发build()方法,从而导致对UI的更新
setState(() {
if (alreadSaved){
_saved.remove(suggestion);
}else{
_saved.add(suggestion);
}
});
},
现在,完整的 _buildRow 方法已经编辑完成,之后的样子是:
Widget _buildRow(WordPair suggestion) {
// 判断当前元素是否已经包含在集合中
final alreadSaved = _saved.contains(suggestion);
return new ListTile(
// 显示标题
title: new Text(
suggestion.asPascalCase,
style: _biggerFont,
),
// 显示图标
trailing: new Icon(
// 判断是否已经收藏来显示不同的图标
alreadSaved ? Icons.favorite : Icons.favorite_border,
color: alreadSaved ? Colors.red : null,
),
// 设置点击事件处理
onTap: (){
// 当心形❤图标被点击时,函数调用setState()通知框架状态已经改变。
// 在Flutter的响应式风格的框架中,
// 调用setState() 会为State对象触发build()方法,从而导致对UI的更新
setState(() {
if (alreadSaved){
_saved.remove(suggestion);
}else{
_saved.add(suggestion);
}
});
},
);
}
直接保存代码,假如你没有结束项目的运行的话,新更改的代码会直接显示出来,这便是 Flutter 的 热重载
跳转到收藏界面
已经实现了单词的收藏功能,并且也已经将收藏的单词保存到了对应的集合中去,那么便新创建一个界面专门来显示已经收藏的单词对。在 Flutter 中创建的一个新界面称为路由。
状态栏添加新路由入口
跳转到新路由的入口可以放置在功能栏里,这样也方便操作。在 RandomWordsState 的 build 方法中为AppBar 添加一个列表图标。当用户点击列表图标时,包含收藏夹的新路由页面入栈显示。
appBar: new AppBar(
title: new Text("Startup Name Generator"),
// 新建一个可以保存不同Widget的 actions
actions: <Widget>[
// 新建一个IconButton来处理点击事件
new IconButton(icon: new Icon(Icons.list), onPressed: _pushSave)
],
),
创建新的路由
在 AppBar中创建的列表图标中的点击事件中调用了_pushSave方法。在 _pushSave 方法中便需要创建一个新的路由,并将 _saved集合中的内容以列表的形式进行展示。
创建一个新的路由需要使用 导航管理器栈,在Flutter中,导航器管理应用程序的路由栈。将路由推入(push)到导航器的栈中,将会显示更新为该路由页面。 从导航器的栈中弹出(pop)路由,将显示返回到前一个路由。使用 Navigator.push调用,这会使路由推入到导航管理器的栈。
新页面的内容在 MaterialPageRoute 的 builder属性中构建,builder是一个匿名函数。在builder 中返回一个 Scaffold,其中包含名为“Saved Suggestions”的新路由的应用栏。新路由的body由包含 ListTiles 行的 ListView 组成,每行之间通过一个分隔线分隔。
void _pushSave() {
// 添加Navigator.push调用,
// 这会使路由入栈(以后路由入栈均指推入到导航管理器的栈)
Navigator.of(context).push(
// 新建一个MaterialPageRoute界面
new MaterialPageRoute(builder: (context){
final tiles = _saved.map(
(pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
}
);
// 对显示的内容进行列表处理,让其以列表的形式显示,列表自带分割线
final divides = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
// builder返回一个Scaffold,
// 其中包含名为“Saved Suggestions”的新路由的应用栏。
// 新路由的body由包含ListTiles行的ListView组成;
// 每行之间通过一个分隔线分隔。
return new Scaffold(
appBar: new AppBar(
title: new Text("Saved Suggestions"),
),
body: new ListView(children: divides,),
);
})
);
}
修改主题为白色主题
主题控制应用程序的外观和风格。可以使用默认主题,该主题取决于物理设备或模拟器,也可以自定义主题以适应不同的品牌。
可以通过配置ThemeData类轻松更改应用程序的主题。 应用程序目前使用默认主题,下面将更改primary color颜色为白色。
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(),
);
}
}
main.dart完整代码
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
// 单行函数或方法的简写
void main() => runApp(new MyApp());
/**
* 创建一个 Material APP,继承自 StatelessWidget 这样
* 使得应用本身也称为一个 Widget
*
* widget的主要工作是提供一个 build方法来描述如何根据其他较低级别的weidgt来显示自己
*
* Stateless widgets 是不可变的, 这意味着它们的属性不能改变 - 所有的值都是最终的.
*/
class MyApp extends StatelessWidget{
// 每次执行该界面是便会执行一遍build方法
@override
Widget build(BuildContext context) {
// TODO: implement build
return new MaterialApp(
// 标题
title: "Startup Name Generator",
// 更改主题,可以通过配置ThemeData类轻松更改应用程序的主题
theme: new ThemeData(
primaryColor: Colors.white
),
// 主要显示内容
home: new RandomWords(),
);
}
}
/**
* Stateful widgets 持有的状态可能在widget生命周期中发生变化. 实现一个 stateful widget 至少需要两个类:
* 一个 StatefulWidget类。
* 一个 State类。 StatefulWidget类本身是不变的,但是 State类在widget生命周期中始终存在.
*
* */
class RandomWords extends StatefulWidget{
@override
createState() => new RandomWordState();
}
/**
* 添加 RandomWordsState 类.该应用程序的大部分代码都在该类中,
* 该类持有RandomWords widget的状态。
* 这个类将保存随着用户滚动而无限增长的生成的单词对,
* 以及喜欢的单词对,用户通过重复点击心形 ❤️ 图标来将它们从列表中添加或删除。
*/
class RandomWordState extends State<RandomWords>{
// dart中使用下划线来将变量声明为私有的,保存单词对
final _suggestions = <WordPair>[];
// 显示字体效果
final _biggerFont = const TextStyle(fontSize: 18.0);
// 添加一个 _saved Set(集合) 到RandomWordsState。这个集合存储用户喜欢(收藏)的单词对。
// 在这里,Set比List更合适,因为Set中不允许重复的值。
final _saved = new Set<WordPair>();
/**
* 用来构建显示单词对的list
* 当用户滚动时,ListView中显示的列表将无限增长。
* ListView的builder工厂构造函数允许您按需建立一个懒加载的列表视图。
*/
Widget _bulidSuggestions(){
return new ListView.builder(
padding: const EdgeInsets.all(16.0),
// 对于每个建议的单词对都会调用一次itemBuilder,然后将单词对添加到ListTile行中
// 在偶数行,该函数会为单词对添加一个ListTile row.
// 在奇数行,该行书湖添加一个分割线widget,来分隔相邻的词对。
// 注意,在小屏幕上,分割线看起来可能比较吃力。
itemBuilder: (context,i){
if(i.isOdd){
// 在每一列之前,添加一个1像素高的分隔线widget
// 只用在奇数行之上添加便可以做到所有的都包含一个分割线
return new Divider();
}
// 语法 "i ~/ 2" 表示i除以2,但返回值是整形(向下取整),比如i为: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));
}
// 对于每一个单词对,_buildSuggestions函数都会调用一次_buildRow。
// 这个函数在ListTile中显示每个新词对,这使您在下一步中可以生成更漂亮的显示行
return _buildRow(_suggestions[index]);
}
);
}
/**
* 用来生成单词对的主要运行函数
*/
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
// 在Flutter中,导航器管理应用程序的路由栈。
// 将路由推入(push)到导航器的栈中,将会显示更新为该路由页面。
// 从导航器的栈中弹出(pop)路由,将显示返回到前一个路由。
appBar: new AppBar(
title: new Text("Startup Name Generator"),
// 新建一个可以保存不同Widget的操作
actions: <Widget>[
new IconButton(icon: new Icon(Icons.list), onPressed: _pushSave)
],
),
body: _bulidSuggestions(),
);
}
/**
* 显示单词
*/
Widget _buildRow(WordPair suggestion) {
// 判断当前元素是否已经包含在集合中
final alreadSaved = _saved.contains(suggestion);
return new ListTile(
// 显示标题
title: new Text(
suggestion.asPascalCase,
style: _biggerFont,
),
// 显示图标
trailing: new Icon(
// 判断是否已经收藏来显示不同的图标
alreadSaved ? Icons.favorite : Icons.favorite_border,
color: alreadSaved ? Colors.red : null,
),
// 设置点击事件处理
onTap: (){
// 当心形❤图标被点击时,函数调用setState()通知框架状态已经改变。
// 在Flutter的响应式风格的框架中,
// 调用setState() 会为State对象触发build()方法,从而导致对UI的更新
setState(() {
if (alreadSaved){
_saved.remove(suggestion);
}else{
_saved.add(suggestion);
}
});
},
);
}
/**
* 点击图标之后的处理函数
*/
void _pushSave() {
// 添加Navigator.push调用,
// 这会使路由入栈(以后路由入栈均指推入到导航管理器的栈)
Navigator.of(context).push(
// 新建一个MaterialPageRoute界面
new MaterialPageRoute(builder: (context){
final tiles = _saved.map(
(pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
}
);
final divides = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
// builder返回一个Scaffold,
// 其中包含名为“Saved Suggestions”的新路由的应用栏。
// 新路由的body由包含ListTiles行的ListView组成;
// 每行之间通过一个分隔线分隔。
return new Scaffold(
appBar: new AppBar(
title: new Text("Saved Suggestions"),
),
body: new ListView(children: divides,),
);
})
);
}
}
热重载应用程序。收藏一些选项,并点击应用栏中的列表图标,在新路由页面中显示收藏的内容。
请注意,导航器会在应用栏中添加一个“返回”按钮。而不必显式实现Navigator.pop。点击后退按钮返回到主页路由。
总结
- Flutter其实是使用 widget来作为所有的界面组件来进行显示。
- 热重载很方便,在运行软件的情况下,只需要保存更改的代码便会自动运行更改的部分。
- Flutter中的界面分为静态界面和可交互界面,分别继承自 StatelessWidget和 StatefulWidget。
- Flutter当中的 widget的主要属性是配置式的进行创建的。
- 可以通过主题来更改 Flutter项目的 UI颜色。
- Flutter中的界面称之为路由(route),Flutter中的界面使用的是导航管理器栈来进行控制。
- 使用用 ListView和 ListTiles可以创建一个延迟加载的无限滚动列表。