本教程 follow Flutter official website ,进行了排坑与更详细的步骤介绍与讲解,希望在此取得比官网更高的学习效率。
我们将实现:取名字的APP。
主要功能:
- 选择(收藏)、取消。一次生成十个名称,滚动生成新一批名称。
- 点击导航栏右边的icon,进入到仅列出收藏名称的新页面。
将学到:
- Flutter应用程序的基本结构.
- 查找和使用packages来扩展功能.
- 使用热重载加快开发周期.
- 如何实现有状态的widget.
- 如何创建一个无限的、懒加载的列表.
- 如何创建并导航到第二个页面.
- 如何使用主题更改应用程序的外观.
您需要准备的:
- 了解面向对象编程,掌握其基本原理。附一个有趣的面向对象编程介绍
- 初始化好一个 Flutter 应用程序并启动。在这里学习初始化
1 创建 Flutter app
知识库:
- Flutter 提供了一套丰富的 material widgets,所以本示例使用 Material(视觉设计语言)来构建。
- 在 Flutter 中,大多数东西都是 widget,包括对齐(alignment)、填充(padding)和布局(layout)。
- widget的主要工作是提供一个 build() 方法来描述如何根据其他较低级别的 widget 来显示自己。
- 本示例中 Center widget 可以将其子 widget 树对其到屏幕中心。
- 格式化:右键选择 Format Document(快捷键:shift + alt + F)。
逗号结尾使构建方法的自动格式化更好。(可尝试一下删去逗号之后格式化的效果)
1.1 替换 lib/main.dart 中所有代码,启动后如下图:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp()); // main函数使用了(=>)符号, 这是Dart中单行函数或方法的简写
class MyApp extends StatelessWidget {
// 该应用程序继承了 StatelessWidget,这将会使应用本身也成为一个widget。
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Welcome to Flutter',
home: new Scaffold(
// Scaffold是Material library中提供的一个widget, 它提供了默认的导航栏、标题和包含主屏幕widget树的body属性。
appBar: new AppBar(
title: new Text('Welcome to Flutter'),
centerTitle: true, // 标题居中
),
body: new Center(
child: new Text('Hello World'),
),
), // 逗号结尾使构建方法的自动格式化更好。
);
}
}
2 使用外部包(package)
知识库:
- 我们将引入 english_words 的开源软件包,它会帮助我们生成所需要的英文单词。
- 可在 pub.dartlang.org 上搜索各种软件包,他们都是开源的。
- pubspec 管理静态资源(图片、软件包等),所以我们稍后会在 pubspec.yaml 中添加依赖的软件包。
2.1 在 pubspec.yaml 中添加 english_words 到依赖项列表
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.0
english_words: ^3.1.0
在控制台输入 flutter packages get 安装依赖项
(即使安装了依赖项,后期还可能报错 Target of URI doesn't exist 'package:flutter/material.dart',再次执行此命令即可)
2.2 在 lib/main.dart 中, 引入 english_words
import 'package:english_words/english_words.dart';
引入后会提示 未使用,建议移除 (如下)Don't worry, let's use it.
2.3 使用 English words 包随机生成英文单词替换“Hello World”.
// 实例化随机生成英文单词的对象
final wordPair = new WordPair.random();
// 在body中使用
body: new Center(
child: new Text(wordPair.asPascalCase),
),
附第2步完整 lib/main.dart 代码和实现效果
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart'; // 引入包
void main() => runApp(new MyApp()); // main函数使用了(=>)符号, 这是Dart中单行函数或方法的简写
class MyApp extends StatelessWidget {
// 该应用程序继承了 StatelessWidget,这将会使应用本身也成为一个widget。
@override
Widget build(BuildContext context) {
final wordPair = new WordPair.random(); // 实例化wordPair
return new MaterialApp(
title: 'Welcome to Flutter',
home: new Scaffold(
// Scaffold是Material library中提供的一个widget, 它提供了默认的导航栏、标题和包含主屏幕widget树的body属性。
appBar: new AppBar(
title: new Text('Welcome to Flutter'),
centerTitle: true, // 标题居中
),
body: new Center(
child: new Text(wordPair.asPascalCase),
),
), // 逗号结尾使构建方法的自动格式化更好。
);
}
}
第3步: 添加一个 有状态的部件(Stateful widget)
知识库:
- Stateless widgets 是不可变的,Stateful widgets 是可变.
- 实现一个 stateful widget 至少需要两个类:
- StatefulWidget 类。
- State 类。
3.1 在 main.dart 中添加一个有状态的 widget → RandomWords,
RandomWords 创建其 State 类 → RandomWordsState,作用是为 widget 维护数据。
class RandomWords extends StatefulWidget {
@override
createState() => new RandomWordsState();
}
3.2 添加 RandomWordsState 类.
该应用程序的大部分代码都在该类中,该类持有 RandomWords 的状态,保存数据实现业务逻辑。
class RandomWordsState extends State<RandomWords> {
@override
Widget build(BuildContext context) {
final wordPair = new WordPair.random();
return new Text(wordPair.asPascalCase);
}
}
通过下面高亮显示的代码,将生成单词对代的码从MyApp移动到RandomWordsState中
body: new Center(
child: new RandomWords(),
),
3.3 热重载查看效果
// 可能会出现此警告,这可能是误报,重新启动即可
Reloading...
Not all changed program elements ran during view reassembly; consider restarting.
附第3步完整 lib/main.dart 代码和实现效果
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart'; // 引入包
void main() => runApp(new MyApp()); // main函数使用了(=>)符号, 这是Dart中单行函数或方法的简写
class MyApp extends StatelessWidget {
// 该应用程序继承了 StatelessWidget,这将会使应用本身也成为一个widget。
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Welcome to Flutter',
home: new Scaffold(
// Scaffold是Material library中提供的一个widget, 它提供了默认的导航栏、标题和包含主屏幕widget树的body属性。
appBar: new AppBar(
title: new Text('Welcome to Flutter'),
centerTitle: true, // 标题居中
),
body: new Center(
child: new RandomWords(),
),
),
);
}
}
class RandomWordsState extends State<RandomWords> {
@override
Widget build(BuildContext context) {
final wordPair = new WordPair.random();
return new Text(wordPair.asPascalCase);
}
}
class RandomWords extends StatefulWidget {
@override
createState() => new RandomWordsState();
}
第4步: 创建一个无限滚动ListView
知识库:
以注释的形式嵌在代码中,更易理解!
4.1 向 RandomWordsState 类中添加业务逻辑
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[]; // 保存数据(Dart语言中使用下划线前缀标识符,会强制将其变成私有的)
final _biggerFont = const TextStyle(fontSize: 18.0); // 改变字体大小
Widget _buildSuggestions() {
return new ListView.builder( // 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));
}
return _buildRow(_suggestions[index]);
}
);
}
// 对于每一个单词对,_buildSuggestions函数都会调用一次_buildRow
Widget _buildRow(WordPair pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
}
}
4.2 更新 RandomWordsState 的 build 方法以使用 _buildSuggestions()
@override
Widget build(BuildContext context) {
return new Scaffold (
appBar: new AppBar(
title: new Text('Startup Name Generator'),
),
body: _buildSuggestions(),
);
}
4.3 更新MyApp的build方法
从 MyApp 中删除 Scaffold 和 AppBar 实例,换成用 RandomWordsState 管理。
这使得用户在下一步中从一个屏幕导航到另一个屏幕时, 可以更轻松地更改导航栏中的的路由名称。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
home: new RandomWords(),
);
}
}
附第4步完整 lib/main.dart 代码和实现效果
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart'; // 引入包
void main() => runApp(new MyApp()); // main函数使用了(=>)符号, 这是Dart中单行函数或方法的简写
class MyApp extends StatelessWidget {
// 该应用程序继承了 StatelessWidget,这将会使应用本身也成为一个widget。
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
home: new RandomWords(),
);
}
}
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _biggerFont = const TextStyle(fontSize: 18.0);
Widget _buildSuggestions() {
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));
}
return _buildRow(_suggestions[index]);
}
);
}
Widget _buildRow(WordPair pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
}
@override
Widget build(BuildContext context) {
return new Scaffold (
appBar: new AppBar(
title: new Text('Startup Name Generator'),
),
body: _buildSuggestions(),
);
}
}
class RandomWords extends StatefulWidget {
@override
createState() => new RandomWordsState();
}
第5步: 添加选择❤️取消交互
知识库:
- 在Flutter的响应式风格的框架中,调用
setState()
会为State对象触发build()
方法,从而导致对UI的更新
5.1 添加存储用户收藏单词的 Set
添加一个 _saved
Set(集合) 到 RandomWordsState。
在这里,Set 比 List 更合适,因为 Set 中不允许重复的值。
final _saved = new Set<WordPair>();
5.2 检查是否收藏过
在 _buildRow
方法中添加 alreadySaved
来检查确保单词对还没有添加到收藏夹中
final alreadySaved = _saved.contains(pair);
5.3 添加心形 ❤️ icon
在 _buildRow()
中, 添加一个❤️到 ListTiles
trailing: new Icon(
alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.red : null,
),
5.4 添加❤️的交互能力
在 _buildRow
中让列表变得可以点击,❤️可变色。
当被点击时,函数调用 setState()
通知框架状态已经改变。
如果单词条目已经添加到收藏夹中, 再次点击它将其从收藏夹中删除。
onTap: () {
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
},
}
附第5步完整 lib/main.dart 代码和实现效果
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart'; // 引入包
void main() => runApp(new MyApp()); // main函数使用了(=>)符号, 这是Dart中单行函数或方法的简写
class MyApp extends StatelessWidget {
// 该应用程序继承了 StatelessWidget,这将会使应用本身也成为一个widget。 在Flutter中,大多数东西都是widget,包括对齐(alignment)、填充(padding)和布局(layout)
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
home: new RandomWords(),
);
}
}
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _saved = new Set<WordPair>();
final _biggerFont = const TextStyle(fontSize: 18.0);
Widget _buildSuggestions() {
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));
}
return _buildRow(_suggestions[index]);
}
);
}
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);
}
});
},
);
}
@override
Widget build(BuildContext context) {
return new Scaffold (
appBar: new AppBar(
title: new Text('Startup Name Generator'),
centerTitle: true,// 标题居中
),
body: _buildSuggestions(),
);
}
}
class RandomWords extends StatefulWidget {
@override
createState() => new RandomWordsState();
}
第6步: 导航到新页面
知识库:
- 我们将学习如何在主路由和新路由之间切换。
- 在Flutter中,导航器管理应用程序的路由栈。
- 将路由推入(push)到导航器的栈中,将会显示更新为该路由页面。
- 从导航器的栈中弹出(pop)路由,将显示返回到前一个路由。
- 导航器会主动在应用栏中添加一个“返回”按钮。我们不必显式实现Navigator.pop。
6.1 在 RandomWordsState 的 build 方法中为 AppBar 添加一个列表icon。点击icon,切换路由,进入新页面。
提示: 某些 widget 属性需要单个 widget(child),而其它一些属性,如 action,需要一组 widgets(children),用方括号[]表示。
@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(),
);
}
6.2 向 RandomWordsState 类添加一个_pushSaved() 方法.
void _pushSaved() {
Navigator.of(context).push( // Navigator.push 会使路由入栈(导航管理器的栈)
new MaterialPageRoute( // 新页面的内容在 MaterialPageRoute 的 builder属性中构建,
builder: (context) { // builder是一个匿名函数。
final tiles = _saved.map(
(pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
// 在每个ListTile之间添加1像素的分割线。
final divided = 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: divided),
);
},
),
);
}
附第6步完整 lib/main.dart 代码和实现效果
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart'; // 引入包
void main() => runApp(new MyApp()); // main函数使用了(=>)符号, 这是Dart中单行函数或方法的简写
class MyApp extends StatelessWidget {
// 该应用程序继承了 StatelessWidget,这将会使应用本身也成为一个widget。 在Flutter中,大多数东西都是widget,包括对齐(alignment)、填充(padding)和布局(layout)
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
home: new RandomWords(),
);
}
}
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _saved = new Set<WordPair>();
final _biggerFont = const TextStyle(fontSize: 18.0);
Widget _buildSuggestions() {
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));
}
return _buildRow(_suggestions[index]);
}
);
}
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);
}
});
},
);
}
@override
Widget build(BuildContext context) {
return new Scaffold (
appBar: new AppBar(
title: new Text('Startup Name Generator'),
centerTitle: true,// 标题居中
actions: <Widget>[
new IconButton(icon: new Icon(Icons.list), onPressed: _pushSaved),
],
),
body: _buildSuggestions(),
);
}
void _pushSaved() {
Navigator.of(context).push( // Navigator.push 会使路由入栈(导航管理器的栈)
new MaterialPageRoute( // 新页面的内容在 MaterialPageRoute 的 builder属性中构建,
builder: (context) { // builder是一个匿名函数。
final tiles = _saved.map(
(pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
// 在每个ListTile之间添加1像素的分割线。
final divided = 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: divided),
);
},
),
);
}
}
class RandomWords extends StatefulWidget {
@override
createState() => new RandomWordsState();
}
第7步:使用主题更改UI
更改主题为红色
主题颜色常量查询 Colors
可使用 ThemeData 来更改其他 UI 颜色
附第7步 MyApp 类的完整代码和实现效果
class MyApp extends StatelessWidget {
// 该应用程序继承了 StatelessWidget,这将会使应用本身也成为一个widget。 在Flutter中,大多数东西都是widget,包括对齐(alignment)、填充(padding)和布局(layout)
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
theme: new ThemeData(
primaryColor: Colors.red,
),
home: new RandomWords(),
);
}
}
希望在这里有所收获 ~