有状态的组件 StatefulWidget

之前我们学习的组件都是自定义了一个StatelessWidget组件来进行学习的,但是那样的页面是静态的,不能动态改变,如果我们想要实现点击按钮,让页面上的值进行改变,我们需要用到StatefulWidget组件来实现。这是一个有状态组件,可以刷新页面的状态

比如我们点击按钮,上数字增加

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
void main() {
runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text("你好Flutter")),
body: const MyHomePage()),
));
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Center(
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Text(
"$_count",
style: Theme.of(context).textTheme.headline2,
),
const SizedBox(
height: 60,
),
ElevatedButton(
onPressed: () {
setState(() {
// 执行setState的时候,会重新build,然后显示页面
_count++;
});
},
child: const Text("增加数据"))
]),
);
}
}

实现一个动态增加的列表,如下图

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
void main() {
runApp(const MaterialApp(
home: MyHomePage(),
));
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
final List<String> _list = ["我是原来的数据"];
@override
Widget build(BuildContext context) {
return Scaffold(
// 把scaffold放在子组件中,方便用其中的floatingActionButton属性
appBar: AppBar(title: const Text("你好Flutter")),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
_list.add("我是新增数据");
});
},
child: const Icon(Icons.add),
),
body: ListView(
children: _list
.map((e) => ListTile(
title: Text(e),
))
.toList(),
));
}
}

Scaffold 组件属性

bottomNavigationBar 自定义底部导航

items List底部导航条按钮集合
iconSize 配置菜单大小
currentlridex 默认选中第几个
onTap 选中变化回调函数
fixedColor 选中的颜色
type

  • BottomNavigationBarType.fixed 如果底部有4个或4个以上的菜单,就要配置这个,否则会被挤掉
  • BottomNavigationBarType.shifting

下图是点击导航栏切换的演示图

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

void main() {
runApp(MaterialApp(
title: "Flutter Demo",
theme: ThemeData(primaryColor: Colors.blue),
home: const MyHomePage(),
));
}

class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});

@override
Widget build(BuildContext context) {
return const Tabs();
}
}

class Tabs extends StatefulWidget {
const Tabs({super.key});
@override
State<Tabs> createState() => _TabsState();
}
class _TabsState extends State<Tabs> {
int currentindex = 1;
final List<Widget> _pages = const [MyCategory(), Myhome(), MySettings()];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Flutter App"),
),
body: _pages[currentindex],
bottomNavigationBar: BottomNavigationBar(
onTap: (index) {
setState(() {
currentindex = index;
});
},
currentIndex: currentindex,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.category), label: "分类"),
BottomNavigationBarItem(icon: Icon(Icons.home), label: "主页"),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: "设置"),
]),
);
}
}

class Myhome extends StatelessWidget {
const Myhome({super.key});
@override
Widget build(BuildContext context) {
return const Text("这个是home页面");
}
}
class MyCategory extends StatelessWidget {
const MyCategory({super.key});
@override
Widget build(BuildContext context) {
return const Text("这个是category页面");
}
}
class MySettings extends StatelessWidget {
const MySettings({super.key});
@override
Widget build(BuildContext context) {
return const Text("这个是Settings页面");
}
}

floatingActionButton 浮动按钮

通过floatingActionButton来实现一个导航栏中间突起的圆形按钮,在上面导航栏的代码上直接进行修改,效果入下图

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
class _TabsState extends State<Tabs> {
int currentindex = 1;
final List<Widget> _pages = const [MyCategory(), Myhome(), MySettings()];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Flutter App"),
),
body: _pages[currentindex],
bottomNavigationBar: BottomNavigationBar(
onTap: (index) {
setState(() {
currentindex = index;
});
},
currentIndex: currentindex,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.category), label: "分类"),
BottomNavigationBarItem(icon: Icon(Icons.home), label: "主页"),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: "设置"),
]),
floatingActionButton: Container( // 浮动的按钮 ,外面包裹一个Container可以改变按钮的大小等
height: 60,
width: 60,
padding: const EdgeInsets.all(2),
margin: const EdgeInsets.only(top: 6),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
),
child: FloatingActionButton(
backgroundColor: currentindex == 1 ? Colors.blue : Colors.yellow,
onPressed: () {
setState(() {
currentindex = 1;
});
},
child: const Icon(Icons.add),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
);
}
}

Drawer侧边栏

Flutter 中通过 drawer 可以配置左侧侧边栏,endDrawer来配置右侧侧边栏,如果没有设置 AppBarleading 属性,则当使用 Drawer 的时候会自动显示一个 IconButton 来告诉用户有侧边栏(在 Android 上通常是显示为三个横的图标)

Drawer 组件的属性

  • backgroundColor 整个Drawer的背景颜色
  • elevation 设置 z 轴的 elevation,和父容器相关,阴影的部分
  • width 画出来侧边栏的宽度
  • child 子组件

在侧边栏Drawer组件中,可以在child中放一个ListView组件或者Column组件来设置自己的布局,我们会在列组件的children 属性中放一个 DrawerHeader 组件,设置侧边栏的头部。 我们还有一个 UerAccountDrawerHeader 组件,可以快速形成一个个人名片样子的侧边栏头部。

DrawerHeader 组件的属性

  • decoration 设置顶部颜色背景图片等
  • margin 外边距
  • padding 内边距
  • child 子元素

UerAccountDrawerHeader 组件的属性

  • decoration 设置顶部背景颜色
  • accountName 账户名称
  • accountEmail 账户邮箱
  • currentAccountPicture 用户头像
  • otherAccountsPictures 用来设置当前账户其他账户头像
  • onDetailsPressed 点击监听
  • margin 外边距
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
import 'package:flutter/material.dart';

void main() {
runApp(MaterialApp(
title: "Flutter Demo",
theme: ThemeData(primaryColor: Colors.blue),
home: const MyHomePage(),
));
}

class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});

@override
Widget build(BuildContext context) {
return const Tabs();
}
}

class Tabs extends StatefulWidget {
const Tabs({super.key});

@override
State<Tabs> createState() => _TabsState();
}

class _TabsState extends State<Tabs> {
int currentindex = 1;
final List<Widget> _pages = const [MyCategory(), Myhome(), MySettings()];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Flutter App"),
),
drawer: Drawer(
// backgroundColor: Colors.black38,
// elevation: 1,
// shape: ,
width: 300,
// shape: const ShapeBorder(),
semanticLabel: "sdfsdfsdfsdf",
child: Column(
children: const [
SizedBox(
width: double.infinity, // 外面套一个SizedBox让组件平铺
child: DrawerHeader(
decoration: BoxDecoration(color: Colors.yellow),
// margin: EdgeInsets.all(10),
duration: Duration(seconds: 20),
child: Text("头部")) // 这里可以写别的组件进行自己的布局
),
ListTile(
leading: CircleAvatar(child: Icon(Icons.person)),
title: Text("个人中心"),
),
Divider(),
ListTile(
leading: CircleAvatar(child: Icon(Icons.settings)),
title: Text("系统设置"),
),
Divider()
],
)),
endDrawer: const Drawer(child: Text("右侧侧边栏")),
body: _pages[currentindex],
bottomNavigationBar: BottomNavigationBar(
onTap: (index) {
setState(() {
currentindex = index;
});
},
currentIndex: currentindex,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.category), label: "分类"),
BottomNavigationBarItem(icon: Icon(Icons.home), label: "主页"),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: "设置"),
]),
);
}
}

class Myhome extends StatelessWidget {
const Myhome({super.key});

@override
Widget build(BuildContext context) {
return const Text("这个是home页面");
}
}

class MyCategory extends StatelessWidget {
const MyCategory({super.key});

@override
Widget build(BuildContext context) {
return const Text("这个是category页面");
}
}

class MySettings extends StatelessWidget {
const MySettings({super.key});

@override
Widget build(BuildContext context) {
return const Text("这个是Settings页面");
}
}

AppBar顶部导航栏

AppBar 自定义顶部按钮图标、颜色

AppBar 的一些属性

leading 在标题前面显乐的一个控件,在首页通常显示应用的logo;在其他界面通常显示为返回按钮
title 标题,通常显示为当前界面的标题文字,可以放组件
actions 通常使用IconButton来表示,可以放按钮组
backgroundColor 导航背景颜色
iconTheme 图标样式
centerTitle 标题是否居中显示
bottom 通常放tabBar,标题下面显示一个Tab导航栏

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(MaterialApp(
debugShowCheckedModeBanner: false,
title: "Flutter Demo",
theme: ThemeData(primaryColor: Colors.blue),
home: const MyHomePage(),
));
}

class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});

@override
Widget build(BuildContext context) {
return const Tabs();
}
}

class Tabs extends StatefulWidget {
const Tabs({super.key});

@override
State<Tabs> createState() => _TabsState();
}

class _TabsState extends State<Tabs> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
// 前面左侧的图标
icon: const Icon(Icons.menu),
onPressed: () {
print("左侧的按钮");
},
),
title: const Text("顶部导航"), // 顶部导航显示的标题
backgroundColor: Colors.red, // 设置背景颜色的红色
actions: [
// 右侧的图标,可以有多个
IconButton(
onPressed: () {
print("右侧的按钮");
},
icon: const Icon(Icons.search)),
IconButton(
onPressed: () {
print("右侧的按钮");
},
icon: const Icon(Icons.more_horiz)),
],
centerTitle: true, // 标题居中显示
),
body: Text("我是主页"),
);
}
}

AppBar 配合 TabBarTabBarView 实现滑动效果

TabBar 的一些属性

tabs 显示的标签内容,一般使用Tab对象,也可以是其它的Widget
controller TabController对象
isScrollable 是否可以滚动
indicatorColor 指示器颜色 ,指示器就是下面那个条条,指示当前选中的是哪一个
indicatorWeight 指示器高度
indicatorPadding 底部指示器的Padding
indicator 指示器decoration,比如边框等
indicatorSize 指示器大小计算方式,

  • TabBarIndicatorSize.label 跟文字等宽
  • TabBarIndicatorSize.tab 跟每个tab等宽
    labelColor 选中label的颜色
    labelStyle 选中label的Style
    labelPadding 每个label的padding值
    unselectedLabelStyle 未选中label的Style
    unselectedLabelStyle 未选中label的Style

首先我们要在我们的类中 with SingleTickerProviderStateMixin 然后定义TabController 并指定它的长度

在这之后,在AppBarbottom 属性中,传入TabBar组件,并在TabBar组件中传入刚才定义的TabController ,最后在Scaffoldbody属性中使用TabBarView 组件,也传入刚才定义的 TabController ,并在children 属性中传入想要布局的组件,注意:TabBartabs 属性和 TabBarViewchilidren 属性中的元素个数要和TabController 在定义的时候传入的一样

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
void main() {
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
title: "Flutter Demo",
theme: ThemeData(primaryColor: Colors.blue),
home: const MyHomePage(),
));
}

class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});

@override
Widget build(BuildContext context) {
return const Tabs();
}
}

class Tabs extends StatefulWidget {
const Tabs({super.key});

@override
State<Tabs> createState() => _TabsState();
}

class _TabsState extends State<Tabs> with SingleTickerProviderStateMixin {
late TabController _tabController;

// 生命周期函数:当组件初始化的时候就会触发
@override
void initState() {
// TODO: implement initState
super.initState();
_tabController = TabController(length: 9, vsync: this);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
// 前面左侧的图标
icon: const Icon(Icons.menu),
onPressed: () {
print("左侧的按钮");
},
),
title: const Text("顶部导航"), // 顶部导航显示的标题
backgroundColor: Colors.red, // 设置背景颜色的红色
actions: [
// 右侧的图标,可以有多个
IconButton(
onPressed: () {
print("右侧的按钮");
},
icon: const Icon(Icons.search)),
IconButton(
onPressed: () {
print("右侧的按钮");
},
icon: const Icon(Icons.more_horiz)),
],
centerTitle: true, // 标题居中显示
bottom: TabBar(
controller: _tabController,
isScrollable: true, // 可以滚动,
indicatorColor: Colors.white, // 指示器颜色
indicatorWeight: 2, // 指示器高度
indicatorPadding:
const EdgeInsets.all(5), // 指示器和上下左右的间距都是5,视觉效果就是浮在空中
indicatorSize: TabBarIndicatorSize.label, // 指示器和文字等宽
labelColor: Colors.yellow,
unselectedLabelColor: Colors.white,
labelStyle: const TextStyle(fontSize: 15),
unselectedLabelStyle: const TextStyle(fontSize: 12),
tabs: const [
Tab(
child: Text("关注"),
),
Tab(
child: Text("热门"),
),
Tab(
child: Text("视频"),
),
Tab(
child: Text("关注"),
),
Tab(
child: Text("热门"),
),
Tab(
child: Text("视频"),
),
Tab(
child: Text("关注"),
),
Tab(
child: Text("热门"),
),
Tab(
child: Text("视频"),
),
]),
),
body: TabBarView(controller: _tabController, children: [
Container(
child: ListView(
children: const [
ListTile(
title: Text("我是关注container列表"),
)
],
),
),
ListView(
children: const [
ListTile(
title: Text("我是视频列表"),
)
],
),
ListView(
children: const [
ListTile(
title: Text("我是热门列表"),
)
],
),
ListView(
children: const [
ListTile(
title: Text("我是关注列表"),
)
],
),
ListView(
children: const [
ListTile(
title: Text("我是视频列表"),
)
],
),
ListView(
children: const [
ListTile(
title: Text("我是热门列表"),
)
],
),
ListView(
children: const [
ListTile(
title: Text("我是关注列表"),
)
],
),
ListView(
children: const [
ListTile(
title: Text("我是视频列表"),
)
],
),
ListView(
children: const [
ListTile(
title: Text("我是热门列表"),
)
],
),
]),
);
}
}

在有底部导航栏的页面加载顶部导航栏,只需要在body挂在的页面中也使用Scaffold组件就好了,在下面的代码中,我们还实现了获取点击的index,实际开发时,可以用这个index请求不同的数据,并且封装一个 keepAliveWrapper 用来缓存页面当前的状态。

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
/*封装的 KeepAliveWrapper 组件*/
import 'package:flutter/material.dart';
class KeepAliveWrapper extends StatefulWidget {
const KeepAliveWrapper(
{super.key, @required this.child, this.keepAlive = true});

final Widget? child;
final bool keepAlive;
@override
State<KeepAliveWrapper> createState() => _KeepAliveWrapperState();
}
class _KeepAliveWrapperState extends State<KeepAliveWrapper>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
return widget.child!;
}
@override
bool get wantKeepAlive => widget.keepAlive;
@override
void didUpdateWidget(covariant KeepAliveWrapper oldWidget) {
// keepAlive 状态药更新,实现在AutomatickKeepAliveClientMixin中
if (oldWidget.keepAlive != widget.keepAlive) {
updateKeepAlive();
}
super.didUpdateWidget(oldWidget);
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
import 'package:flutter/material.dart';
import './keepAliveWrapper.dart';

void main() {
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
title: "Flutter Demo",
theme: ThemeData(primaryColor: Colors.blue),
home: const MyHomePage(),
));
}

class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});

@override
Widget build(BuildContext context) {
return const Tabs();
}
}

class Tabs extends StatefulWidget {
const Tabs({super.key});

@override
State<Tabs> createState() => _TabsState();
}

class _TabsState extends State<Tabs> {
int currentindex = 1;
final List<Widget> _pages = const [MyCategory(), Myhome(), MySettings()];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Flutter App"),
backgroundColor: Colors.red,
),
body: _pages[currentindex],
bottomNavigationBar: BottomNavigationBar(
onTap: (index) {
setState(() {
currentindex = index;
});
},
currentIndex: currentindex,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.category), label: "分类"),
BottomNavigationBarItem(icon: Icon(Icons.home), label: "主页"),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: "设置"),
]),
);
}
}

class Myhome extends StatefulWidget {
const Myhome({super.key});

@override
State<Myhome> createState() => _MyhomeState();
}

class _MyhomeState extends State<Myhome> with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
// _tabController.addListener(() {
// if (_tabController.animation!.value == _tabController.index) {
// print(_tabController.index); // 得到索引我们就可以通过这个索引来请求对于的数据
// }
// }); // 添加一个监听,可以获取到对应的索引值
}

// 组件销毁的时候触发
@override
void dispose() {
// TODO: implement dispose
super.dispose();
_tabController.dispose();
print("销毁了");
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(40), // 可以配置appbar的高度
child: AppBar(
elevation: 1, // 底部阴影的宽度
title: SizedBox(
// 套一个SizedBox 可以设置TabBar的高度
height: 40,
child: TabBar(
controller: _tabController,
indicatorColor: Colors.red,
labelStyle: const TextStyle(fontSize: 15),
labelColor: Colors.red,
unselectedLabelColor: Colors.black,
unselectedLabelStyle: const TextStyle(fontSize: 12),
onTap: (index) {
// 只能监听点击事件,不能监听滑动事件
print(index);
},
tabs: const [
Tab(
child: Text("关注"),
),
Tab(
child: Text("热门"),
),
Tab(
child: Text("视频"),
),
]),
), // 顶部导航显示的标题
backgroundColor: Colors.white, // 设置背景颜色的红色
centerTitle: true, // 标题居中显示
),
),
body: TabBarView(controller: _tabController, children: [
KeepAliveWrapper(
child: ListView(
children: const [
ListTile(
title: Text("我是关rntainer列表"),
),
ListTile(
title: Text("我1ntai3表"),
),
ListTile(
title: Text("我是2ontainer列表"),
),
ListTile(
title: Text("我是关注contterhhh表"),
),
ListTile(
title: Text("我是关注container列表"),
),
ListTile(
title: Text("我是关注2iner列表"),
),
ListTile(
title: Text("我是关注contai123ner列表"),
),
ListTile(
title: Text("我是关注c12列表"),
),
ListTile(
title: Text("我是关注2iner列表"),
),
ListTile(
title: Text("我是关注contai123ner列表"),
),
ListTile(
title: Text("我是关注c12列表"),
),
],
),
),
ListView(
children: const [
ListTile(
title: Text("我是热门列表"),
)
],
),
ListView(
children: const [
ListTile(
title: Text("我是关注列表"),
)
],
)
]),
);
}
}

class MyCategory extends StatelessWidget {
const MyCategory({super.key});

@override
Widget build(BuildContext context) {
return const Text("这个是category页面");
}
}

class MySettings extends StatelessWidget {
const MySettings({super.key});

@override
Widget build(BuildContext context) {
return const Text("这个是Settings页面");
}
}

Flutter 中的路由

Flutter中的路由通俗的讲就是页面跳转。在Flutter中通过Navigator组件管理路由导航并提供了管理堆栈的方法。如:Navigator.pushNavigator.pop 。Flutter中给我们提供了两种配置路由跳转的方式:1、基本路由2、命名路由

普通路由的使用

在主页定义一个按钮,点击后跳转到别的页面并传值,然后在其它页面中点击返回

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

// 接受传过来的参数
class FormPage extends StatefulWidget {
final String title;
const FormPage({super.key, required this.title});

@override
State<FormPage> createState() => _FormPageState();
}

class _FormPageState extends State<FormPage> {
@override
void initState() {
// TODO: implement initState
super.initState();
print(widget.title); // 这就是获取到上面的属性
}

@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.pop(context), // 返回上一页
child: const Icon(Icons.home),
),
appBar: AppBar(
title: Text(widget.title), // 获取属性
),
body: const Center(child: Text("我是表单页面")),
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*SearchPage*/
import 'package:flutter/material.dart';

class SearchPage extends StatefulWidget {
const SearchPage({super.key});

@override
State<SearchPage> createState() => _SearchPageState();
}

class _SearchPageState extends State<SearchPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("搜索页面标题"),
),
body: const Center(
child: Text("搜索页面"),
),
);
}
}
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
import 'package:flutter/material.dart';
import './search.dart';
import './form.dart';

void main() {
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
title: "Flutter Demo",
theme: ThemeData(primaryColor: Colors.blue),
home: const MyHomePage(),
));
}

class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});

@override
Widget build(BuildContext context) {
return const Tabs();
}
}

class Tabs extends StatefulWidget {
const Tabs({super.key});

@override
State<Tabs> createState() => _TabsState();
}

class _TabsState extends State<Tabs> {
int currentindex = 1;
final List<Widget> _pages = const [MyCategory(), MySettings()];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Flutter App"),
backgroundColor: Colors.red,
),
body: _pages[currentindex],
bottomNavigationBar: BottomNavigationBar(
onTap: (index) {
setState(() {
currentindex = index;
});
},
currentIndex: currentindex,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.category), label: "分类"),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: "设置"),
]),
);
}
}

class MyCategory extends StatelessWidget {
const MyCategory({super.key});

@override
Widget build(BuildContext context) {
return const Text("这个是category页面");
}
}

class MySettings extends StatelessWidget {
const MySettings({super.key});

@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (BuildContext context) {
return const SearchPage();
}));
},
child: const Text("点击搜索")),
ElevatedButton(
onPressed: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (BuildContext context) {
return const FormPage(title: "传过来的值呀");
}));
},
child: const Text("点击到表单"))
],
),
);
}
}

命名路由

为了方便管理我们的路由,我们还可以使用命名路由

首先在main.dart 中将我们的页面全部引入,然后在MaterialApp 组件中有一个routes 属性,在这里面配置路由的名称,最后再使用就好了

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

class SearchPage extends StatefulWidget {
final Map arguments;
const SearchPage({super.key, required this.arguments});
@override
State<SearchPage> createState() => _SearchPageState();
}

class _SearchPageState extends State<SearchPage> {
@override
void initState() {
// TODO: implement initState
super.initState();
print(widget.arguments);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("搜索页面标题"),
),
body: const Center(
child: Text("搜索页面"),
),
);
}
}
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
import 'package:flutter/material.dart';
import './search.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
MyApp({super.key});
// 配置路由
Map routes = {
'/': (context) => const Tabs(),
'/search': (context, {arguments}) => SearchPage(
// 这里要和那边接受的名字一样
arguments: arguments,
)
};

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: "Flutter Demo",
theme: ThemeData(primaryColor: Colors.blue),
// home: const MyHomePage(),
initialRoute: "/", // 刚开始要加载的路由
/// 配置onGenerateRoute
onGenerateRoute: (RouteSettings settings) {
final String? name = settings.name; // 获取命名路由的名称
final Function? pageContentBuilder =
routes[name]; // 把上面map中的function拿过来
if (pageContentBuilder != null) {
if (settings.arguments != null) {
final Route route = MaterialPageRoute(
builder: (context) =>
pageContentBuilder(context, arguments: settings.arguments));
return route;
} else {
final Route route = MaterialPageRoute(
builder: (context) => pageContentBuilder(context));
return route;
}
}
return null;
},
);
}
}

class Tabs extends StatefulWidget {
const Tabs({super.key});

@override
State<Tabs> createState() => _TabsState();
}

class _TabsState extends State<Tabs> {
int currentindex = 1;
final List<Widget> _pages = const [MyCategory(), MySettings()];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Flutter App"),
backgroundColor: Colors.red,
),
body: _pages[currentindex],
bottomNavigationBar: BottomNavigationBar(
onTap: (index) {
setState(() {
currentindex = index;
});
},
currentIndex: currentindex,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.category), label: "分类"),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: "设置"),
]),
);
}
}

class MyCategory extends StatelessWidget {
const MyCategory({super.key});

@override
Widget build(BuildContext context) {
return const Text("这个是category页面");
}
}

class MySettings extends StatelessWidget {
const MySettings({super.key});

@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () =>
Navigator.pushNamed(context, '/search', arguments: {
"name": "sdfsdf",
"chuan": 1,
}),
child: const Text("命名路由search"),
)
],
),
);
}
}

Flutter返回上一级路由

1
Navigator.of(context).pop();

Flutter中替换路由

比如我们从用户中心页面跳转到了registerFirst页面,然后从registerFirst页面通过pushReplacementNamed跳转到了registerSecond页面。这个时候当我们点击registerSecond的返回按钮的时候它会直接返回到用户中心。

1
Navigator.of(context).pushReplacementNamed('/registersecond');

Flutter返回到根路由

比如我们从用户中心跳转到registerFirsti顷面,然后从registerFirsti顷面跳转到registerSecond页面,然后从registerSecond跳转到了registerThird页面。这个时候我们想的是registerThird注册成功后返回到用户中心。这个时候就用到了返回到根路由的方法。

1
2
3
4
5
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder:(BuildContext context){
return const Tabs(); // 这个是根组件
})/MaterialPageRoute
(route)=false);

同样风格的路由跳转

Material组件库中提供了一个MaterialPageRoute组件,它可以使用和平台风格一致的路由切换动画,如在iOS上会左右滑动切换,而在Android上会上下滑动切换,CupertinoPageRoute是Cupertino组件库提供的iOS风格的路由切换组件如果在Android上也想使用左右切换风格,可以使用CupertinoPageRoute.

  1. 引入 import 'package:flutter/cupertino.dart'
  2. MaterialPageRoute改为CupertinoPageRoute

全局配置主题

1
2
3
4
5
6
7
8
9
10
11
12
return MaterialApp(
debugshowcheckedModeBanner:false,
title:'Flutter Demo',
theme:ThemeData(
primaryswatch:Colors.blue,
appBarTheme:const AppBarTheme(
centerTitle:true, // 所有的标题都在中间
)
),
initialRoute:"/",
onGenerateRoute:onGenerateRoute,
);

Dialog组件

警告弹窗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void _alertDialog() async {
var result = await showDialog(context:context,builder:(context){
return AlertDialog( // 警告弹窗
title:const Text("提示信息!"),
content:const Text("您确定要删除吗"),
actions:[
TextButton(onPressed:(){
print("ok");
Navigator.of(context).pop("ok");//点击按钮让AlertDialog消失
},child:const Text("确定")),//TextButton
TextButton(onPressed:(){
print("cancel");
Navigator.of(context).pop("取消"); //这里传入的值是返回给result的
},child:const Text("取消"))
],
)
});
print(result); // 获取点击的是哪个按钮
}

选择框

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
var _simpleDialog() async{
var result = await showDialog(context:context,builder:(context){
return SimpleDialog(
barrierDismissible:false, //点击灰色背景这个框会不会消失
title:Text("请求选择语言")
children:[
SimpleDialogOption(
onPressed:(){
print("汉语");
Navigator.of(context).pop("汉语");
}
child:Text("汉语"),
),
SimpleDialogOption(
onPressed:(){
print("英语");
Navigator.of(context).pop("英语");
}
child:Text("英语"),
),
],
);
});
print(result);
}

底部弹窗

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
void _modelBottomSheet() async{
var result = await showModalBottomSheet(context:context,builder:(context){
return Container( // 这里就可以自己定义一些布局
child:Column(
childre:[
ListTile(
title:Text("分享");
onTap:(){
print("分享");
Navigator.of(context).pop("分享");
}
),
ListTile(
title:Text("收藏");
onTap:(){
print("收藏");
Navigator.of(context).pop("收藏");
}
),
]
),
);
});
print(result);
}

fluttertoast 提示信息

这个是第三方库,我们可以去官网进行安装使用

pubspec.yaml 文件中配置插件后,运行 flutter pub get 命令就可以进行下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'package:fluttertoast/fluttertoast.dart'; // 引入

void _toast(){
Fluttertoast.showToast(
msg:"This is Center Short Toast",
toastLength:Toast.LENGTH_SHORT,
gravity:ToastGravity.CENTER, // 显示的位置
timeInSecForIosWeb:1, // 提示的时间,这个针对 ios,
toastLength:Toast.LENGTH_SHORT, // android 中这个可以配置显示的时间长短
backgroundColor:Colors.red, // 背景颜色
textColor:Colors.white, // 字体颜色
fontsize:16.0 // 字体大小
);
}

自定义Dialog

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 MyDialog extends Dialog{
final String title;
final String content;
final Function? onTap;
const MyDialog({key?key,required this.title,required this.content,required this.onTap}):super(key:key);
Widget build(BuildContext context){
return Material(
type:MaterialType.transparency,// 设置背景透明
child:Center( // 包裹一个center组件,不然会全屏
child:Container(
height:300,
width:300,
color:Colors.white,
child:Column(
children:[
Padding(
padding:const EdgInsets.all(5),
child:Stack(
children:[
Align(
alignment:Alignment.centerLeft,
child:Text(this.title),
),
Align(
alignment:Alignment.centerLeft,
child:InkWell(// flutter中专门让点击的事件
onTap:onTap,
child:Icon(Icons.close),
),
),
]
)
),
Divider(),
Container(
width:double.infinity,
child:Text(this.content),
)
]
)
)
)
)
}
}


void _myDialog(){
showDialog(context:context,builder:(context){
return MyDialog(title:"提示",content:"我是内容"
onTap:(){
print("点了");
});
})
}

PageView组件

通过PageView组件可以实现类似于抖音的上下滑动切换的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PageView(
scrollDirection:Axis.vertical// 配置滑动的方向
onPageChanged:(index){// 切换页面的时候触发的方式
//通过比较index 和children的长度,我们可以提前获取数据,然后实现了懒加载的效果
}
children:[ // children里面每一个都是一个页面
Center(
child:Text("第一个")
),
Center(
child:Text("第二个")
)
]
)

我们也可以使用 PaveView.builder 来生成我们的PageView,在PaveView外套一个SizedBox来控制高度

给组件with AutomaticKeepAliveClientMixin 并重写 wantKeepAlive 函数返回true缓存页面

Flutter 中的 Key

我们平时一定接触过很多的Widget,比如ContainerRowColumn等,它们在我们绘制界面的过程中发挥着重要的作用。但是不知道你有没有注意到,在几乎每个Widget的构造函数中,都有一个共同的参数,它们通常在参数列表的第一个,那就是Key,在Flutter中,Key是不能重复使用的,所以Key一般用来做唯一标识。组件在更新的时候,其状态的保存主要是通过判断组件的类型或者key值是否一致。因此,当各组件的类型不同的时候,类型已经足够用来区分不同的组件了,此时我们可以不必使用key。但是如果同时存在多个同一类型的控件的时候,此时类型已经无法作为区分的条件了,我们就需要使用到key。

Flutter key子类包含LocalKeyGlobalKey.

  • 局部键(LocalKey):ValueKey、ObjectKey、UniqueKey
    • UniqueKey,唯一值,每次运行的时候会随机生成
    • ValueKey,指定一个数作为key
  • 全局键(GlobalKey):GlobalKey、GlobalObjectKey
    • 通过globalKey.currentState 可以获取到该元素的状态

AnimatedList

AnimatedList和ListView的功能大体相似,不同的是,AnimatedList可以在列表中插入或删除节点
时执行一个动画,在需要添加或删除列表项的场景中会提高用户体验。

AnimatedList是一个StatefulWidget,它对应的State类型为AnimatedListState,添加和删除元素的
方法位于AnimatedListState中:

  • void insertItem()
  • void removeItem()
    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
    List<String> list = ["1","2"];
    _globalKey = GlobalKey<AnimatedListState>();
    list.add("新增一条数据");
    _globalKey.currentState!.insertItem(list.length-1);
    _globalKey.currentState!.removeItem(index,(context,animation){
    var removeItem = list[index];
    list.removeAt(index);
    return FadeTransition(
    opacity:animation,
    child:removeItem,// 执行动画的元素
    );
    });
    AnimatedList(
    key:_globalKey,
    initialItemCount:list.length,
    itemBuilder:((context,index,animation){
    return FadeTransition(
    opacity:animation,
    child:ListTile(
    title:Text(list[index]),
    )
    );
    })
    );

    // 动画执行需要时间,如果没有执行完成再次点击可能有报错,我们可以设置锁来解决
    bool flag = true
    if(flag){
    flag = false;
    // 执行动画
    // 设置定时器执行完成后释放锁
    Timer.peridic(Duration(millisecondes:500),(timer){
    flag = true;
    timer.cancel();
    });
    }

Flutter动画

动画基本原理及Flutter动画简介

动画原理
在任何系统的UI框架中,动画实现的原理都是相同的,即:在一段时间内,快速地多次改变UI外观;由于人眼会产生视觉暂留,所以最终看到的就是一个“连续的动画,这和电影的原理是一样的。我们将UI的一次改变称为一个动画帧,对应一次屏幕刷新,而决定动画流畅度的一个重要指标就是帧率FPS(Frame Per Second),即每秒的动画帧数。很明显,帧率越高则动画就会越流畅!一般情况下,对于人眼来说,动画帧率超过16FPS,就基本能看了,超过32FPS就会感觉相对平滑,而超过32FPS,大多数人基本上就感受不到差别了。由于动画的每一帧都是要改变UI输出,所以在一个时间段内连续的改变UI输出是比较耗资源的,对设备的软硬件系统要求都较高,所以在UI系统中,动画的平均帧率是重要的性能指标,而在Flutter中,理想情况下是可以实现60FPS的,这和原生应用能达到的帧率是基本是持平的。

Flutteri动画简介
FLutter中的动画主要分为:隐式动画显式动画自定义隐式动画自定义显式动画、和Hero动画

隐式动画

通过几行代码就可以实现隐式动画,由于隐式动画背后的实现原理和繁琐的操作细节都被隐去了,所以叫隐式动画,FLutter中提供的AnimatedContainerAnimatedPaddingAnimatedPositionedAnimatedOpacityAnimatedDefaultTextStyleAnimatedSwitcher都属于隐式动画,隐式动画中可以通过duration配置动画时长、可以通过Curve(曲线)来配置动画过程

这几个组件其实就是对应的ContainerPaddingPositionedOpacity等组件的有动画版本,即正常我们改变形状是瞬间改变,这些组件会有一个改变的过程

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
void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Flutter Demo",
theme: ThemeData(primaryColor: Colors.blue),
home: const Tabs(),
);
}
}

class Tabs extends StatefulWidget {
const Tabs({super.key});

@override
State<Tabs> createState() => _TabsState();
}

class _TabsState extends State<Tabs> {
bool flag = true;
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(onPressed: () {
setState(() {
flag = !flag;
});
}),
appBar: AppBar(
title: const Text("Flutter App"),
backgroundColor: Colors.red,
),
body: Center(
child: AnimatedContainer(
duration: const Duration(seconds: 1),
height: flag ? 200 : 300,
width: flag ? 200 : 300,
color: Colors.yellow,
)),
);
}
}

Hero动画

微信朋友圈点击小图片的时候会有一个动画效果到大图预览,这个动画效果就可以使用Hro动画实现。Hero指的是可以在路由(页面)之间飞行”的widget,简单来说Hero动画就是在路由切换时,有一个共享的widget可以在新旧路由间切换。

使用photo_view 可以预览图片

1
import 'package:photo_view/photo_view.dart' // 预览单张图片
1
import 'package:photo_view/photo_view_gallery.dart' // 预览多张图片

Flutter Getx

状态管理

通俗的讲:当我们想在多个页面(组件Widget)之间共享状态(数据),或者一个页面(组件Widget)中的多个子组件之间供享状态(数据),这个时候我们就可以用Flutter中的状态管理来管理统一的状态(数据),实现不同组件之间的传值和数据共享。

现在Flutter的状态管理方案很多,redux、bloc、state、provider、Getx

provider是官方提供的状态管理解决方案,主要功能就是状态管理。Getx是第三方的状态管理插件,不仅具有状态管理的功能,还具有路由管理、主题管理、国际化多语言管理、Obx局部更新、网络请求、数据验证等功能,相比其他状态管理插件Geⅸ简单、功强大并且高性能。

Flutter Getx介绍

Getx以是Flutter上的一个轻量且强大的解决方案,Getx为我们提供了高性能的状态管理、智能的依赖注入和便捷的路由管理。
Getx 中文文档

  • Getx以有3个基本原则:

    • 性能:Getx以专注于性能和最小资源消耗。Get以打包后的apk占用大小和运行时的内存占用与其他状态管理插件不相上下。
    • 效率:Getx以的语法非常简捷,并保持了极高的性能,能极大缩短你的开发时长。
    • 结构:Getx以可以将界面、逻辑、依赖和路由完全解耦,用起来更清爽,逻辑更清晰,代码更容易维护
  • GetX并不臃肿,却很轻量。如果你只使用状态管理,只有状态管理模块会被编译,甚他没用到的东西都不会被编译到你的代码中。它拥有众多的功能,但这些功能都在独立的容器中,只有在使用后才会启动。

  • Getx有一个庞大的生态系统,能够在Android、iOS、Web、Mac、Linux、Windowsi和你的服务器上用同样的代码运行。通过GetServer可以在你的后端完全重用你在前端写的代码。