# Flutter教程 - 3 Flutter初体验
在上面我们刚创建一个项目就有了一堆的代码,看上去云里雾里不知所云,下面我们从0开始,从Hello World开始熟悉Flutter应用的实现。
# 3.1 Hello World
现在我们来写HelloWorld程序,自动生成的demo程序中的 lib/main.dart
文件是app的程序入口文件,而文件中的 main
方法是app的入口方法,也就是程序是从这里开始执行的。
# 1 初级Hello World
现在将 main.dart
文件中内容全部删掉,从 main
方法开始编写。
import 'package:flutter/material.dart';
void main() {
runApp(从这里开始);
}
2
3
4
5
查看 runApp
方法,会看到参数 Widget
类型,那么什么是 Widget
?
在 Flutter 中,万物都是 Widget
,可以理解为**组件、部件、控件。**文本组件,甚至边距都是 Widget。
在 Flutter 有一个文本的 Widget
,就是 Text
,现在给 runApp
传递一个文本的 Widget
。
import 'package:flutter/material.dart';
void main() {
runApp(Text("Hello World", textDirection:TextDirection.ltr));
}
2
3
4
5
创建 Text 的 Widget,传递了两个参数,一个是用来显示的文本,一个是设置文字的方向,因为这里 Text 没有被任何 Widget 包裹,所以才需要设置 textDirection 属性,否则会报错,在后面的开发中,没有特殊的要求文字显示的方向,不需要设置 textDirection 属性。
重新运行项目,显示效果如下:
可以看到在屏幕的左上角,显示了 Hello World。
但这个 Hello World 也太™寒碜了,下面我们使用 Material
库来优化一下我们的Hello World程序。
在优化 Hello World 之前,先讲解一下热重载和热启动。
# 2 热重载和热重启
在开发 Android 原生 app 的时候,我们修改了代码就需要重新运行程序,效率很低,不能实现热重载。
但在进行 Flutter 开发的时候,我们修改了代码,可以直接进行热重载或热重启。
举个栗子:
我们修改上面的代码中的Hello World文本内容:
import 'package:flutter/material.dart';
void main() {
runApp(Text("Hello World!!!!!!", textDirection:TextDirection.ltr));
}
2
3
4
5
修改完成,直接保存,可以直接点击下面的热重启按钮,则会立刻显示修改的内容,是不是麻雀啄了牛屁股——雀食牛逼啊。
后面我们修改了内容保存后,可以直接使用热重载和热重启。
那么热重载和热重启有什么区别呢?
热重载 可以快速更新 UI,并且保持应用程序的状态,保存应用的状态就是数据还在,就像前面的demo程序,如果我们修改了代码,然后进行热重载,如果我们点击了3次按钮,计数器变为3,使用热重载后,计数器还是3,数据还在。
热启动 会重新启动整个应用程序,并重置状态。
# 3 Material 风格
下面我们使用 Material
风格来优化Hello World。在上面的代码中,我们已经 import 引入了 Material
库,Material是Google提供的一个设计语言和UI组件库。
修改代码如下:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: Scaffold(body: Text("Hello World"))));
}
2
3
4
5
在开发 Flutter 的时候,创建 Widget 的时候,需要传递很多命名参数,为了便于阅读,一般会进行换行处理。上面的代码经过格式化如下:
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
body: Text("Hello World")
)
)
);
}
2
3
4
5
6
7
8
9
10
11
解释一下上面的代码:
MaterialApp
是Flutter中的一个顶级组件,它包装了整个应用程序,并提供了一些应用程序级别的功能,所以它是应用程序的根组件。在创建 MaterialApp 的时候,传递了一个 Scaffold
作为显示的主页,Scaffold
是一个布局组件,相当于一个页面。
Scaffold 的body属性表示是页面显示的body部分,Scaffold还可以包含appBar、tabBar等部分。
所以上面的操作就是使应用更加层次化,创建了一个应用的 Widget,在应用的 Widget 中又创建了一个页面的 Widget,在页面中添加了一个文本的 Widget。
最终的显示效果如下:
卧槽,你会说这是上坟烧报纸——糊弄鬼呢,和之前除了背景不一样,没啥不一样。
下面在这基础上继续进行拓展。
我们先给页面添加一个 appBar
:
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("第一个Flutter程序")
),
body: Text("Hello World")
)
)
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
给 Scaffold
添加了 appBar,并设置了 appBar 的 title
属性。
显示效果如下:
终于像个 Hello World 了吧。下面我们调整一下Hello World显示的位置,将Hello World显示在屏幕中间,并修改一下Hello World的字体大小。
# 4 设置Hello World样式
想要 Hello World 居中显示,这里用到一个 Center 的 Widget,它可以将其子 Widget 相对于中心位置居中对齐。
所以这里我们将 Scaffold 的 body 使用 Center 组件,然后在 Center 组件中放置 Hello World 的 Text。
代码如下:
Center 通过 child 属性设置子组件,Text 通过 style 属性设置样式,上面设置了字体大小和字体颜色。
我们再将右上角的debug给去掉,给 MaterialApp 添加 debugShowCheckedModeBanner 属性。
现在是不是看上去像个Hello World了。
# 3.2 StatelessWidget
上面的 Hello World 感觉看上去代码很乱,各个 Widget 嵌套,都写在 main
函数里面结构很不清晰,现在还只是个 Hello World,如果是真正的项目,页面组件一多,那还了得。
所以我们需要对代码进行封装,创建我们自己的 Widget,将各种组件封装到我们的 Widget中,实现结构的清晰划分。
在 Flutter 中,我们可以创建两种 Widget,StatelessWidget
和 StatefulWidget
,什么区别呢?
- StatelessWidget: 没有状态的Widget,也就是没有数据变化的 Widget,通常这种Widget仅仅是做一些展示工作而已;
- StatefulWidget: 需要保存状态,也就是会有数据变化的Widget,例如 demo 程序中有计数的变化;
# 1 重构Hello World
下面对 Hello World 进行优化,因为 Hello World 中没有数据的变化,所以使用 StatelessWidget
即可。
我们将 MaterialApp
封装在一个 Widget
中,表示整个应用。将 Scaffold
封装在一个 Widget
中,表示的是一个页面。
继承 StatelessWidget
需要重写 build 方法,build 方法需要返回一个 Widget,也就是告诉 Flutter,我们要渲染一个什么组件,例如可以返回Text Widget、Scaffold Widget等。
将 MaterialApp 封装在一个 Widget 中:
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
2
3
4
5
6
7
8
9
home 是一个页面,也就是之前的 Scaffold,我们将 Scaffold 封装到 HomePage 中。
class HomePage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第一个Flutter程序")
),
body: Center(
child: Text(
"Hello World!",
style: TextStyle(
fontSize: 30,
color: Colors.blue
),
),
)
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
当页面内容很多的时候,为了结构清晰,很多页面的组件可能都需要单独封装成 Widget,这里结构简单,只封装到了页面这一层。
这样我们的 main 方法就很简单了,直接创建一个 MyApp就可以了。
void main() {
runApp(MyApp());
}
2
3
因为只有一行代码,所以可以使用箭头函数:
void main() => runApp(MyApp());
完整的代码如下:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
/**
* App根Widget
*/
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
/**
* 页面Widget
*/
class HomePage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第一个Flutter程序")
),
body: Center(
child: Text(
"Hello World!",
style: TextStyle(
fontSize: 30,
color: Colors.blue
),
),
)
);
}
}
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
# 2 优化代码
Hello World已经实现了,但是代码有一些规范性的提示,提示我们优化代码。
下面提示添加一个 key
参数的构造函数。
点击添加会生成一个带有key参数的构造函数。
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
2
3
4
5
6
7
8
9
10
11
这个key有什么作用呢?
这个key是widget的唯一标识,Flutter 可以识别小部件是否在不同的渲染帧之间保持不变,控制是否更新、重建或重新创建小部件,从而优化性能。
但是我们现在的代码还很简单,虽然定义了带key的构造函数,但是根本就没有传递这个参数,没有用到,后面我们再介绍如何使用。这里我们先按照建议,针对widget统一都生成带key的构造函数。
下面提示使用常量构造函数。
在学习dart的时候,已经知道了使用常量构造函数在创建属性值相同的对象时,使用的是相同的内存空间。而在Flutter中,const不仅仅节省创建组件的内存开销,还可以在重新构建组件的时候,不重新构建const组件,从而提高性能。
所以我们根据提示的建议优化我们的代码,代码如下:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
/// App根Widget
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
/// 页面Widget
class HomePage extends StatelessWidget {
const HomePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("第一个Flutter程序")
),
body: const Center(
child: Text(
"Hello World!",
style: TextStyle(
fontSize: 30,
color: Colors.blue
),
),
)
);
}
}
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
需要注意:当最一个组件使用了const的时候,他的子组件也将是const的,所以子组件不用再添加了。在开发中,我们使用const修饰了父组件,后面添加了非const的子组件,要将父组件的const去掉,同时查看是否需要给其他子组件单独添加const。
后面我们在编写代码的时候,可以根据建议进行代码优化。甚至注释也进行了优化。
在 Flutter 中,约定是使用三斜线 ///
来表示文档注释。在使用工具(如 dartdoc
)生成代码文档时,使用 ///
格式的文档注释会被识别,并生成详细的代码文档。
# 3 实现消息列表
下面再来实现一个简单的消息列表,进一步熟悉一下 StatelessWidget
。
实现下图的列表。
我们现在不用太关心布局,后面会学习布局相关的 Widget。
另外我们现在的数据是写死的,所以不涉及状态(数据)的变化,所以这里都使用 StatelessWidget 来实现。
实现上面的列表,我们先构建应用的 Widget,然后构建页面的 Widget,页面的 body 部分是一个列表,列表中是一个个 Item 的 Widget,结构清晰,开搞:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
/// App根Widget
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: MessagePage(),
);
}
}
/// 消息页面
class MessagePage extends StatelessWidget {
const MessagePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("消息")
),
body: ListView(
children: [
],
)
);
}
}
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
上面实现了应用、页面的 Widget,页面的 Body 是一个 ListView,ListView 中有一个 children 参数,里面需要填写列表中的 Item。下面我们来构建一下 消息的 Item 的 Widget。
/// 消息Item
class MessageItem extends StatelessWidget {
final imageUrl;
final name;
final message;
const MessageItem(this.imageUrl, this.name, this.message, {super.key});
Widget build(BuildContext context) {
return Row( // 行
children: [
Image.network(imageUrl),
Column( // 列
children: [
Text(name),
Text(message)
],
),
],
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
我们定义了三个属性,imageUrl、name、message,在 StatelessWidget 中只能定义 final 类型的常量,通过构造函数进行初始化。
然后在 build 方法中返回一个 行组件,行组件中包含了一个图片组件 和 一个 列组件,在 列组件中添加了两个Text 组件,这样构成一个Item的布局。
然后我们在ListView中创建几个 Item。
/// 消息页面
class MessagePage extends StatelessWidget {
const MessagePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("消息")
),
body: ListView(
children: const [
MessageItem("http://doubibiji.com/open-assets/img/telangpu.jpg", "特朗普", "拜登今天肯定拉在裤裆里啦"),
MessageItem("http://doubibiji.com/open-assets/img/pujing.jpg", "普京", "你怎么知道的"),
MessageItem("http://doubibiji.com/open-assets/img/baideng.jpg", "拜登", "胡说,不是今天"),
],
)
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
显示效果:
上面显示的黄黑条是因为内容超过了屏幕。
现在没有设置图片的大小,现在调整一下图片的尺寸,同时设置一下文字的尺寸。
/// 消息Item
class MessageItem extends StatelessWidget {
final imageUrl;
final name;
final message;
const MessageItem(this.imageUrl, this.name, this.message, {super.key});
Widget build(BuildContext context) {
return Row(
children: [
Image.network(
imageUrl,
width: 60, // 设置图片尺寸
height: 60,
),
Column(
children: [
Text(
name,
style: const TextStyle( // 设置字体样式
fontSize: 18
),
),
Text(
message,
style: const TextStyle(
fontSize: 14
),
)
],
),
],
);
}
}
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
显示效果:
再修改一下文字的对齐方式,文字的颜色和文字的间距:
使用 crossAxisAlignment: CrossAxisAlignment.start,
修改文字的方向。间距也是使用 Widget 来实现的,通过 SizedBox
来增加间距。
/// 消息Item
class MessageItem extends StatelessWidget {
final imageUrl;
final name;
final message;
const MessageItem(this.imageUrl, this.name, this.message, {super.key});
Widget build(BuildContext context) {
return Row(
children: [
Image.network(
imageUrl,
width: 60, // 设置图片尺寸
height: 60,
),
const SizedBox(width: 10), // 增加间距
Column(
crossAxisAlignment: CrossAxisAlignment.start, // 文字方向
children: [
Text(
name,
style: const TextStyle( // 设置字体样式
fontSize: 18
),
),
const SizedBox(height: 3), // 增加间距
Text(
message,
style: const TextStyle(
fontSize: 14,
color: Color.fromARGB(255, 100, 100, 100) // 文字颜色
),
)
],
),
],
);
}
}
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
显示效果:
下面再给每个Item增加一个 padding 间距,padding 也是一个 Widget,我们使用 Padding 组件将 Item 包裹起来。
/// 消息Item
class MessageItem extends StatelessWidget {
final imageUrl;
final name;
final message;
const MessageItem(this.imageUrl, this.name, this.message, {super.key});
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(10, 10, 10, 10), // 增加间距
child: Row(
// ...
)
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
显示效果:
现在先不用太纠结如何实现布局,后面再详细介绍布局相关的 Widget。
最终完整代码:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
/// App根Widget
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: MessagePage(),
);
}
}
/// 消息页面
class MessagePage extends StatelessWidget {
const MessagePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("消息")
),
body: ListView(
children: const [
MessageItem("http://doubibiji.com/open-assets/img/telangpu.jpg", "特朗普", "拜登今天肯定拉在裤裆里啦"),
MessageItem("http://doubibiji.com/open-assets/img/pujing.jpg", "普京", "你怎么知道的"),
MessageItem("http://doubibiji.com/open-assets/img/baideng.jpg", "拜登", "胡说,不是今天"),
],
)
);
}
}
/// 消息Item
class MessageItem extends StatelessWidget {
final imageUrl;
final name;
final message;
const MessageItem(this.imageUrl, this.name, this.message, {super.key});
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(10, 10, 10, 10),
child: Row(
children: [
Image.network(
imageUrl,
width: 60, // 设置图片尺寸
height: 60,
),
const SizedBox(width: 10), // 增加间距
Column(
crossAxisAlignment: CrossAxisAlignment.start, // 文字方向
children: [
Text(
name,
style: const TextStyle( // 设置字体样式
fontSize: 18
),
),
const SizedBox(height: 3), // 增加间距
Text(
message,
style: const TextStyle(
fontSize: 14,
color: Color.fromARGB(255, 100, 100, 100) // 文字颜色
),
)
],
),
],
)
);
}
}
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
# 3.3 StatefulWidget
下面来讲解一下带状态的 Widget。
现在也是以实现一个样例的方式,来介绍 StatefulWidget,我们来实现一开始创建项目时实现的计数器功能。
先将页面结构搭建出来,先创建应用的 Widget,然后创建页面的 Widget。
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
/// App根Widget
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: CounterPage(),
);
}
}
/// 计数器页面
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
下面主要来实现CounterPage的 Widget,CounterPage因为有计数的变化,是有状态的StatefulWidget。
编写CounterPage,继承自StatefulWidget。
/// 计数器页面
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
State<StatefulWidget> createState() {
}
}
2
3
4
5
6
7
8
9
我们会发现 StatefulWidget
和 StatelessWidget
有区别,其中实现的方法不是 build
,而是 createState
方法,返回的是一个 State
。
虽然 StatefulWidget 是有状态的,但是 Widget 类中是无法定义状态的,需要重新定一个状态的类。
所以在 CounterPage
中返回一个 CounterState
:
/// 计数器页面
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
State<StatefulWidget> createState() {
return CounterState();
}
}
2
3
4
5
6
7
8
9
下面我们来创建 CounterState
类,在状态类中,我们构建整个页面的显示内容,状态类的build的方法是返回 Widget 的:
class CounterState extends State<CounterPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("计数器")
),
body: const Center(
child: Text(
"0",
style: TextStyle(
fontSize: 30
),
)
)
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
我们在页面添加了AppBar,并在页面中间添加了一个 Text ,并设置了字体大小。
保存运行,显示效果如下:
现在我们要操作,那么需要给页面添加一个按钮,点击按钮来增加计数。
像 demo 一样,我们在右下角添加一个悬浮按钮:
/// 计数器页面
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
State<StatefulWidget> createState() {
return CounterState();
}
}
class CounterState extends State<CounterPage> {
int _counter = 0;
void _incrementCounter() {
setState(() { // 更新变量的值必须在setState方法中更新才会让页面刷新
_counter++;
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("计数器")
),
body: Center(
child: Text(
"$_counter", // 读取_counter的值
style: const TextStyle(
fontSize: 30
),
)
),
floatingActionButton: FloatingActionButton( // 添加悬浮按钮
onPressed: _incrementCounter, // 绑定按钮的点击事件
child: const Icon(Icons.add),
),
);
}
}
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
直接在 Scaffold 中添加一个 floatingActionButton
,floatingActionButton 需要设置点击事件,点击事件需要传递的是一个方法,所以我们创建了一个私有方法 _incrementCounter
,在方法中,我们将 _counter++
。
你会发现,我们根本就没有直接操作组件去修改组件中的内容或显示,这和以前的Android和iOS区别很大,和 Vue 的双向数据绑定是一样的。通过 Widget 和 State 的绑定,当我们修改了 State,Widget会自动更新显示。
所以在上面的代码中,我们点击了按钮,修改了 _counter
的值后,Text 引用并显示了 _counter
的值,所以 Text 会自动重新渲染。
需要注意,修改 State 的值,需要在 setState()
方法中进行修改才有效。
最终的显示效果:
完整代码:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
/// App根Widget
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: CounterPage(),
);
}
}
/// 计数器页面
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
State<StatefulWidget> createState() {
return CounterState();
}
}
class CounterState extends State<CounterPage> {
int _counter = 0;
void _incrementCounter() {
setState(() { // 一定要在setState方法中更新变量
_counter++;
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("计数器")
),
body: Center(
child: Text(
"$_counter",
style: const TextStyle(
fontSize: 30
),
)
),
floatingActionButton: FloatingActionButton( // 添加悬浮按钮
onPressed: _incrementCounter, // 绑定按钮的点击事件
child: const Icon(Icons.add),
),
);
}
}
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
总结:
- 通过 Widget 和 State 的绑定,当我们修改了 State,Widget会自动更新显示;
- 修改 State 的值,需要在
setState()
方法中进行修改才会重新渲染。