安装与运行

Windows

下载安装包:https://flutter.io/sdk-archive/#windows

临时镜像:

1
2
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

在Flutter安装目录的flutter文件下找到flutter_console.bat,双击运行并启动flutter命令行

flutter\bin 添加到环境变量 Path

编译桌面端

  1. 安装go:https://studygolang.com/dl

  2. 安装hover

    1
    2
    3
    go env -w GOPROXY=https://goproxy.cn
    set GO111MODULE=on
    go get -u -a github.com/go-flutter-desktop/hover

    如果出现如下错误提示,则需要升级你的go版本,最低支持1.12

    1
    cmdApp.ProcessState.ExitCode undefined (type *os.ProcessState has no field or method ExitCode)

    还需要确保你的电脑具有GLFW的依赖:看这里https://github.com/go-flutter-desktop/hover,或者这里https://www.glfw.org/docs/latest/compile.html#compile_deps

  3. 编译
    第一次将hover应用于项目时,需要初始化桌面项目。hover init需要项目路径,通常是你在GitHub或者托管到git服务上的项目路径。如:

    1
    hover init github.com/iwxyi/QQ-Notification_Reply

    这路径不重要以后可以随时更改
    执行初始化之后将在项目中创建desktop并添加样板文件,如go代码和默认图标。
    默认flutter项目是以main.dart作为入口文件,编译桌面应用,你需要新建一个名为main_desktop.dart的文件,并且在runApp(..);之前加上以下代码:

    1
    debugDefaultTargetPlatformOverride =  TargetPlatform .fuchsia;

    然后执行hover run启动程序即可,项目启动后,在命令行按r可以热重载。
    要构建一个独立的应用程序可以使用hover build命令。输出在desktop/build/outputs/Windows目录中。

第一个应用

本笔记来源:Flutter中文网

1、Hello World

lib/main.dart文件

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( // Scaffold提供默认的导航栏、标题和主屏幕widget树
appBar: new AppBar(
title: new Text('Welcome to Flutter'),
),
body: new Center(
child: new Text('Hello World'),
),
),
);
}
}

2、Package

pubspec.yaml

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

cupertino_icons: ^0.1.0
english_words: ^3.1.0

点击右上角的 Packages get,会自动下载依赖包

lib/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
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'),
child: new Text(wordPair.asPascalCase),
),
),
);
}
}

3、State

StatefulWidget类本身是不变的,但是 State类在widget生命周期中始终存在

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

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

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

4、ListView

ListView设置方法:ListView.builder( itemBuilder:(context, i) { } )

1
2
3
4
5
6
7
8
9
10
Widget _buildList() {
return new ListView.builder(itemBuilder: (context, i){
// return new Text("Text"); // 简单文字
return new ListTile( // 列表项组
title: new Text("text"),
trailing: new Icon(Icons.favorite),
onTap: (){},
);
});
}
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
/*...*/

class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[]; // 列表项

@override
Widget build(BuildContext context) {
return new Scaffold( // 设置整体界面
appBar: new AppBar(
title: new Text('Flutter'),
),
body: _buildSuggestions(),
);
}

// 在Dart语言中使用下划线前缀标识符,会强制其变成私有的
Widget _buildSuggestions() {
// ListView类提供了一个builder属性,itemBuilder 值是一个匿名回调函数, 接受两个参数
// - BuildContext和行迭代器i。迭代器从0开始, 每调用一次该函数,i就会自增1,对于每个
// 建议的单词对都会执行一次。该模型允许建议的单词对列表在用户滚动时无限增长。
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: new TextStyle(fontSize: 18.0),
),
);
}
}

5、onTap

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
/*...*/

class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _saved = new Set<WordPair>(); // 收藏的单词

/*...*/

// 记录每一项的状态
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
return new ListTile(
title: new Text(
pair.asPascalCase,
style: new TextStyle(fontSize: 18.0),
),
trailing: new Icon(
alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.red : null,
),
onTap: (){ // 会出现点击水波纹动画效果
setState((){ // 调用setState() 会为State对象触发build()方法,导致UI更新
if (alreadySaved) {
_saved.remove(pair); // 移除出收藏夹
} else {
_saved.add(pair); // 加入到收藏夹
}
});
},
);
}
}

6、Router

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
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _saved = new Set<WordPair>();
final _biggerFont = const TextStyle(fontSize: 18.0);

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Flutter'),
// 为AppBar添加一个列表图标,以及点击事件
actions: <Widget>[
new IconButton(icon: new Icon(Icons.list), onPressed: _pushSaved),
],
),
body: _buildSuggestions(),
);
}

/* ... */

void _pushSaved() {
// 添加Navigator.push调用,这会使路由入栈(以后路由入栈均指推入到导航管理器的栈)s
Navigator.of(context).push(
// 添加MaterialPageRoute及其builder
new MaterialPageRoute(builder: (context){
// 生成ListTile行
final tiles = _saved.map((pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
});
// ListTile的divideTiles()方法在每个ListTile之间添加1像素的分割线
final divided = ListTile
.divideTiles(
context: context,
tiles: tiles
).toList();

// builder返回一个Scaffold
return new Scaffold(
appBar: new AppBar(
title: new Text('Saved Suggestions'),
),
body: new ListView(children: divided),
);
}),
);
}

}

7、Theme

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Welcome to Flutter',
// 设置主色调白色(包括应用栏)
theme: new ThemeData(
primaryColor: Colors.white,
),
home: new RandomWords(),
);
}
}

完整代码

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
125
126
127
128
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: 'Welcome to Flutter',
// 设置主色调白色(包括应用栏)
theme: new ThemeData(
primaryColor: Colors.white,
),
home: new RandomWords(),
);
}
}

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

class RandomWordsState extends State<RandomWords> {
// 在Dart语言中使用下划线前缀标识符,会强制其变成私有的
final _suggestions = <WordPair>[]; // 列表项
final _saved = new Set<WordPair>(); // 收藏的单词
final _biggerFont = const TextStyle(fontSize: 18.0);

@override
Widget build(BuildContext context) {
return new Scaffold( // Scaffold提供默认的导航栏、标题和主屏幕widget树
appBar: new AppBar(
title: new Text('Flutter'),
// 为AppBar添加一个列表图标
actions: <Widget>[
new IconButton(icon: new Icon(Icons.list), onPressed: _pushSaved),
],
),
body: _buildSuggestions(),
);
}

/// 加载整个列表
Widget _buildSuggestions() {
// ListView类提供了一个builder属性,itemBuilder 值是一个匿名回调函数, 接受两个参数
// - BuildContext和行迭代器i。迭代器从0开始, 每调用一次该函数,i就会自增1,对于每个
// 建议的单词对都会执行一次。该模型允许建议的单词对列表在用户滚动时无限增长。
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((){ // 调用setState() 会为State对象触发build()方法,导致UI更新
if (alreadySaved) {
_saved.remove(pair); // 移除出收藏夹
} else {
_saved.add(pair); // 加入到收藏夹
}
});
},
);
}

/// 单击事件
void _pushSaved() {
// 添加Navigator.push调用,这会使路由入栈(以后路由入栈均指推入到导航管理器的栈)s
Navigator.of(context).push(
// 添加MaterialPageRoute及其builder
new MaterialPageRoute(builder: (context){
// 生成ListTile行
final tiles = _saved.map((pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
});
// ListTile的divideTiles()方法在每个ListTile之间添加1像素的分割线
final divided = ListTile
.divideTiles(
context: context,
tiles: tiles
).toList();

// builder返回一个Scaffold
return new Scaffold(
appBar: new AppBar(
title: new Text('Saved Suggestions'),
),
body: new ListView(children: divided),
);
}),
);
}
}

Widget总览

Widget目录

基础 Widget

  • Text
  • Row、Column
  • Stack
  • Container:矩形视觉元素

处理手势

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 使用GestureDetector来检测各种输入手势,包括点击、拖动和缩放
return new GestureDetector( // GestureDetector 不具有显示效果
onTap: () {
print('MyButton was tapped!');
},
child: new Container(
height: 36.0,
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.symmetric(horizontal: 8.0),
decoration: new BoxDecoration(
borderRadius: new BorderRadius.circular(5.0),
color: Colors.lightGreen[500],
),
child: new Center(
child: new Text('Engage'),
),
),
);
}
}

StatefulWidget

每次点击“Increment”按钮,显示的Count+1

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
class _CounterState extends State<Counter> {
int _counter = 0;

void _increment() {
setState(() {
// 告诉Flutter这个State发生改变,并通过build()刷新界面
_counter++;
});
}

// 多次build(),class中的成员变量保持不变
@override
Widget build(BuildContext context) {
// 每次setState()被调用(即_increment()),立即运行build()
// 任何更新界面的改动都会在这里
return new Row(
children: <Widget>[
new RaisedButton(
onPressed: _increment,
child: new Text('Increment'),
),
new Text('Count: $_counter'),
],
);
}
}

Stateless+Stateful

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
import 'package:flutter/material.dart';

void main() {
runApp(new MaterialApp(
title: 'Flutter Tutorial',
home: new Counter(),
));
}

// 计数显示
class CounterDisplay extends StatelessWidget {
CounterDisplay({this.count});

final int count;

@override
Widget build(BuildContext context) {
return new Text('Count: $count');
}
}

// 计数按钮
class CounterIncrementor extends StatelessWidget {
CounterIncrementor({this.onPressed});

final VoidCallback onPressed; // 按下按钮的回调

@override
Widget build(BuildContext context) {
return new RaisedButton(
onPressed: onPressed,
child: new Text('Increment'),
);
}
}

// 计数组件
class Counter extends StatefulWidget {
@override
_CounterState createState() => new _CounterState();
}

// 计数State
class _CounterState extends State<Counter> {
int _counter = 0;

void _increment() {
setState(() {
++_counter;
});
}

@override
Widget build(BuildContext context) {
return new Row(children: <Widget>[
new CounterIncrementor(onPressed: _increment),
new CounterDisplay(count: _counter),
]);
}
}

购物车示例

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
import 'package:flutter/material.dart';

class Product {
const Product({this.name});
final String name;
}

typedef void CartChangedCallback(Product product, bool inCart);

// 单个Item的控件
class ShoppingListItem extends StatelessWidget {
ShoppingListItem({Product product, this.inCart, this.onCartChanged})
: product = product,
super(key: new ObjectKey(product)); // 属性值作为构造函数参数传入

final Product product;
final bool inCart;
final CartChangedCallback onCartChanged;

Color _getColor(BuildContext context) {
return inCart ? Colors.black54 : Theme.of(context).primaryColor;
}

TextStyle _getTextStyle(BuildContext context) {
if (!inCart) return null;

return new TextStyle(
color: Colors.black54,
decoration: TextDecoration.lineThrough,
);
}

@override
Widget build(BuildContext context) {
return new ListTile(
onTap: () {
onCartChanged(product, !inCart);
},
leading: new CircleAvatar( // 圆角图像框
backgroundColor: _getColor(context),
child: new Text(product.name[0]), // 名字首字母作为头像
),
title: new Text(product.name, style: _getTextStyle(context)),
);
}
}

ShoppingListItem widget是无状态的。它将其在构造函数中接收到的值存储在final成员变量中,然后在build函数中使用它们。 例如,inCart布尔值表示在两种视觉展示效果之间切换:一个使用当前主题的主色,另一个使用灰色。

当用户点击列表项时,widget不会直接修改其inCart的值。相反,widget会调用其父widget给它的onCartChanged回调函数。 此模式可让您在widget层次结构中存储更高的状态,从而使状态持续更长的时间。在极端情况下,存储传给runApp应用程序的widget的状态将在的整个生命周期中持续存在。

当父项收到onCartChanged回调时,父项将更新其内部状态,这将触发父项使用新inCart值重建ShoppingListItem新实例。 虽然父项ShoppingListItem在重建时创建了一个新实例,但该操作开销很小,因为Flutter框架会将新构建的widget与先前构建的widget进行比较,并仅将差异部分应用于底层RenderObject


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
class ShoppingList extends StatefulWidget {
ShoppingList({Key key, this.products}) : super(key: key);

final List<Product> products; // 先作为构造函数参数传入,之后传递到 State 中

@override
_ShoppingListState createState() => new _ShoppingListState();
}

class _ShoppingListState extends State<ShoppingList> {
Set<Product> _shoppingCart = new Set<Product>(); // 延长Item值生命周期

// Item 单击事件,作为参数传入 item 的 Widget 中
void _handleCartChanged(Product product, bool inCart) {
setState(() { // 通知更新界面,即重新调用 build()
if (inCart)
_shoppingCart.add(product);
else
_shoppingCart.remove(product);
});
}

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Shopping List'),
),
body: new ListView(
padding: new EdgeInsets.symmetric(vertical: 8.0),
// 获取并遍历ShoppingList.products,从 Widget 中传递到 State
// ☆返回单行控件,List<Product> => Map<Widget> => List<Widget>
children: widget.products.map((Product product) {
return new ShoppingListItem(
product: product,
inCart: _shoppingCart.contains(product),
onCartChanged: _handleCartChanged,
);
}).toList(),
),
);
}
}

void main() {
runApp(new MaterialApp(
title: 'Shopping App',
home: new ShoppingList(
products: <Product>[ // 直接拟造数据list
new Product(name: 'Eggs'),
new Product(name: 'Flour'),
new Product(name: 'Chocolate chips'),
],
),
));
}

当这个widget的父级重建时,父级将创建一个新的ShoppingList实例,但是Flutter框架将重用已经在树中的_ShoppingListState实例,而不是再次调用createState创建一个新的。

要访问当前ShoppingList的属性,_ShoppingListState可以使用它的widget属性。 如果父级重建并创建一个新的ShoppingList,那么 _ShoppingListState也将用新的widget值重建。如果希望在widget属性更改时收到通知,则可以覆盖didUpdateWidget函数,以便将旧的oldWidget与当前widget进行比较。

生命周期事件

在StatefulWidget调用createState之后,框架将新的状态对象插入树中,然后调用状态对象的initState。 子类化State可以重写initState,以完成仅需要执行一次的工作。

Key

使用key来控制框架将在widget重建时与哪些其他widget匹配。要求两个widget具有相同的keyruntimeType

  • 如果没有key,当前构建中的第一个条目将始终与前一个构建中的第一个条目同步
  • 通过给列表中的每个条目分配为“语义” key,无限列表可以更高效,因为框架将同步条目与匹配的语义key并因此具有相似(或相同)的可视外观。此外,语义上同步条目意味着在有状态子widget中,保留的状态将附加到相同的语义条目上,而不是附加到相同数字位置上的条目。

全局 Key

使用全局key来唯一标识子widget。全局key在整个widget层次结构中必须是全局唯一的,这与局部key不同,后者只需要在同级中唯一。由于它们是全局唯一的,因此可以使用全局key来检索与widget关联的状态。


零散控件笔记

NestedScrollView

详细说明:NestedScrollView类

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
final _tabs = ['Tab1', 'Tab2', 'Tab3'];

DefaultTabController(
length: _tabs.length,
child: NestedScrollView( // 滚动嵌套
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber( // 水平重叠控件
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
child: SliverAppBar( // 可收起的标题栏
title: const Text('Books'), // 标题栏文字
pinned: true, // 是否阻止标题栏向上收起
expandedHeight: 150.0, // 可收起的距离
forceElevated: innerBoxIsScrolled,
bottom: TabBar( // 添加水平Tab标签
tabs: _tabs.map((String name) => Tab(text: name)).toList(),
),
),
),
];
},
body: TabBarView( // 可水平滑动的大页面
children: _tabs.map((String name) { // 添加每个Tab子页面
return SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (BuildContext context) {
return CustomScrollView( // 竖向列表框
key: PageStorageKey<String>(name),
slivers: <Widget>[
SliverOverlapInjector( // 支持多个重叠手势?
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverFixedExtentList( // 添加竖向列表框items
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
);
},
childCount: 30,
),
),
),
],
);
},
),
);
}).toList(),
),
),
)