上一篇文章 中,我们讲解了 Flutter 开发环境搭建 , 以及运行了官方demo简单体验了下 Flutter app .

此篇我们将开始对一些 Flutter app 中的一些基本概念进行讲解 , 一些基本的操作做一些示例 , 主要是参考官网的教程 Write Your First Flutter App

若你对面向对象编程熟悉,以及对基本编程概念如变量、循环、条件了解 , 则适合阅读本篇文章 . 不必需要拥有 Dart 或移动编程经验.

为了更好的阅读体验 , 请点击 阅读原文 :)

我们将创建什么

我们将实现一个简单的移动应用 , 它会生成创业公司的名称 . 用户可以选择和反选名称 , 保存喜好的那些 . 代码一次生成 10 个名称 . 当用户滑动时 , 新一批的名称就会生成 . 用户可以点击导航栏右上的按钮进入一个只展示喜好的名称的列表新页面.

我们将学到:

  • Flutter app 的基本结构
  • 使用额外的包去拓展功能
  • 使用热部署来快速开发
  • 如何去实现一个stateful 小部件
  • 如何创建一个无线滑动,懒加载的列表
  • 如何跳转去下一个界面
  • 如果通过主题去修改app外观

步骤 1 : 创建及启动 Flutter app

这里创建一个简单的 flutter app

1
2
3
flutter create flutter_first_app
cd flutter_first_app
flutter run

如有疑问 , 可参考 前一篇文章 指引

简单地 , 我们先将 lib/main.dart中的代码全部删除 , 替换为以下代码 , 其主要就是在屏幕中间展示 ‘Hello World’ .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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'),
),
),
);
}
}

重新运行得到结果

发现

  • 这个例子创建了一个 Material Design 风格的app . Material 是一种在移动端及web上标准的视觉设计语言 . Flutter 提供了丰富的 Material 风格小部件
  • main 方法使用了一个大箭头=>写法 , 它是一行代码功能或方法的缩写 . 同许多语言开发一样, main() 方法为程序入口 .
  • app 继承 StatefulWidget 使得其自身也是个widget . 在 Flutter 里 , 大多数元素都是 widget , 包括对齐方式(alignment)、 内边距(padding)、布局(layout) .
  • Material 库 中的脚手架小部件 (Scaffold widget) , 提供了一个默认的导航栏、 标题、 内容属性在屏幕中维持了部件树🌲.部件子树可以很复杂.
  • 一个小部件的主要工作就是提供 build()方法 , 它是用来表明如何展示其他低层级的widget.
  • 这个示例的部件树由 包含一个 Text 子部件 的Center Widget 组成 . 这个 Center Widget 将其子部件树排列在屏幕中间 .

步骤 2 : 使用一个外部的程序包

在这个步骤里 , 我们将开始使用一个开源程序包 english_words , 它包含了较多的常用的英文单词还有一些工具方法 .

我们可以在 pub.dartlang.org 找到 english_words 及 其他开源程序包

1. pubspec 文件负责管理 Flutter 应用的资源. 在 pubspec.yaml 文件中,添加 english_words(3.1.0或更高版本)到依赖里.

1
2
3
4
5
6
7
dependencies:
flutter:
sdk: flutter

cupertino_icons: ^0.1.0
english_words: ^3.1.0

**2. **当我们在IDEA 视图中 , 修改yaml文件后 , 可点击右上方的 Packages get 使之生效.它会拉取我们才添加的依赖包, 控制台中打印

1
2
3
flutter packages get
Running "flutter packages get" in flutter_first_app...
Process finished with exit code 0

**3. ** 在 lib/main.dart文件中,添加 import 语句 , 导入依赖相关类

1
import 'package:english_words/english_words.dart';

**4. ** 用开源库生成文本代替原来的 ‘Hello World’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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.
),
),
);
}
}

**5. **若app正在运行, 可以通过点击⚡️按钮进行热部署. 每次点击或者保存时 , 会生成新的一个随机单词. 这是因为单词是在 build(...) 方法中生成, 它会在每次 MaterialApp 需要渲染或触发平台检视时执行.

步骤 3 : 增加一个 Stateful Widget

Stateless widgets 是不可变的 , 意味着其属性是不可改变的 - 所有值均为final .

Stateful widgets 维持着生命周期中可变的状态 . 实现一个 stateful widget 需要至少两个类: 一个 State 类 和 一个创建State示例的 StatefulWidget . StatefulWidget本身是不可变的 , 但是 State 类在widget生成周期中一直存留 .

在这个步骤里 , 我们将添加一个 stateful widget - RandomWords , 它创建自己的 State 类 - RandomWordsState . state 类将为widget最终维持建议的和喜好的单词.

**1. ** 添加 stateful RandomWords 到 main.dart

1
2
3
4
class RandomWords extends StatefulWidget {
@override
createState() => new RandomWordsState();
}

**2. ** 添加 RandomWordsState . 大部分app的代码会在这个类中 , 将维持着 RandomWords 部件的状态 . 这个类将会保存生成的词对 , 它们随着用户滑动页面无线增加 . 然后喜好的词对 , 用户通过点击列表的心形按钮进行添加或移除 .

我们一步一步来创建这个类

1
2
class RandomWordsState extends State<RandomWords> {
}

**3. ** 在添加 state 类后 , IDE会提示错误, 需要我们取实现未实现的方法 .

1
2
3
4
5
6
7
class RandomWordsState extends State<RandomWords> {
@override
Widget build(BuildContext context) {
final wordPair = new WordPair.random();
return new Text(wordPair.asPascalCase);
}
}

**4. ** 移除单词生成代码 ,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 RandomWords(),
),
),
);
}
}

步骤 4 : 创建一个无限滚动的 ListView

这个步骤里, 我们将扩充 RandomWordsState 类 来生成和展示单词对的列表. 当用户滑动页面, ListView widget 展示的列表将会无限增加. ListView 的 builder 工厂构造器允许我们视需懒加载创建列表视图

**1. **在 RandomWordsState 类中添加成员变量 _suggestions 列表用来保存推荐的单词对.在 Dart 语言中 , 以 _下划线开头的变量/方法为私有访问权限.

同样的 , 添加 biggerFont 变量用来使字体大小更大

1
2
3
4
5
6
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];

final _biggerFont = const TextStyle(fontSize: 18.0);
...
}

**2 ~ 3. **添加 _buildSuggestions()方法到 RandomWordsState 类中. 此方法负责构建列表 ListView 和展示建议的单词对.

ListView 类提供了一个 builder 属性 , itemBuilder 一个工厂构建者和指定匿名函数的回调功能.两个参数被传递给函数- BuildContext 还有行迭代器 i . 迭代器从 0 开始且每次方法调用递增.

添加 _buildRow 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Widget _buildSuggestions() {
return new ListView.builder(
// padding 16
padding: const EdgeInsets.all(16.0),
// 每一对单词对调用一次itemBuilder 回调 ,然后放置一个推荐的单词对在行内
// 偶数行 , 函数增加个内容行显示单词对,
// 奇数行 , 函数添加一条分割线小部件 (Divider Widget)去显示分割条目

itemBuilder: (context, i) {
if (i.isOdd) return new Divider();

// index 为 i/2 的余整数
final index = i ~/ 2;
if (index >= _suggestions.length) {
// ...生成10个词对,添加到list
_suggestions.addAll(generateWordPairs().take(10));
}
return _buildRow(_suggestions[index]);
}
);
}

**4. **更新 RandomWordsState 的 build 方法 , 使用 _buildSuggestions ,不再直接使用调用单词生产库.

1
2
3
4
5
6
7
8
9
10
11
12
13
class RandomWordsState extends State<RandomWords> {
...
@override
Widget build(BuildContext context) {
return new Scaffold (
appBar: new AppBar(
title: new Text('Startup Name Generator'),
),
body: _buildSuggestions(),
);
}
...
}

**5. **更新 MyApp 的 build 方法 . 从 MyApp 中移除 Scaffold 和 AppBar 实例. 这些将会被 RandomWordsState 管理 ,这样将更容易地在下个步骤页面跳转时去改变导航栏上的名称.

1
2
3
4
5
6
7
8
9
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
home: new RandomWords(),
);
}
}

步骤 5 : 增加交互

这个步骤中 , 我们将增加可点击的心形图标到每一行 . 当用户点击列表条目时 , 触发其 “favorite” 状态 ,状态改变会将对应单词对添加到保存的集合或从中移除

**1. ** 添加 _saved 集合到 RandomWordsState 里 . 集合存储用户喜好的单词对 , 更倾向于用 Set 是因为 Set 中不允许有重复的条目

1
2
3
4
5
6
7
8
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];

final _saved = new Set<WordPair>();

final _biggerFont = const TextStyle(fontSize: 18.0);
...
}

**2. **在 _buildRow 函数里 , 添加 alreadySaved 变量来检查确保单词对还未被添加到喜好的集合中.

1
2
3
4
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
...
}

**3 ~ 4. **还是在_buildRow 里 , 添加心形图标 . 重启应用 , 我们可以看到心形已被添加 , 只是暂时没有交互事件

1
2
3
4
5
6
7
8
9
10
11
12
13
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,
),
);
}

**5. **在_buildRow 中设置心形可点击. 如果一个单词条目被添加到喜欢的集合时, 再次单击它就能从喜欢的集合中移除 . 当心形被点击 , 函数会调用 setState()去通知框架状态被改变了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 react式的框架中 , 调用 setState() 会为 State 对象触发 build() 方法 , 最后更新到UI上.

步骤 6 : 跳转到新页面

在这个步骤里 , 我们将添加一个页面 (在Flutter里叫 route ) 展示喜好的推荐词对 . 我们将学到如何从主页面导航到新页面 .

在 Flutter 中 , Navigator 管理着一个包含app页面的栈 . 推送一个页面进入 Navigator 的栈中, 则会更新显示这个页面 . 从 Navigator栈中推出一个页面 , 则会显示上一个页面 .

**1 ~ 3. ** 在 RandomWordsState 的 build 方法中给 AppBar 添加一个列表图标 . 当用户点击图标 , 一个包含喜好列表的页面会被推送呈现 .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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(),
);
}
...
}

然后在 RandomWordsState 中添加 _pushSaved方法

1
2
3
4
5
class RandomWordsState extends State<RandomWords> {
...
void _pushSaved() {
}
}

**4 ~ 6. ** 添加 MaterialPageRoute 及它的 builder . 添加代码生成 ListTile 行 . ListTile 的divideTiles() 方法在每一条条目间增加水平距离 . divided 变量保存着最终的行 , 通过函数 toList() 转换为列表

builder 属性返回一个 Scaffold , 包含了新页面的导航栏 ,名为 “Saved Suggestion” .新页面的内容部分由 ListView包含 ListTiles 行组成 , 每一行由一个 divider 分隔 .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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();
},
),
);
}

步骤 7 : 通过主题改变UI

在这个最终步骤中, 我们将改变app的主题 .

**1. **我们可以简单地通过配置 ThemeData 类 改变app的主题 . 当前app是默认主题, 我们将改变主色为紫色

1
2
3
4
5
6
7
8
9
10
11
12
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
home: new RandomWords(),
theme: new ThemeData(
primaryColor: Colors.purple
),
);
}
}

最后贴一下 main.dart 完整代码 , 方便小友们查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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) {
return new MaterialApp(
title: 'Startup Name Generator',
home: new RandomWords(),
theme: new ThemeData(primaryColor: Colors.purple),
);
}
}

class RandomWords extends StatefulWidget {
@override
createState() => new RandomWordsState();
}

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 16
padding: const EdgeInsets.all(16.0),
// 每一对单词对调用一次itemBuilder 回调 ,然后放置一个推荐的单词对在行内
// 偶数行 , 函数增加个内容行显示单词对,
// 奇数行 , 函数添加一条分割线小部件 (Divider Widget)去显示分割条目

itemBuilder: (context, i) {
if (i.isOdd) return new Divider();

// index 为 i/2 的余整数
final index = i ~/ 2;
if (index >= _suggestions.length) {
// ...生成10个词对,添加到list
_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'),
actions: <Widget>[
// new IconButton(icon: new Icon(Icons.colorize),onPressed: _changeTheme),
new IconButton(icon: new Icon(Icons.list), onPressed: _pushSaved)

],
),
body: _buildSuggestions(),
);
}

void _changeTheme(){

}

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),
);
},
),
);
}
}

完成!

至此 , 我们第一个 app 已经完成 . GitHub 地址

功能相对来说较简单 , 但是大体上让我们对开发 Flutter app 有了一定了解. 之后我们将延续阅读官网的教程 , 开始较全面地了解构建UI相关的部分.