Appearance
介绍
官方文档 flutter.cn
社区文档 book.flutterchina.club
搭建开发环境
安装 Flutter SDK
js
// https://docs.flutter.dev/development/tools/sdk/releases 下载链接
// Flutter SDK 自带了 Dart sdk 所以安装了 Flutter SDK 就不需要再单独安装 Dart sdk了
配置环境变量
js
// 和配置 Dart 环境变量一致
// flutter doctor -v 在命令行中输入检查是否配置成功
配置镜像
js
// 由于在国内访问 flutter 可能会受到限制,所以我们需要配置镜像来避免
// 和配置 Dart 环境变量一致,只不过是在用户变量中新建两个变量
// flutter doctor -v 在命令行中输入检查是否配置成功
需要在用户环境变量中新建两个变量,如下图
具体值:
PUB_HOSTED_URL=https://pub.flutter-io.cn
FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
使用 Android Studio 安装 Android SDK
js
// https://developer.android.google.cn/studio 下载地址
安装 vscode 插件
js
// 直接搜索 Flutter 安装插件
初始化项目
js
// flutter create 工程名 打开终端创建flutter 项目
启动 可以在 vscode 右下角切换运行的平台
如果在浏览器启动失败,就打开项目目录启动命令行运行如下命令
js
// flutter run -d chrome --web-port=8080 --web-hostname=127.0.0.1
目录结构
主文件介绍
main.dart
dart
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// 指定应用程序的第一个页面
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
Scaffold 组件
组件库使用
组件简介
无状态组件
dart
// 无状态组件只能通过外部重新创建该组件才能更新页面
// 导入组件库
import 'package:flutter/material.dart';
class MyWidget1 extends StatelessWidget {
MyWidget1([this.text = '无状态组件']);
// 无状态组件里的数据都是不可变的,所以必须使用 final 定义
final String text;
@override
Widget build(BuildContext context) {
// Container组件类似于 html 里的 span 标签
// flutter 是通过组件嵌套来构建页面的
return Container(
// 设置组件背景色
color: Colors.white,
// 设置文字居中对齐
alignment: Alignment.center,
// 可以直接通过属性名使用属性
child: Text(text),
);
}
}
有状态组件
dart
// 导入组件库
import 'package:flutter/material.dart';
// 在编译器里写入 st 即可唤出快捷创建组件代码块
class MyWidget2 extends StatefulWidget {
const MyWidget2({super.key});
@override
State<MyWidget2> createState() => _MyWidget2State();
}
class _MyWidget2State extends State<MyWidget2> {
int num = 0;
@override
Widget build(BuildContext context) {
return Container(
// 文字按钮组件
child: TextButton(
// 可以通过 style 属性设置样式
child: Text('点击了$num次', style: TextStyle(fontSize: 30.0)),
// 当 TextButton 被点击会触发 onPressed 函数
onPressed: () {
// 有状态组件中通过 setState 方法修改状态,当被修改时组件中所有依赖此状态的地方会自动更新
setState(() {
num++;
});
}),
);
}
}
生命周期
dart
// -----------------------------------组件生命周期---------------------------------------
// createState 组件创建 State 对象的方法,只调用一次(创建 state 时)
// initState 初始化状态时调用,只调用一次(在 state 被插入视图树时)
// didChangeDependencies 当前组件 state 对象依赖关系发生变化后,会在 initState 后立即调用 (initState 后及 state 对象发生变化时,widget 树中,若节点的父级,结构中层级或父级结构中的任一节点的widget类型发生变化,才会调用,若仅仅只是父结构某一节点的属性变化则不会调用。可以告诉你图层正在进行较大成本的重绘)
// build 渲染组件时(state 准备好数据需要渲染时)
// addPostFrameCallback 组件渲染结束之后的回调,只会调用一次
// didUpdateWidget 当Widget配置发生变化时,比如父Widget触发重建(即父Widget的状态发生变化 时),热重载,系统会调用这个函数(父组件更新状态触发重载时)
// deactivate 组件销毁前
// dispose 组件销毁后
// -----------------------------------app 生命周期---------------------------------------
// resumed 可见并能响应用户输入
// inactive 处在不活跃状态无法响应用户输入
// paused 不可见也无法响应用户输入
// -----------------------------------路由生命周期---------------------------------------
// RouteAware:监听路由变化
// RouteObserver 是一个配合 RouteAware 的一个类,通过这个类可以通知当前页面该执行那个生命周期方法否则只混入 RouteAware 是不能执行的。另外还有 RouteObserver 需要注册在 MaterialApp 中,这样才能在导航的过程中执行对应生命周期方法
//
// didPush 从其他页面跳转到当前页
// didPushNext 从当前页面跳转到其他页面调用
// didPop 从当前页退回到上个页面
// didPopNext 从其他页面退回到当前页
dart
// 导入组件库
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
// 在编译器里写入 st 即可唤出快捷创建组件代码块
class MyWidget2 extends StatefulWidget {
const MyWidget2({super.key});
@override
// createState 表示创建状态时
State<MyWidget2> createState() => _MyWidget2State();
}
class _MyWidget2State extends State<MyWidget2> {
int num = 0;
@override
// 组件初始化调用
void initState() {
print('initState');
// 注册监听回调,监听组件是否渲染结束
SchedulerBinding.instance.addPersistentFrameCallback((timeStamp) {
print('组件渲染完成');
});
super.initState();
}
@override
// 当前组件依赖的数据发生变化时
void didChangeDependencies() {
print('didChangeDependencies');
super.didChangeDependencies();
}
@override
// 组件销毁前
void deactivate() {
print('deactivate');
super.deactivate();
}
@override
// 组件销毁后
void dispose() {
print('dispose');
super.dispose();
}
@override
// 渲染组件时
Widget build(BuildContext context) {
return Container(
// 文字按钮组件
child: TextButton(
// 可以通过 style 属性设置样式
child: Text('点击了$num次', style: TextStyle(fontSize: 30.0)),
// 当 TextButton 被点击会触发 onPressed 函数
onPressed: () {
// 有状态组件中通过 setState 方法修改状态,当被修改时组件中所有依赖此状态的地方会自动更新
setState(() {
num++;
});
}),
);
}
}
基础组件
Container
dart
import 'package:flutter/material.dart';
class MyWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('头部部分'),
),
body: Container(
// 当需要子盒子在父盒子中居中时需要在父组件中设置居中属性
alignment: Alignment.center,
child: Container(
width: 300,
height: 200,
color: Colors.blue,
padding: EdgeInsets.only(left: 30, top: 30, right: 30, bottom: 10),
// 子组件默认是撑满父组件的
child: Container(
// 当设置了 decoration 属性后就不能再单独设置 color 属性了
// color: Colors.yellow,
decoration: BoxDecoration(
color: Colors.yellow,
border: Border.all(color: Colors.red, width: 2),
// 设置组件上下左右各角圆角都为10
// 如果想分别设置可以使用 BorderRadius.only
borderRadius: BorderRadius.circular(10)),
),
),
));
}
}
Text
dart
import 'package:flutter/material.dart';
class MyWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('头部部分'),
),
body: Container(
alignment: Alignment.center,
child: Container(
width: 200,
height: 200,
color: Colors.blue,
child: Text(
'文字组件,文字组件,文字组件,文字组件,文字组件',
style: TextStyle(
color: Colors.red, fontSize: 24, fontWeight: FontWeight.w700),
// 文字最大显示行数
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
));
}
}
Image
加载本地资源
dart
import 'package:flutter/material.dart';
class MyWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('头部部分'),
),
body: Container(
alignment: Alignment.center,
child: Container(
width: 300,
height: 400,
color: Colors.blue,
// child: Image.network(),
child: Image.asset(
'assets/log.jpg',
// 设置图片填充方式
fit: BoxFit.cover,
// Image 组件在加载新图片时默认先将旧图片清空,所以在新图加载展示完成之前会有一段空白内容,会造成 “闪一下”的问题,gaplessPlayback 设置为 true 可以指定在加载新图时不清空旧图直到新图展示出来为止,可以解决上述问题
gaplessPlayback: true
)),
));
}
}
布局组件
线性布局
dart
import 'package:flutter/material.dart';
class MyWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('头部部分'),
),
// Center 组件等价于 Container组件设置alignment: Alignment.center居中属性,里面的子组件会居中显示
body: Center(
child: Container(
width: 200,
height: 240,
color: Colors.yellow,
// Column的子组件会纵向排列
child: Column(
// 主轴方向的对齐方式
// mainAxisAlignment: MainAxisAlignment.start,
// 辅轴方向的对齐方式
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(
'assets/log.jpg',
width: 200,
height: 140,
fit: BoxFit.cover,
),
Padding(
padding: EdgeInsets.only(top: 10),
child: Text('文字1'),
),
Padding(
padding: EdgeInsets.only(top: 10),
child: Text('文字2'),
)
]),
),
));
}
}
dart
import 'package:flutter/material.dart';
class MyWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('头部部分'),
),
body: Center(
child: Container(
height: 100,
color: Colors.yellow,
// Column的子组件会纵向排列
child: Row(
// 主轴方向的对齐方式
mainAxisAlignment: MainAxisAlignment.spaceAround,
// 辅轴方向的对齐方式
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(
'assets/log.jpg',
width: 86,
height: 86,
fit: BoxFit.cover,
),
Text('文字1')
]),
),
));
}
}
弹性布局
dart
import 'package:flutter/material.dart';
class MyWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('头部部分'),
),
body: Center(
child: Container(
height: 100,
color: Colors.yellow,
// Column的子组件会纵向排列
child: Row(children: [
Expanded(
flex: 1,
child: Image.asset(
'assets/log.jpg',
height: 140,
fit: BoxFit.cover,
)),
// 使用Expanded组件解决文字溢出问题
Expanded(
flex: 2,
child: Padding(
padding: EdgeInsets.only(left: 10),
child: Text('文字2文字2文字2文字2文字2文字2文字2文字2文字2文字2文字2文字2'),
))
]),
),
));
}
}
层叠布局
dart
import 'package:flutter/material.dart';
class MyWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('头部部分'),
),
body: Center(
child: Stack(alignment: Alignment.center, children: [
Positioned(
top: 70,
child: Container(
width: 200,
height: 200,
color: Colors.pink,
)),
Positioned(
top: 0,
child: Image.asset(
'assets/log.jpg',
width: 80,
height: 80,
fit: BoxFit.cover,
))
]),
));
}
}
综合案例
dart
import 'package:flutter/material.dart';
class MyWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('头部部分'),
),
body: Container(
padding: EdgeInsets.all(10),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Color 可以设置自定义颜色,0x 表示16进制颜色,FF表示透明度(不透明)后面则是色值
Text('2021-05-15 21:49:48',
style: TextStyle(color: Color(0xFF666666), fontSize: 13)),
Text('待发货',
style: TextStyle(color: Color(0xFFFF9246), fontSize: 14))
],
),
Padding(
padding: EdgeInsets.only(top: 10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(
'assets/log.jpg',
width: 86,
height: 86,
fit: BoxFit.cover,
),
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 10),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'茶水分离杯耐热隔热玻璃杯茶水分离杯耐热隔热玻璃杯',
style: TextStyle(
color:
Color(0xFF262626),
fontSize: 13))),
Padding(
padding:
EdgeInsets.only(left: 10),
child: Text('x1',
style: TextStyle(
color: Color(0xFF262626),
fontSize: 15)),
),
]),
Padding(
padding: EdgeInsets.only(top: 6),
child: Container(
decoration: BoxDecoration(
color: Color(0xFFF7F7F7),
borderRadius:
BorderRadius.circular(2),
),
padding: EdgeInsets.only(
left: 5,
right: 2,
top: 3,
bottom: 3),
child: Text(
'规格:白色240ml',
style: TextStyle(
color: Color(0xFF888888),
fontSize: 11),
),
),
),
Padding(
padding: EdgeInsets.only(top: 6),
child: Text(
'¥119.5',
style: TextStyle(
color: Color(0xFF888888),
fontSize: 11),
),
)
])))
])),
Padding(
padding: EdgeInsets.only(top: 10),
child: Container(
alignment: Alignment.centerRight,
child: Text(
'合计:¥195',
style: TextStyle(color: Color(0xFF262626), fontSize: 16),
),
),
),
Padding(
padding: EdgeInsets.only(top: 10),
child: Container(
alignment: Alignment.centerRight,
child: Container(
decoration: BoxDecoration(
border:
Border.all(color: Color(0xFF666666), width: 0.5),
borderRadius: BorderRadius.circular(2)),
padding: EdgeInsets.only(
left: 15, right: 15, top: 7, bottom: 7),
child: Text(
'再次购买',
style:
TextStyle(color: Color(0xFF262626), fontSize: 16),
)),
),
),
],
),
));
}
}
滚动组件
ListView
默认构造函数方式创建
dart
import 'package:flutter/material.dart';
class MyWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('头部部分'),
),
body: Container(
child: ListView(
// 滚动效果:禁止滚动
physics: NeverScrollableScrollPhysics(),
// 滚动方向:纵向(默认)横向滚动
scrollDirection: Axis.horizontal,
children: [
Container(
height: 120,
color: Colors.red,
child: Text('item1'),
),
Container(
height: 120,
color: Colors.yellow,
child: Text('item1'),
),
Container(
height: 120,
color: Colors.black,
child: Text('item1'),
),
Container(
height: 120,
color: Colors.white,
child: Text('item1'),
),
Container(
height: 120,
color: Colors.blue,
child: Text('item1'),
),
Container(
height: 120,
color: Colors.amberAccent,
child: Text('item1'),
),
Container(
height: 120,
color: Colors.grey,
child: Text('item1'),
),
],
),
));
}
}
命名构造函数 build 方式
dart
class MyWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('头部部分'),
),
body: Container(
child: ListView.builder(
// 用于指定item渲染的个数,如果不设置则会无限滚动
itemCount: 10,
// itemExtent:120 使用该属性代替给子组件设置高可以提高性能
// itemBuilder 用于设置列表每个item项
// context表示当前组件上下文,index 表示当前渲染的索引值
itemBuilder: ((context, index) {
return Container(
height: 120,
color: Colors.red,
child: Text('item$index'),
);
})),
));
}
}
命名构造函数 separated
dart
class MyWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('头部部分'),
),
body: Container(
child: ListView.separated(
// 用于指定分割线
separatorBuilder: ((context, index) {
return Container(color: Colors.grey, height: 8);
}),
// 用于指定item渲染的个数,如果不设置则会无限滚动
itemCount: 10,
// itemBuilder 用于设置列表每个item项
itemBuilder: ((context, index) {
return Container(
height: 120,
color: Colors.red,
child: Text('item$index'),
);
})),
));
}
}
GridView
默认构造函数方式
布局方式 1
固定次轴子元素个数
dart
class MyWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('头部部分'),
),
body: Container(
child: GridView(
// SliverGridDelegateWithFixedCrossAxisCount 默认是纵向排列滚动的,主轴是纵向的
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // 表示次轴 item 项个数
mainAxisSpacing: 10, // 主轴间距
crossAxisSpacing: 10, // 次轴间距
// 当屏幕大小不一时 flutter 会按照这个比例自行计算宽高
// 当用户将手机展示方向换为横屏时布局也会和竖屏一致
childAspectRatio: 4 / 3 // 表示每个item项的长宽比
),
children: [
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
],
),
));
}
}
布局方式 2
固定次轴子元素最大宽度
dart
class MyWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('头部部分'),
),
body: Container(
child: GridView(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 60, // 表示次轴 item 项最大宽度
mainAxisSpacing: 10, // 主轴间距
crossAxisSpacing: 10, // 次轴间距
childAspectRatio: 4 / 3 // 表示每个item项的长宽比
),
children: [
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
],
),
));
}
}
命名构造函数 builder
dart
class MyWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('头部部分'),
),
body: Container(
child: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 4 / 3),
itemCount: 20, // 子元素个数
// 指定子元素
itemBuilder: (context, index) {
return Container(
color: Colors.red,
child: Text('item'),
);
},
),
));
}
}
命名构造函数 count 和 extent
dart
class MyWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('头部部分'),
),
body: Container(
// GridView.count 可以直接设置子元素个数的布局方式
// GridView.extent 可以直接设置子元素最大宽度的布局方式
child: GridView.count(
// 快速指定样式配置
crossAxisCount: 3,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 4 / 3,
children: [
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
]),
));
}
}
CustomScrollView
dart
class MyWidget2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('头部部分'),
),
body: Container(
child: CustomScrollView(
slivers: [
// SliverGrid.count 对应 GridView.count
SliverGrid.count(
crossAxisCount: 3,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 4 / 3,
children: [
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
Container(
color: Colors.red,
child: Text('item'),
),
]),
SliverList(
// delegate 类似与 ListView 中的 itemBuilder
delegate: SliverChildBuilderDelegate((context, index) {
return Container(
height: 120,
child: Text('item$index'),
);
},
// 子元素个数
childCount: 10),
)
],
)));
}
}
动画组件
平移动画
dart
class MyWidget2 extends StatefulWidget {
@override
State<MyWidget2> createState() => _MyWidget2State();
}
class _MyWidget2State extends State<MyWidget2>
with SingleTickerProviderStateMixin {
/// 动画控制器
late AnimationController _animationController;
/// 动画对象,Offset用于约束此动画是平移动画
late Animation<Offset> _animation;
@override
void initState() {
// 创建动画控制器
// vsync 用于监听屏幕刷新,如果动画不在可展示区域,会自动停止动画提高性能
// 将本组件指定给 vsync 即可,但本组件必须混入 SingleTickerProviderStateMixin,使本组件可以监听屏幕刷新
// duration 用于指定动画执行时间
_animationController =
AnimationController(vsync: this, duration: Duration(seconds: 2));
// 创建动画对象,Offset 表示偏移x、y的值,这里表示一倍宽高,Offset.zero 相当于Offset(0,0)
// 还需要通过 animate 将动画挂载到动画控制器
_animation = Tween<Offset>(begin: Offset.zero, end: Offset(1, -1))
.animate(_animationController);
// 启动动画,repeat 表示循环执行动画
_animationController.repeat();
}
@override
void dispose() {
// 销毁动画
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('动画组件'),
),
body: Center(
// 平移组件
child: SlideTransition(
position: _animation,
child: Container(
width: 80,
height: 80,
color: Colors.red,
),
),
));
}
}
缩放动画
dart
class MyWidget2 extends StatefulWidget {
@override
State<MyWidget2> createState() => _MyWidget2State();
}
class _MyWidget2State extends State<MyWidget2>
with SingleTickerProviderStateMixin {
/// 动画控制器
late AnimationController _animationController;
/// 动画对象,double用于约束此动画是缩放动画
late Animation<double> _animation;
@override
void initState() {
// 创建动画控制器
_animationController =
AnimationController(vsync: this, duration: Duration(seconds: 2));
// 这里表示从0缩放到2倍
_animation = Tween<double>(begin: 0, end: 2)
.animate(_animationController);
_animationController.repeat();
}
@override
void dispose() {
// 销毁动画
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('动画组件'),
),
body: Center(
// 缩放组件
child: ScaleTransition(
scale: _animation,
child: Container(
width: 10,
height: 10,
color: Colors.red,
),
),
));
}
}
底部 tab 栏组件
dart
Widget build(BuildContext context) {
return Scaffold(
// 底部 tab 栏组件
bottomNavigationBar: BottomNavigationBar(
// 设置选中时的字号
selectedFontSize: 10,
// 设置未选中时的字号
unselectedFontSize: 10,
// 设置选中时的文字颜色
selectedItemColor: const Color(0xFF3CCEAF),
// 设置未选中时的文字颜色
unselectedItemColor: const Color(0xFF383838),
// 用于解决当 items 个数大于 3 个时组件会变得“透明”
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
// 设置未选中时的图标
icon: Icon(Icons.home),
// 设置选中时的图标
activeIcon: Image.asset('assets/home_sel.png'),
label: '首页'
),
BottomNavigationBarItem(icon: Icon(Icons.category), label: '分类'),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart), label: '购物车'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
]),
);
}
主题组件
主题组件可以修改子组件的主题样式
dart
// 使用 Theme 组件修改 BottomNavigationBar 组件点击和长按时的背景色
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: Theme(
data: ThemeData(
// 设置点击时的背景颜色为透明
splashColor: Colors.transparent,
// 设置长按时的背景颜色为透明
highlightColor: Colors.transparent),
child: BottomNavigationBar(),
),
);
}
分页组件
dart
// PageView 在使用时必须指定高度
Container(
height: 200,
child: PageView(
// children 中每一项都作为一页
children: [Text('第一页'), Text('第二页')],
// PageView 翻页事件参数表示当前页索引
onPageChanged: (i) {
setState(() {
isSingle = widget.CategoryGrids!
.sublist(i * 10, widget.CategoryGrids!.length)
.length <=
5;
});
},
),
),
高度变化动画组件
dart
// AnimatedContainer 组件用于高度宽度发生变化时展示动画
AnimatedContainer(
// 动画执行的时间
duration: Duration(milliseconds: 400),
// 动画函数,ease 表示先快后慢的动画
curve: Curves.ease,
height: isSingle ? totalHeight : totalHeight2,
child: PageView(
children: _buildPages(widget.CategoryGrids as List),
// PageView 翻页事件参数表示当前页索引
onPageChanged: (i) {
setState(() {
isSingle = widget.CategoryGrids!
.sublist(i * 10, widget.CategoryGrids!.length)
.length <=
5;
});
},
),
)
富文本组件
dart
RichText(
text: const TextSpan(
text: '¥',
style: TextStyle(
color: Color(0xFFCF4444), fontSize: 9, fontWeight: FontWeight.w400),
children: [
TextSpan(
text: '99',
style: TextStyle(
fontSize: 12,
)),
TextSpan(
text: '.0',
style: TextStyle(
fontSize: 12,
))
],
));
吸顶组件
dart
// 吸顶组件
SliverPersistentHeader(
// 设置是否吸顶
pinned: true,
delegate: CustomSlivePersistentHeaderDelegate());
// 自定义 SliverPersistentHeaderDelegate 子类,构建吸顶视图
class CustomSlivePersistentHeaderDelegate
extends SliverPersistentHeaderDelegate {
// 用于构建吸顶视图内容
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
// shrinkOffset 表示吸顶之后视图滚动的距离(范围是 0 - maxExtent)
return Container(
height: 44,
color: Colors.red,
);
}
// 定义吸顶视图最大高度
@override
double get maxExtent => 44;
// 定义吸顶视图最小高度
@override
double get minExtent => 44;
// 在视图吸顶时是否需要重新构建视图
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
// oldDelegate 表示当前视图是否发生变化
// 如果返回true表示在视图吸顶的时候需要重新构建吸顶视图
return true;
}
}
导航组件
dart
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
// 创建 TabController 控制器必须混入 TickerProviderStateMixin
class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
/// TabController
TabController? _controller;
// 构建分类导航内容
List<Widget> _buildTabBar(List? categoryBanners) {
List<Widget> items = [];
categoryBanners?.forEach((element) {
items.add(Container(
width: 60,
height: 44,
child: Text(element['name']),
));
});
return items;
}
@override
void initState() {
// 初始化 TabController 保证在页面初始化时 _controller 是有控制器的
_controller = TabController(length: 0, vsync: this);
getHttp();
super.initState();
}
getHttp() async {
try {
Response response = await index();
setState(() {
// vsync 用于监听 TabController 的交互,直接传递当前组件即可(当前组件必须混入 TickerProviderStateMixin)
_controller =
TabController(length: categoryBanners!.length, vsync: this);
});
} catch (e) {
debugPrint('$e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF7F7F8),
appBar: AppBar(
title: const Text('首页'),
),
body: CustomScrollView(
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: CustomSlivePersistentHeaderDelegate(
// TabBar 导航栏组件
tabBar: TabBar(
// 是否允许滚动
isScrollable: true,
// 选中和未选中文字颜色
labelColor: const Color(0xFF27BA9B),
unselectedLabelColor: const Color(0xFF333333),
// 去除底部指示器
indicator: const BoxDecoration(),
// 取消文本左右间距
labelPadding: EdgeInsets.zero,
// 控制器
controller: _controller,
tabs: _buildTabBar(categoryBanners),
))),
SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.only(left: 10, right: 10),
height: 1200,
// TabBarView 组件
// controller 必须传递要与之联动的 TabBar 组件的控制器
child: TabBarView(
controller: _controller,
// children 存放每个 TabBarView 的内容
children: _buildCategoryGoods(categoryBanners)),
),
),
],
),
);
}
}
手势组件
dart
// GestureDetector 组件可以处理各种手势,如点击双击等
GestureDetector(
onTap: () {
print('点击');
},
child: const Icon(Icons.person),
)
监听滚动状态组件
dart
// NotificationListener 可以监听滚动视图的状态
NotificationListener(
// notification 保存滚动视图的滚动状态的
onNotification: (ScrollNotification notification) {
// notification.depth 获取当前滚动组件层级,这里指最底层的滚动视图
// ScrollEndNotification 表示是否滚动结束
if (notification.depth == 0 &&
notification is ScrollEndNotification) {
// notification.metrics.pixels 获取当前滚动的距离
// MediaQuery.of(context).size.height 获取当前设备屏幕高度
if (notification.metrics.pixels >=
MediaQuery.of(context).size.height) {
_isShowTop = true;
} else {
_isShowTop = false;
}
setState(() {});
}
// 返回 true 表示已经拿到需要的信息,不再继续冒泡下发通知
return true;
},
child: Stack(
children: [
ListView()
]
)
)
跳转组件
dart
Navigator.push(
context,
CupertinoPageRoute(
// 表示页面推入的方式,false(默认值) 表示从屏幕右侧向左推入
// true 表示从屏幕底向上推入
fullscreenDialog: true,
builder: (context) {
// 表示要跳转的组件
return const AccountLoginPage();
}));
// 退出当前页面
Navigator.pop(context)
非组件文件跳转
dart
// 在 main.dart文件
// 创建 navigatorKey 用于在非组件文件做页面跳转
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const RootPage(),
builder: EasyLoading.init(),
// 用于记录并获取全局的 navigatorKey 的状态(NavigatorState)
navigatorKey: navigatorKey,
);
}
}
dart
// 在非组件文件跳转
// 在没有组件上下文的情况下跳转页面
navigatorKey.currentState!
.push(CupertinoPageRoute(builder: (context) {
// 这里 AccountLoginPage 是需要跳转组件
return const AccountLoginPage();
}));
携带参数
dart
Navigator.push(context, CupertinoPageRoute(builder: (context) {
// 在跳转时携带参数(本质就是父子组件通信)
return RegisterVerifyPage(mobile: _accountController.text);
}));
dart
// 在跳转的组件中声明参数
class RegisterVerifyPage extends StatefulWidget {
// 准备参数
final String? mobile;
RegisterVerifyPage({this.mobile});
@override
State<RegisterVerifyPage> createState() => _RegisterVerifyPageState();
}
class _RegisterVerifyPageState extends State<RegisterVerifyPage> {
@override
Widget build(BuildContext context) {}
}
清除路由栈
dart
// 第二个参数表示需要推入的页面
// 第三个参数表示如何删除路由栈里的路由(true:不再清空路由,false: 清空所有路由)
// 顺序是先删除路由再推入第二个参数的页面
// ModalRoute.withName('/') 表示除首页保留其他全清除
Navigator.pushAndRemoveUntil(context,
CupertinoPageRoute(builder: (context) {
return const AccountLoginPage();
}), ModalRoute.withName('/'));
appBar 组件
dart
AppBar(
backgroundColor: const Color(0xFF00BF9B),
// 左侧部分
leading: IconButton(
onPressed: () {
Navigator.pop(context);
},
icon: const Icon(Icons.person),
),
// 右侧部分可传递多个组件
actions: [
GestureDetector(
onTap: () {
debugPrint('注册');
},
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.only(right: 20),
child: const Text(
'新用户注册',
style: TextStyle(fontSize: 14),
),
),
)
],
),
滚动自适应组件
dart
// 当页面内容比较多当前屏幕无法放下时可自动生成滚动条,反之则不会生成
SingleChildScrollView(child: Column()),
表单输入组件
dart
class _AccountLoginPageState extends State<AccountLoginPage> {
// 是否展示一键清空
bool _isShowCleanAccount = false;
// 是否展示一键清空
bool _isShowCleanPassword = false;
// 定义表单控制器
final TextEditingController _accountController =
TextEditingController(text: '');
// 定义表单控制器 text 表示表单默认文本,后续获取表单内容可以 _passwodrController.text 获取
final TextEditingController _passwodrController =
TextEditingController(text: '');
// 表单焦点
final FocusNode _accountFocusNode = FocusNode();
final FocusNode _passwordFocusNode = FocusNode();
// 是否密文展示
bool isObscureText = true;
@override
void initState() {
// 监听表单焦点事件
_passwordFocusNode.addListener(() {
// hasFocus 表示当前表单是否获取到焦点
print(_passwordFocusNode.hasFocus);
});
super.initState();
}
@override
void dispose() {
_passwodrController.dispose();
_accountFocusNode.dispose();
_passwordFocusNode.dispose();
// _passwordFocusNode.unfocus(); 取消焦点实现关闭输入虚拟键盘
super.dispose();
}
Widget _buildPasswordTextField() {
return TextField(
// 焦点
focusNode: _passwordFocusNode,
// 是否密文展示表单内容默认为 false
obscureText: isObscureText,
// 表单控制器
controller: _passwodrController,
// 表单内容变化事件
onChanged: (value) {
_passwordOnChanged(value);
},
// 光标设置
cursorRadius: const Radius.circular(10),
cursorColor: const Color(0xFFFFBD3B),
cursorHeight: 18,
style:
const TextStyle(fontSize: 14, color: Color(0xFF333333)),
decoration: const InputDecoration(
// 文本居中
contentPadding: EdgeInsets.only(top: 12),
// 辅助文本居中
isCollapsed: true,
// 去除边框
border: InputBorder.none,
// 空表单提示文字
hintText: '6到20位字母、数字符号组合',
hintStyle:
TextStyle(fontSize: 14, color: Color(0xFF333333)),
),
// 表单类型
keyboardType: TextInputType.text,
// 控制键盘回车键
textInputAction: TextInputAction.done,
),
),
)),
GestureDetector(
onTap: () {
// 清空表单
_passwodrController.clear();
setState(() {
_isShowCleanPassword = false;
});
},
child: Text(_isShowCleanPassword ? '清空' : ''),
),
GestureDetector(
onTap: () {
setState(() {
isObscureText = !isObscureText;
});
},
child: const Text('看'),
),
禁止事件输入组件
dart
// AbsorbPointer 组件用于禁止组件事件()输入,absorbing 为 true 时表示禁止
// 如下写法可以阻止 TabBar 组件的点击事件
AbsorbPointer(absorbing: true,child: TabBar())
组件通信
父 call 子
GlobalKey 直接调用
子组件
dart
class FramsAnimation extends StatefulWidget {
// 接收 key 并通过 super 传递给父组件(StatefulWidget 组件)
const FramsAnimation({Key? key, this.initIndex = 1}) : super(key: key);
}
// 子组件改为非私有方便父组件进行类型约束(_FramsAnimationState 改为 FramsAnimationState)
class FramsAnimationState extends State<FramsAnimation> {
void startAnimation() => _animationController.forward();
void resetAnimation() => _animationController.reset();
}
父组件
dart
class RootPage extends StatefulWidget {
const RootPage({super.key});
@override
State<RootPage> createState() => _RootPageState();
}
class _RootPageState extends State<RootPage> {
// 父组件创建 GlobalKey 并指定泛型
List framesKeys = [
GlobalKey<FramsAnimationState>(debugLabel: 'home_key'),
GlobalKey<FramsAnimationState>(debugLabel: 'category_key'),
GlobalKey<FramsAnimationState>(debugLabel: 'cart_key'),
GlobalKey<FramsAnimationState>(debugLabel: 'mine_key')
];
@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: Theme(data: ThemeData(child: BottomNavigationBar(
onTap: (i) {
// 通过 GlobalKey 直接调用子组件方法
framesKeys[i].currentState?.startAnimation();
framesKeys[_curentIndex].currentState?.resetAnimation();
},
),
);
}
}
网络请求
dart
// https://pub.flutter-io.cn/ 对标js中的npm
安装 dio
打开 pubspec.yaml 在 dependencies 节点下写入插件名以及版本号保存即可,flutter 会自行去下载安装
基本使用
dart
import 'package:flutter/material.dart';
// 引入 dio 插件
import 'package:dio/dio.dart';
class MyWidget2 extends StatefulWidget {
@override
State<MyWidget2> createState() => _MyWidget2State();
}
class _MyWidget2State extends State<MyWidget2> {
/// 记录请求数据
List _orderList = [];
@override
void initState() {
getHttp();
super.initState();
}
void getHttp() async {
try {
Response response =
await Dio().get('https://mock.boxuegu.com/mock/1172/orders');
// 修改状态更新组件
setState(() {
_orderList = response.data;
});
} catch (e) {
print(e);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('dio'),
),
body: Container(
child: ListView.separated(
separatorBuilder: ((context, index) {
return Container(color: Colors.grey, height: 8);
}),
// 填充数据
itemCount: _orderList.length,
itemBuilder: ((context, index) {
return Container(
height: 120,
color: Colors.red,
child: Text(_orderList[index]['name']),
);
})),
));
}
}
封装 dio 单例类
在 lib/service/request.dart 写入
dart
import 'package:dio/dio.dart';
/// 网络请求单例类
class XTXRequestManager {
// 声明dio
Dio? dio;
// 私有静态属性
static XTXRequestManager? _instance;
// 私有命名构造函数
XTXRequestManager._initManager() {
// 创建dio实例
if (dio == null) {
BaseOptions baseOptions = BaseOptions(
// 配置连接超时时间和响应超时时间,单位都是毫秒
connectTimeout: 15000,
receiveTimeout: 5000,
baseUrl: 'https://pcapi-xiaotuxian-front.itheima.net/');
dio = Dio(baseOptions);
dio!.interceptors.add(InterceptorsWrapper(
// 请求拦截器
onRequest: (options, handler) {
// 设置请求头信息
options.headers = {'Authorization': '', 'source-client': 'app'};
return handler.next(options);
},
// 响应拦截器
onResponse: (e, handler) {
return handler.next(e);
},
// 异常拦截器
onError: (e, handler) {
return handler.next(e);
},
));
}
}
// 创建单例对象并向外界提供单例对象的方法
factory XTXRequestManager() {
// 只有当单例对象不存在时才创建
_instance ??= XTXRequestManager._initManager();
return _instance!;
}
// 封装处理请求的公共方法
Future<Response> handleRequest(
String path,
String method, {
data,
Map<String, dynamic>? queryParameters,
}) {
return dio!.request(path,
data: data,
queryParameters: queryParameters,
options: Options(method: method));
}
}
新建 lib/service/homeApi.dart 写入
dart
import './request.dart';
import 'package:dio/dio.dart' show Response;
// 封装接口函数
Future<Response> index() => XTXRequestManager().handleRequest('home/index', 'get');
在组件中使用
dart
// 引入依赖及接口函数
import 'package:dio/dio.dart' show Response;
import '../../service/homeApi.dart' show index;
void initState() {
getHttp();
super.initState();
}
getHttp() async {
try {
// 调用函数
Response response = await index();
print(response.data);
} catch (e) {
print(e);
}
}
捕获异常
dart
import 'package:dio/dio.dart' show Response, DioError;
try {
Response userInfo =
await login(_accountController.text, _passwodrController.text);
// 捕获指定的异常(这里是捕获 dio 抛出的异常)
} on DioError catch (e) {
EasyLoading.showToast(e.response!.data['message']);
}
无感刷新 token
dart
class XTXRequestManager {
Dio? dio;
static XTXRequestManager? _instance;
XTXRequestManager._initManager() {
if (dio == null) {
BaseOptions baseOptions = BaseOptions(
connectTimeout: 15000,
receiveTimeout: 5000,
baseUrl: 'https://pcapi-xiaotuxian-front.itheima.net/');
dio = Dio(baseOptions);
Map userInfo = {};
// 异常拦截器
onError: (e, handler) async {
// 监听 401
if (e.response!.statusCode == 401) {
// 判断是否是刷新 token 接口出错
if (e.requestOptions.path == 'login/refresh') {
// 清空本地缓存
await saveUserInfo({});
// 在没有组件上下文的情况下跳转页面(还需在 main.dart 中做配置)
navigatorKey.currentState!
.push(CupertinoPageRoute(builder: (context) {
return const AccountLoginPage();
}));
// 以一个异常结束,而且异常不会再被 onError 拦截到
return handler.reject(e);
}
Response tokenInfo =
await refreshToken(userInfo['account'], userInfo['id']);
await saveUserInfo(tokenInfo.data['result']);
// 重新设置请求头
e.requestOptions.headers['Authorization'] =
'Bearer ${tokenInfo.data['result']['token']}';
// 拿到新的token 再重新发起一次请求(刚因token失效调用失败的接口)
Response res = await dio!.fetch(e.requestOptions);
// 当成功后必须以 resolve 将此次正常的响应返回
return handler.resolve(res);
}
return handler.next(e);
},
));
}
}
}
项目中使用
实现底部导航栏
dart
import 'package:flutter/material.dart';
import '../cart/cart_page.dart';
import '../category/category_page.dart';
import '../home/home_page.dart';
import '../mine/minePage.dart';
class RootPage extends StatefulWidget {
const RootPage({super.key});
@override
State<RootPage> createState() => _RootPageState();
}
class _RootPageState extends State<RootPage> {
/// 页面列表
List pages = [
const HomePage(),
const CategoryPage(),
const CartPage(),
const minePage()
];
int _pageIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: pages[_pageIndex],
// 底部导航
bottomNavigationBar:
// 借助 Theme 去除长按和点击的默认背景色,Theme 组件用于自定义子组件主题样式
Theme(
data: ThemeData(
// 控制点击底部导航栏 icon 图标时的背景色
splashColor: Colors.transparent,
// 控制长按底部导航栏 icon 图标时的背景色
highlightColor: Colors.transparent),
child: BottomNavigationBar(
// 设置选中和未选中的图标 icon 和文字颜色
selectedItemColor: Color(0xFF3CCEAF),
unselectedItemColor: Color(0xFF383838),
// 设置选中时的字体大小
selectedFontSize: 10,
// 设置未选中时的字体大小
unselectedFontSize: 10,
// 监听底部导航点击事件,可以得到当前点击图标的索引值
onTap: (value) {
setState(() {
_pageIndex = value;
});
},
// 设置当前选中的图标项
currentIndex: _pageIndex,
// 设置底部导航样式
type: BottomNavigationBarType.fixed,
items: [
// 首页
BottomNavigationBarItem(
// 设置选中状态图标
// img 组件在加载新图时会默认先将旧图清空,再展示新图之前会有一段空白内容
// 给人感觉就是会“闪一下”,gaplessPlayback 属性设置为 true 时
// 可以不清空旧图
activeIcon: Image.asset('assets/log.jpg',gaplessPlayback: true),
// 设置未选中状态图标
icon: Image.asset('assets/log.jpg'),
// icon: Icon(Icons.home),
label: '首页'),
// 分类
BottomNavigationBarItem(
// 使用 flutter 内置的图标
icon: Icon(Icons.category), label: '分类'),
// 购物车
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart), label: '购物车'),
// 我的
BottomNavigationBarItem(
icon: Icon(Icons.person), label: '我的'),
],
)),
);
}
}
自定义帧动画
dart
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin {
/// 动画控制器
late AnimationController _animationController;
/// 动画对象
late Animation _animation;
@override
void initState() {
// 创建动画控制器
_animationController =
AnimationController(vsync: this, duration: Duration(seconds: 7));
_animation = Tween(begin: 0.0, end: 6.0).animate(_animationController);
_animationController.forward();
// _animationController.reset();
// 监听动画刷新:flutter 动画每一秒中刷新60次(帧)
_animationController.addListener(() {
setState(() {});
});
}
@override
void dispose() {
// 销毁动画
_animationController.dispose();
super.dispose();
}
// 准备帧动画内容
List Frames = [
Container(width: 20, height: 20, color: Colors.red),
Container(width: 20, height: 20, color: Colors.orange),
Container(width: 20, height: 20, color: Colors.yellow),
Container(width: 20, height: 20, color: Colors.green),
Container(width: 20, height: 20, color: Colors.cyan),
Container(width: 20, height: 20, color: Colors.blue),
Container(width: 20, height: 20, color: Colors.purple),
];
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
// 渲染
child: Frames[_animation.value.floor()]);
}
}
打印
轮播图插件
flutter_swiper_null_safety: ^1.0.2 安装插件
dart
import 'package:flutter/material.dart';
import 'package:flutter_swiper_null_safety/flutter_swiper_null_safety.dart';
class imageSwiperWidget extends StatelessWidget {
final List? imageBanners;
final double? height;
final double? borderRadius;
imageSwiperWidget(
{this.imageBanners, this.height = 140, this.borderRadius = 4});
@override
Widget build(BuildContext context) {
return imageBanners != null
? Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(borderRadius!)),
// 用于当父元素设置圆角时同时切割子元素
clipBehavior: Clip.antiAlias,
height: height,
// 使用时必须给 Swiper 设置高度
child: Swiper(
// 设置指示器
// pagination: SwiperPagination(), 使用插件提供的默认指示器
pagination: SwiperPagination(
// 使用自定义指示器
builder: CustomSwiperPagination()),
// 指定自动播放
autoplay: true,
// 指定元素个数
itemCount: imageBanners!.length,
// 指定每个元素
itemBuilder: (context, index) {
return Image.network(
imageBanners![index]['imgUrl'],
fit: BoxFit.cover,
);
},
),
)
: Container(
height: height,
color: Color(0xFFEBEBEB),
);
}
}
// 自定义轮播图指示器
// 继承插件提供的 SwiperPlugin 可以拿到轮播图的信息,方便实现
class CustomSwiperPagination extends SwiperPlugin {
@override
Widget build(BuildContext context, SwiperPluginConfig config) {
// config 可以提供指示器元素个数以及当前展示的指示器索引
int itemCount = config.itemCount;
int activeIndex = config.activeIndex;
// 存放指示器
List<Widget> items = [];
// 循环创建指示器元素
for (var i = 0; i < itemCount; i++) {
bool isActive = i == activeIndex;
items.add(Container(
margin: EdgeInsets.only(left: 3, right: 3),
width: 13,
height: 3,
// white60 表示半透明
color: isActive ? Colors.white : Colors.white60,
));
}
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: items,
);
}
}
瀑布流插件
yaml
flutter_staggered_grid_view: ^0.4.0 #安装插件
dart
Widget _buildItem(List CategoryGrids) {
return StaggeredGridView.countBuilder(
// 表示横向上要排列多少个元素
// 最终每一行展示出的个数 = crossAxisCount / StaggeredTile.fit(1) = 5
crossAxisCount: 5,
// 设置一共有多少个元素
itemCount: CategoryGrids.length,
// 设置纵向上每个元素之间的距离
mainAxisSpacing: 18,
// 禁用滚动
physics: NeverScrollableScrollPhysics(),
// shrinkWrap 搭配 physics 解决滚动视图之间的滚动冲突
shrinkWrap: true,
// 指定每个元素
itemBuilder: (context, index) {
return Column(
children: [
Image.network(CategoryGrids[index]['picture']),
Text(CategoryGrids[index]['name'])
],
);
},
// 表示纵向上需要排列多少个元素
staggeredTileBuilder: ((index) {
return StaggeredTile.fit(1);
}),
);
}
轻提示插件
yaml
flutter_easyloading: ^3.0.3
dart
// 在 main.dart 文件中初始化插件
import 'package:flutter_easyloading/flutter_easyloading.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const RootPage(),
// 初始化插件
builder: EasyLoading.init(),
);
}
}
dart
// 在组件中使用
import 'package:flutter_easyloading/flutter_easyloading.dart';
EasyLoading.showToast('用户名密码必填!');
获取当前设备信息
dart
@override
Widget build(BuildContext context) {
// 可以通过 context 拿到当前设备的一些信息
// 获取屏幕宽度
double screenWidth = MediaQuery.of(context).size.width;
// 计算分类图标的宽度
_ImageWidth = (screenWidth - 6 * 16.0) / 5;
}
UI 或文本溢出
dart
// 文本用 TextOverflow
// Row或Column溢出可将Row或Column替换为 Wrap 组件,Wrap可以将溢出的部分换行显示
return Scaffold(
resizeToAvoidBottomInset:false // 可以通过设置这个属性防止键盘覆盖内容或者键盘撑起内容
)
不规则屏幕的处理方式
dart
// 通过 MediaQuery 即可获取不规则屏幕的范围
Widget build(BuildContext context) {
// MediaQuery.of(context).padding.top 某些设备顶部不规则情况(苹果刘海屏)
// 底部操作栏的高度:自身高度 + 不规则屏幕底部间距
_bottomBarHeight = 60.0 + MediaQuery.of(context).padding.bottom;
}
自定义顶部 appBar
dart
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF7F7F8),
appBar: HomeAppBar(),
);
}
}
// 自定义 appBar 必须实现 PreferredSizeWidget 接口
class HomeAppBar extends StatelessWidget implements PreferredSizeWidget {
// 用于指定顶部 appBar 的高度 kToolbarHeight 默认是 56
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
@override
Widget build(BuildContext context) {
return Container(
// MediaQuery.of(context).padding.top 获取顶部状态栏高度
height: MediaQuery.of(context).padding.top + kToolbarHeight,
color: const Color(0xFF00b796),
padding: const EdgeInsets.only(left: 18, right: 18),
// SafeArea 组件用于解决不规则屏幕(刘海屏)遮挡内容的问题
child: SafeArea(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Icon(Icons.home),
Expanded(
child: Container(
padding: const EdgeInsets.only(left: 18, right: 18),
height: 30,
color: Colors.orange,
)),
const Icon(Icons.home),
],
)));
}
}
dart
import 'dart:io';
import 'package:flutter/services.dart';
void main() {
runApp(const MyApp());
// 注意运行在浏览器端时可能会报错,这里用 try catch 包裹
try {
if (Platform.isAndroid) {
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(statusBarColor: Colors.transparent));
}
} catch (e) {
debugPrint('$e');
}
}
图片插件
cached_network_image 插件可以快速实现图片缓存
dart
cached_network_image: ^3.1.1 // 安装
dart
import 'package:cached_network_image/cached_network_image.dart';
// 使用起来和原生 image 组件差不多
CachedNetworkImage(
imageUrl: hotRecommends['leftIcon'],
width: 100,
height: 100,
fit: BoxFit.cover,
// 设置当图片还没加载完成时的占位图
placeholder: (context, url) {
return Container(
width: width,
height: height,
color: const Color(0xFFEBEBEB),
);
},
// 当图片加载失败时的占位图
errorWidget: (context, url, error) {
return Container(
width: width,
height: height,
color: const Color(0xFFEBEBEB),
);
},
),
下拉刷新
dart
pull_to_refresh: ^2.0.0 // pull_to_refresh 可以快速实现下拉刷新和上拉加载
dart
import 'package:pull_to_refresh/pull_to_refresh.dart';
class _HomePageState extends State<HomePage> {
// 刷新数据插件的控制器
late RefreshController _refreshConttroller;
@override
void initState() {
// 创建刷新数据插件的控制器
_refreshConttroller = RefreshController();
getHttp();
super.initState();
}
getHttp() async {
try {
Response response = await index();
// 当网络请求完成时告诉插件已完成刷新
_refreshConttroller.refreshCompleted();
} catch (e) {
debugPrint('$e');
// 网络请求出错时
_refreshConttroller.refreshFailed();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF7F7F8),
appBar: HomeAppBar(),
// SmartRefresher 用于实现加载
body: SmartRefresher(
// 表示控制器,控制是否刷新成功
controller: _refreshConttroller,
// 是否允许下拉刷新
enablePullDown: true,
// 是否允许下拉加载
enablePullUp: true,
// 下拉刷新执行的回调
onRefresh: () {
getHttp();
},
// 下拉加载回调
onLoading: _onLoading,
// 下拉刷新时的加载动画,CustomHeader 用于自定义动画
header: CustomHeader(
builder: (context, mode) {
String refreshText = '下拉刷新';
// 这时表示用户刚刚往下拉时(空闲)
if (mode == RefreshStatus.idle) {
refreshText = '下拉刷新';
} else if (mode == RefreshStatus.canRefresh) {
// 这里表示用户已经拉到一定距离(可以刷新)
refreshText = '松手刷新';
} else if (mode == RefreshStatus.refreshing) {
// 这里表示用户已经松手(正在刷新)
refreshText = '刷新中';
} else if (mode == RefreshStatus.completed) {
// 这里表示刷新成功
refreshText = '刷新成功';
} else if (mode == RefreshStatus.failed) {
// 这里表示刷新失败
refreshText = '刷新失败';
} else {
// 这里处理一下其他情况
refreshText = '再刷新看看吧';
}
return Container(
alignment: Alignment.center,
child: Text(refreshText),
);
},
),
// 下拉加载动画
footer: CustomFooter(
builder: (BuildContext context,LoadStatus mode){
Widget body ;
if(mode==LoadStatus.idle){
body = Text("pull up load");
}
else if(mode==LoadStatus.loading){
body = CupertinoActivityIndicator();
}
else if(mode == LoadStatus.failed){
body = Text("Load Failed!Click retry!");
}
else if(mode == LoadStatus.canLoading){
body = Text("release to load more");
}
else{
body = Text("No more Data");
}
return Container(
height: 55.0,
child: Center(child:body),
);
},
),
// 其子组件必须是一个可滚动的组件
child: CustomScrollView(
slivers: [],
),
));
}
}
回到顶部
dart
// 定义控制器
late ScrollController? _scrollController;
void initState() {
// 初始化滚动控制器
_scrollController = ScrollController();
}
void dispose() {
// 销毁控制器
_scrollController?.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF7F7F8),
appBar: HomeAppBar(),
body: CustomScrollView(
// 使用滚动控制器
controller: _scrollController,
slivers: [
SliverToBoxAdapter(
child: GestureDetector(
onTap: () {
// 400 毫秒后以先快后慢的动画回到顶部
_scrollController!.animateTo(0,
duration: const Duration(milliseconds: 400),
curve: Curves.ease);
})
])
}
缓存页面
第一步:改写根组件切换方式
dart
class RootPage extends StatefulWidget {
const RootPage({super.key});
@override
State<RootPage> createState() => _RootPageState();
}
class _RootPageState extends State<RootPage> {
// PageView 控制器
PageController? _controller;
/// 页面列表
List<Widget> pages = [
const HomePage(),
const CategoryPage(),
const CartPage(),
const minePage()
];
@override
void initState() {
_controller = PageController();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
// body: pages[_pageIndex],
// 将 PageView 包裹组件并由它来控制切换
body: PageView(
// 禁用滚动
physics: const NeverScrollableScrollPhysics(),
controller: _controller,
children: pages,
),
bottomNavigationBar:
Theme(
// 监听底部导航点击事件,可以得到当前点击图标的索引值
onTap: (i) {
setState(() {
// 控制 PageView 跳转到指定页面
_controller!.jumpToPage(i);
_pageIndex = i;
});
},
currentIndex: _pageIndex,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: '首页'),
BottomNavigationBarItem(
icon: Icon(Icons.category), label: '分类'),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart), label: '购物车'),
BottomNavigationBarItem(
icon: Icon(Icons.person), label: '我的'),
],
)),
);
}
}
第二步:在需要缓存的组件使用 AutomaticKeepAliveClientMixin
dart
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
@override
// 在需要缓存的组件混入 AutomaticKeepAliveClientMixin 并重写 wantKeepAlive 属性
// 还需要调用父类的 build 方法
// true 代表缓存该组件
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
// 缓存组件需要调用父类的 build 方法
super.build(context);
}
}
获取组件信息
dart
class _CategoryPageState extends State<CategoryPage> {
// 定义 GlobalKey ,debugLabel 必须是一个全局惟一的字符串
final GlobalKey _primaryCategoryGlobalKey =
GlobalKey(debugLabel: 'primaryCategoryGlobalKey');
}
void _primaryCategoryOnTap(index) {
// 获取组件位置
// RenderBox box = _keys[i].currentContext?.findRenderObject() as RenderBox;
// 表示从组件左上角取位置,dy表示获取纵坐标
// box.localToGlobal(Offset.zero).dy;
// 根据 GlobalKey 获取组件信息(这里获取组件高度信息)
RenderBox box = _primaryCategoryGlobalKey.currentContext?.findRenderObject()
as RenderBox;
double primaryCategoryHeight = box.size.height;
}
Widget _buildPrimaryCategory() {
return Container(
child: ListView.builder(
// 将 key 绑定到指定组件
key: _primaryCategoryGlobalKey));
}
@override
void initState() {
// 初始化控制器
_primaryController = ScrollController();
super.initState();
}
@override
void dispose() {
// 清除控制器
_primaryController?.dispose();
super.dispose();
}
本地缓存插件
yaml
shared_preferences: ^2.0.7
dart
import 'package:shared_preferences/shared_preferences.dart'
show SharedPreferences;
import 'dart:convert' show json;
Future<bool> saveUserInfo(Map data) async {
// 获取 SharedPreferences 单例
SharedPreferences pre = await SharedPreferences.getInstance();
// 存储数据,json.encode 将数据转化为 json 字符串
return await pre.setString('userInfo', json.encode(data));
// 读取本地存储并将 json 字符串转换为字典
// json.decode(pre.getString('userInfo'));
}
内置图标库
定时器
dart
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_time <= 0) {
// 清除定时器
_timer.cancel();
return;
}
});
GlobalKey 使用
子组件
dart
// 定义 GlobalKey,debugLabel 的值以一个全 app 唯一的 值
GlobalKey<_GoodsDetailAppBarState> goodsDetailAppBarKey =
GlobalKey(debugLabel: 'goodsDetailAppBarKey');
class GoodsDetailAppBar extends StatefulWidget {
// 声明 key
const GoodsDetailAppBar({Key? key}) : super(key: key);
@override
State<GoodsDetailAppBar> createState() => _GoodsDetailAppBarState();
}
class _GoodsDetailAppBarState extends State<GoodsDetailAppBar> {
// 子组件方法
getGoodsDetailAppBarOpacity(double opacity) {
if (myopacity != 1 - opacity) {
setState(() {
myopacity = 1 - opacity;
});
}
}
}
父组件
dart
class GoodsDetailPage extends StatefulWidget {
@override
State<GoodsDetailPage> createState() => _GoodsDetailAppBarState();
}
class _GoodsDetailPageState extends State<GoodsDetailPage> {
_goodsDetailOnscroll(double offset) {
// 直接调用子组件方法
goodsDetailAppBarKey.currentState?.getGoodsDetailAppBarOpacity(opacity);
}
@override
Widget build(BuildContext context) {
// 传递 key(这个为子组件定义的 GlobalKey)
return GoodsDetailBottomBar(key: goodsDetailAppBarKey)
}
}