Appearance
介绍
官方文档 flutter.cn
社区文档 book.flutterchina.club
目录结构
主文件介绍
runApp 函数
dart
void main() {
// runApp 函数是 Flutter 内部提供的一个函数,启动一个 Flutter 应用就是从调用这个函数开始的
runApp(const MyApp());
}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'),
);
}flutter 混合开发
flutter 工程模式
分为如下五种:
1.Flutter App:构建一个标准FlutterApp(统一管理模式),包含 Dart 层和平台层(iOS/Android)
2.Flutter Module:创建一个Flutter 模块(三端分离模式,即混合开发),以模块的形式分别嵌入原生项目
3.Flutter Package:纯 Dart 插件工程,不依赖 Flutter,仅包含Dart层的实现,通常用来定义一些公共库
4.Flutter Plugin:flutter 平台插件,包含 Dart 层与 Native 平台层的实现
5.Flutter Skeleton:自动生成 Flutter 模板,提供常用框架
创建
bash
# --template=type 指定不同的项目类型,有五种,如上面所述。
# --platforms=ios/android/windows/linux/macos/web 指定平台
# 创建 Flutter Module 模式用于混合开发
flutter create --template=module --platforms=android,ios helloiFlutterflutter 与原生通信
混合开发中需要 flutter 消息通道来与原生语言来进行互相调用。
BasicMessageChannel
用于传递字符串和半结构化的信息
dart
// 创建 BasicMessageChannel 实例,用于后续与原生通信
// BasicMessageChannel 名称要与原生完全一致
static const _channel = BasicMessageChannel('messageChannel',StringCodec());
void initState() {
super.initState();
// 通过 BasicMessageChanneL 实例,注册一个接收回调,并且返回信息。
_channel.setMessageHandLer((message) async {
// 接收原生发送过来的字符串消息
print('receive message: $message');
});
}
Future<void>_sendMessage() async {
// 向原生发送信息
String? message = await _chonnel.send('Hello from dart');
print(message);
}EventChannel
用于数据流(event streams)的通信。和 BasicMessageChannel 类似。
dart
static const _channel = EventChannel('eventChannel');
void initState() {
super.initState();
// 通过 BasicMessageChanneL 实例,注册一个接收回调,用于接收消息。
_channel.receiveBroadcastStream().listen((){
(dynamic event) {
print('Receive event: $event');
};
});
}MethodChannel
此种方式最为常用,用于传递方法调用。
dart
static const _channel = MethodChannel('methodchannel');
void initState() async {
super.initState();
// 调用原生端准备好的方法并传递参数,再从函数中获取原生端的返回值
String res = await _channel.invokeMethod('getFLutterInfo',{'name':'张三'});
}组件
无状态组件
纯展示型组件,没有用户交互操作
dart
// 当组件被创建或者父组件状态发生变化导致其需要重新构建时,build 方法会被调用。组件才会更新
// 需要继承 StatelessWidget 类
class MainPage extends StatelessWidget {
// 无状态组件里的数据都是不可变的,所以必须使用 final 定义
final String title = '无状态组件';
// 无状态组件需要重写 build 方法
@override
Widget build(BuildContext context) {
return MaterialApp(
// 使用变量
title: title,
home: Scaffold(
appBar: AppBar(title: Center(child: Text('头部区域'))),
body: Container(child: Center(child: Text('中部区域'))),
bottomNavigationBar: Container(
height: 80,
child: Center(child: Text('底部区域')),
),
),
);
}
}有状态组件
有状态组件是构建动态交互界面的核心,能够管理变化的内部状态,当状态改变时,组件会更新显示内容
dart
void main() {
runApp(MainPage());
}
// 需要创建两个类
// 这个类需要继承 StatefulWidget 类,用于创建一个有状态组件
class MainPage extends StatefulWidget {
@override
State<StatefulWidget> createState() => _MainPageState();
}
// 这个类需要继承 State 类,用于构建视图和处理业务逻辑
// 并且类名需要以 _ 开头,表示这个类是私有的
class _MainPageState extends State<MainPage> {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '有状态组件',
home: Scaffold(
appBar: AppBar(title: Center(child: Text('头部区域'))),
body: Container(child: Center(child: Text('中部区域'))),
bottomNavigationBar: Container(
height: 80,
child: Center(child: Text('底部区域')),
),
),
);
}
}生命周期
无状态组件只有一个生命周期钩子就是 build 函数。
有状态组件常用声明周期钩子函数如下:
dart
// 在编译器里写入 st 即可唤出快捷创建组件代码块
class MainPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
print('createState 执行,第一个执行');
return _MainPageState();
}
}
class _MainPageState extends State<MainPage> {
void initState() {
print('initState 执行,第二个执行');
}
@override
void didChangeDependencies() {
print('didChangeDependencies 执行,第三个执行');
super.didChangeDependencies();
}
@override
void didUpdateWidget(covariant MainPage oldWidget) {
print('didUpdateWidget 执行');
super.didUpdateWidget(oldWidget);
}
@override
void deactivate() {
print('deactivate 执行');
super.deactivate();
}
@override
void dispose() {
print('dispose 执行');
super.dispose();
}
@override
Widget build(BuildContext context) {
print('build 执行,第四个执行');
return MaterialApp(title: '有状态组件', home: Scaffold());
}
}详细生命周期函数如下:
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 从其他页面退回到当前页setState
执行 setState 方法会让组件 build 函数重新执行,进而更新页面。
dart
class _MainPageState extends State<MainPage> {
// 定义变量
int num = 0;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '有状态组件',
home: Scaffold(
body: TextButton(
onPressed: () {
// setState 是异步的,只要调用此函数(即使函数体内没有任何逻辑)组件就会更新
setState(() {
num++;
});
},
// 使用变量
child: Text(num.toString()),
),
),
);
}
}Material 组件库
是 Flutter 内置的一套独有的设计风格,里面有很多拆箱可用的 widget
基础组件
MaterialApp
整个应用被 MaterialApp 包裹,方便我们对整个应用的属性进行整体设计
dart
void main() {
runApp(MaterialApp(
// 用来展示窗口的标题内容
title: 'Flutter',
// 来设置整个应用的主题
theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple)),
// 来展示窗口的主体内容
home: Scaffold(),
));
}Scaffold
用于构建 Material Design 风格页面的核心布局组件,提供标准、灵活配置的页面骨架。常用属性如下:
appBar:页面顶部的应用栏,通常用于显示标题、导航按钮和操作菜单
body:页面的主要内容区域,可以放置任何其他组件,是页面的核心
bottomNavigationBar:底部导航栏,方便用户在不同核心功能页面间切换
backgroungColor:设置整个Scaffold的背景颜色
floatingActionButton:悬浮操作按钮,常用于触发页面的主要动作
dart
// 如下代码就会构建一个分为上中下结构的页面
void main() {
runApp(
MaterialApp(
title: 'Flutte',
home: Scaffold(
appBar: AppBar(title: Center(child: Text('头部区域'))),
body: Container(child: Center(child: Text('中部区域'))),
// 底部栏目
bottomNavigationBar: BottomNavigationBar(
// 是否展示未被选中栏目的文本
showUnselectedLabels: true,
// 未选中栏目的文字颜色
unselectedItemColor: const Color(0x009e9e9e),
// 选中栏目的文字颜色
selectedItemColor: Colors.black,
// 当前选中的栏目索引
currentIndex: 0,
// 底部导航栏的布局行为和视觉表现
// fixed 会强制所有导航项的宽度均等分配
type: BottomNavigationBarType.fixed,
// 切换栏目的回调函数
onTap: (value) {},
// 栏目列表
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home_filled),
activeIcon: Icon(Icons.home),
label: '首页',
)
],
)
),
),
);
}AppBar组件
用于实现顶部应用栏。
dart
Scaffold(
appBar: AppBar(
title: Text('登陆'),
backgroundColor: Colors.amber,
// 标题是否居中显示,设置为 false 时标题会居左显示
centerTitle: false,
// 左侧按钮
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context), // 返回上一页
),
// 右侧按钮
actions: [TextButton(onPressed: () {}, child: Text('右侧按钮'))],
)
)自定义 AppBar
dart
Scaffold(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),
// SafeArea 组件用于解决不规则屏幕(刘海屏)遮挡内容的问题
child: SafeArea());
}
}堆叠容器组件
会保留所有子组件的状态并仅显示指定索引的子组件,可用于构建首页选项卡切换场景。
页面切换时无需重新构建,切换流畅无卡顿,适合需要快速切换且保持状态的场景,但子组件过多或复杂时,可能导致内存压力和初始加载延迟
注意:IndexedStack 组件会一次性渲染所有子组件,且切换页面后所有子页面都不会销毁
dart
int _currentIndex = 0;
Scaffold(
body: SafeArea(
child: IndexedStack(
index: _currentIndex,
children: [Text('首页'), Text('分类'), Text('购物车'), Text('我的')],
),
bottomNavigationBar: BottomNavigationBar(
// 栏目列表
items: [
BottomNavigationBarItem(label: '首页'),
BottomNavigationBarItem(label: '分类'),
BottomNavigationBarItem(label: '购物车'),
BottomNavigationBarItem(label: '我的')
],
)
)
);PageView 组件
同样可用于构建首页选项卡切换场景。
仅渲染当前页面,滑动时按需加载,可配合 AutomaticKeepAliveClientMixin 缓存页面状态,缓存的页面不会销毁,再次切换时不会重新构建。
dart
安全区域组件
用于解决设备屏幕边缘安全区域的适配问题,不同设备的非展示区域不一致如全面屏、刘海屏等
dart
// SafeArea 本质上是 MediaQuery 的封装
// 在组件中可通过 MediaQuery.of(context).padding 获取安全区域的内边距
Scaffold(
body: SafeArea(
// 有 top、bottom、left、right 四个属性控制是否在对应方向添加安全区域内边距默认均为 true
left: false,
// 指定最小内边距(例如避免内容过于靠近边缘)
minimum: EdgeInsets.only(left: 20),
child: Center(child: Text("内容不会进入刘海区域")),
);事件组件
用户与应用程序交互时触发的各种动作,比如触摸屏幕、滑动、点击等
使用 GestureDetector 包裹需要被触发事件的元素即可
dart
GestureDetector(
onTap: () {
print('点击轻触事件');
},
child: Text('事件')
)flutter 还有许多其他组件也提供事件,如:TextButton、Inkwell、Checkbox 等
dart
// 会渲染一个按钮,自带点击事件
TextButton(
onPressed: () {
print('按钮点击事件');
},
child: Text('中部区域'),
)Container
是一个拥有绘制、定位、调整大小的组件
dart
Container(
width: 200,
height: 200,
// 设置内边距,这里设置的是四个方向
padding: EdgeInsets.all(20),
// 设置外边距,这里设置的是四个方向
margin: EdgeInsets.all(20),
// 设置子元素水平垂直居中
alignment: Alignment.center,
// 设置旋转,这里设置的是弧度不是角度,所以0.06表示36度
transform: Matrix4.rotationZ(0.06),
decoration: BoxDecoration(
// 设置背景色
color: Colors.blue,
// 设置圆角
borderRadius: BorderRadius.circular(15),
// 设置边框颜色
border: Border.all(color: Colors.amber, width: 5),
// 设置背景图
image: DecorationImage(
// AssetImage() 可设置本地背景图
image: NetworkImage('图片地址'),
fit: BoxFit.cover,
),
),
child: Text(
'Hello Container',
style: TextStyle(color: Colors.white, fontSize: 20),
),
)Center
水平、垂直剧中:将其子组件在父容器的空间内进行水平和垂直方向上的居中排列
默认占满父容器:Center不能设置宽高,Center的最终大小取决于其父组件传递给它的约束,Center会向它的父组件申请尽可能大的空间
dart
Center(child: Text('Hello World'))Text
在用户界面中显示文本的基础组件
dart
Text(
'Text 组件',
// 文本在容器内的水平对齐方式,如.left,.center
textAlign: TextAlign.center,
// 文本显示的最大行数
maxLines: 2,
// 文本超出后的展示方式,ellipsis 表示省略号
overflow: TextOverflow.ellipsis,
// 文本样式
style: TextStyle(
// 字体大小
fontSize: 30,
// 字体颜色
color: Colors.red,
// 字体粗细
fontWeight: FontWeight.bold,
// 字体斜体
fontStyle: FontStyle.italic,
// 字体下划线
decoration: TextDecoration.underline,
// 字体下划线颜色
decorationColor: Colors.blue,
),
)Text/TextSpan
如果需要在同一段文本中显示不同样式,可用 Text.rich 配合 TextSpan 来实现
dart
Text.rich(
TextSpan(
text: '第一段文字',
style: TextStyle(color: Colors.red, fontSize: 30),
children: [
TextSpan(
text: '第二段文字',
// 此段文本的字体大小会继承父组件设置的字体大小,也就是 30
style: TextStyle(color: Colors.blue),
),
],
),
)输入框组件
实现文本输入功能的核心组件,使用它时必须是一个有状态组件
dart
// 定义文本编辑器控制器 _controller.text 获取文本内容
// _controller.clear() 清空内容
final TextEditingController _controller = TextEditingController();
TextField(
// 文本编辑器控制器,用于获取、设置文档内容及监听变化
controller: _controller,
// 输入框内容变化时触发此函数,函数参数能获取当前的输入内容
onChanged: (value) {
print(value);
},
// 输入框内容提交时触发此函数,函数参数能获取当前的输入内容
onSubmitted: (value) {
print(value);
},
// 光标设置
cursorRadius: const Radius.circular(10),
cursorColor: const Color(0xFFFFBD3B),
// 表单类型
keyboardType: TextInputType.text,
// 默认为 false 设置为 true 时不显示实际内容,输入的内容会密文展示。即密码框
obscureText: true,
// 最大字符数
maxLength: 5,
// 最大行数,默认为 1
maxLines: 1,
// 输入文本的样式
style: TextStyle(color: Colors.red),
// 设置输入框样式
decoration: InputDecoration(
// 输入框内边距
contentPadding: EdgeInsets.only(left: 20),
// 输入框提示文本
hintText: '请输入',
// 输入框提示文本样式
hintStyle: TextStyle(color: Colors.red),
// 辅助提示信息,可用于校验表单时展示在输入框下面的提示文字
helperText: '',
// 辅助提示信息样式
helperStyle: TextStyle(color: Colors.red),
// 输入框填充颜色,即背景色
fillColor: Colors.amber,
// 设置背景色时必须设置 filled 为 true,表示允许展示填充颜色
filled: true,
border: OutlineInputBorder(
// 设置输入框边框大小,none 为无边框
borderSide: BorderSide.none,
// 设置圆角
borderRadius: BorderRadius.circular(10),
),
),
)表单组件
Form 组件配合 TextFormField 组件可实现表单校验
dart
// 定义 GlobalKey 用于表单校验
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
Form(
key: _formKey,
child: Column(
children: [
// 密码框
TextFormField(
// 表单校验
validator: (value) {
if (value == null || value.isEmpty) {
// 这里返回的字符会显示在输入框下方
return '密码不能为空';
}
// 使用正则校验
if (!RegExp(r"^1[3-9]\d{9}$").hasMatch(value)) {
return "手机号格式不正确";
}
return null;
},
controller: _phoneController,
// 是否密文,即密码框
obscureText: true,
// 设置样式
decoration: InputDecoration(
// 内容内边距
contentPadding: EdgeInsets.only(left: 20),
// 输入框提示文字
hintText: '请输入密码',
// 输入框填充颜色
fillColor: const Color.fromRGBO(243, 243, 243, 1),
filled: true,
border: OutlineInputBorder(
// 设置外边框
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(25),
),
),
),
// 单选框
Checkbox(
value: _isChecked,
// 选中状态发生变化时触发
onChanged: (bool? value) {
setState(() {
_isChecked = value ?? false;
});
},
),
// 按钮
ElevatedButton(
// 点击后触发
onPressed: () {
// 根据 GlobalKey 调用 Form 组件的表单校验方法触发校验
if (_formKey.currentState!.validate()) {
// 校验通过
}
},
// 按钮样式
style: ElevatedButton.styleFrom(),
child: Text('登录')
)
],
),
)分割线组件
用于在界面中创建水平分隔线以区分内容区块。
dart
Divider(
// 线条高度(含上下间距)
height: 5,
// 线条颜色
color: Colors.grey,
// 线条厚度
thickness: 2,
// 左侧缩进
indent: 20,
// 右侧缩进
endIndent: 20
)Align、Icon
可以精确控制其子组件在父容器空间内的对齐位置
Center 组件是 Align 组件的一个特例,继承自 Align,相当于一个将 alignment 属性为居中的 Align.center
dart
Align(
// 设置子元素对其方式
alignment: Alignment.center,
// 宽度因子,Align 组件的宽度是子元素宽度的 n 倍,这里是 1 倍
widthFactor: 1,
// 高度因子,Align 组件的高度是子元素高度的 n 倍,这里是 1 倍
heightFactor: 1,
// Icon 组件,flutter 内置了许多 icon 图标,可使用 Icon 组件使用
child: Icon(Icons.star, size: 150, color: Colors.yellow),
)Padding
功能单一而纯粹,就是添加内边距。如果需求仅是为组件添加间距,那么直接使用 Padding 组件
dart
Padding(padding: EdgeInsets.all(20), child: Text('Padding 组件'))Image
在用户界面中显示图片的核心部件
图片分类:
1)Image.asset():加载项目资源目录(assets)中的图片。需要在 pubspec.yaml 文件中声明资源路径
2)Image.network():直接从网络地址加载图片
3)Image.file():加载设备本地存储中的图片文件
4)Image.memory():加载内存中的图片数据
asset
1.先在 pubspec.yaml 文件声明资源路径
yaml
flutter:
assets:
# 设置本地资源路径
- lib/images/dart
Image.asset(
// 图片路径
'lib/images/7.png',
width: 50,
height: 50,
// 控制图片如何适应其显示区域(父组件区域),例如是否拉伸、裁剪或保持原比例
fit: BoxFit.none,
// 图片对齐方式
alignment: Alignment.center,
// 当图片小于显示区域(父组件区域)时,设置是否以及如何重复平铺图片
repeat: ImageRepeat.repeat,
// Image 组件在加载新图片时默认先将旧图片清空,所以在新图加载展示完成之前会有一段空白内容,会造成 “闪一下”的问题
// gaplessPlayback 设置为 true 可以指定在加载新图时不清空旧图直到新图展示出来为止,可以解决上述问题
gaplessPlayback: true,
)network
dart
// 加载网络图片
Image.network(
// 当加载网络图片失败时会执行此函数
errorBuilder: (context, error, stackTrace) {
// 可在此函数返回一个默认图片进行展示
return Image.asset('');
},
'网络地址'
)裁切组件
用于将子组件裁剪为圆角矩形的组件,常用于实现圆角图片、按钮、卡片等视觉效果
dart
ClipRRect(
// 圆角裁切 circular() 表示统一裁切(四个角)
// only() 表示只裁切某一个角或多个角
borderRadius: BorderRadius.circular(8),
child: Image.network(
'图片路径',
width: 100,
height: 140,
fit: BoxFit.cover
)
)下拉刷新组件
RefreshIndicator 可快速实现下拉刷新的功能
需要包裹可滚动组件
dart
RefreshIndicator(
// 指示器颜色
color: Colors.blue,
// 背景色
backgroundColor: Colors.grey[100],
// 触发距离
displacement: 50,
onRefresh: () async {
print('触发下拉刷新');
},
child: CustomScrollView(slivers: [SliverToBoxAdapter()])
)消息提示组件
在屏幕底部显示短暂的消息(如操作反馈、提示信息),支持自定义内容、操作按钮和自动隐藏
dart
// 可封装为工具函数,方便外部直接调用
// 定义当前是否有提示框在展示,避免同时出现多个提示框
bool _isShowLoading = false;
msg(BuildContext context, {String msg = '刷新成功', int seconds = 2}) {
if (_isShowLoading) return;
_isShowLoading = true;
// 添加定时器,在指定时间后关闭提示框
Future.delayed(Duration(seconds: seconds), () {
_isShowLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
// 轻提示主体内容
SnackBar(
// 设置背景颜色
backgroundColor: Colors.white,
width: 120,
// 设置圆角
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(40)),
// 控制消息显示位置
// floating 表示悬浮在屏幕底部上方与屏幕底部保持一定间距(可通过 margin 属性调整)
// fixed 默认值。表示固定在屏幕底部
behavior: SnackBarBehavior.floating,
// 提示停留时间
duration: Duration(seconds: 10),
// 必填,显示的主内容(Text、Icon等)
content: Text('刷新成功', textAlign: TextAlign.center),
// 可选,右侧操作按钮(如撤销、关闭)
action: SnackBarAction(
label: '确定',
onPressed: () {
print('确定');
},
),
),
);
}
// 在页面中直接导入直接调用即可
msg(context);弹框组件
dart
showDialog(
context: context,
builder: (context) => AlertDialog(
// 弹框标题
title: Text('提示'),
// 主体提示信息
content: Text('您还未登录,是否前往登录?'),
// 底部按钮
actions: [
TextButton(
onPressed: () {
// 关闭弹框
Navigator.pop(context);
},
child: Text('取消'),
),
TextButton(onPressed: () {}, child: Text('确认')),
],
),
)封装 loading 组件
会在屏幕中央出现一个转圈等待的 loading 效果
dart
// lib/utils/loading
showLoading(BuildContext context, {String msg = '加载中...'}) {
showDialog(
context: context,
builder: (context) => Dialog(
backgroundColor: Colors.transparent,
child: Center(
child: Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 转圈加载组件
CircularProgressIndicator(),
SizedBox(height: 10),
Text(msg),
],
),
),
),
),
);
}
// 页面中通过 Navigator.pop 关闭 loading
Navigator.pop(context);布局组件
线性布局
Column
用于垂直排列其子组件的核心布局容器
本身不支持滚动,如果内容超出,需要使用 ListView 或者 SingleChildScrollView 包裹
明确尺寸约束,父组件的大小直接影响 Column 的最终大小和子组件的布局行为
dart
SizedBox(
// double.infinity 表示正无穷大,这里表示宽度占满整个父容器,类似于 css 中的 width: 100%
width: double.infinity,
height: double.infinity,
child: Column(
// 设置每个子组件主轴的间距
spacing: 10,
// 决定 Column 本身在垂直方向上的尺寸策略:是占满所有可用空间(max),还是仅仅包裹子组件内容(min)
mainAxisSize: MainAxisSize.max,
// 设置主轴(纵向)对齐方式,与 css 中 flex 布局类似
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// 设置交叉轴(辅轴,横向)对齐方式,与 css 中 flex 布局类似
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(height: 100, width: 100, color: Colors.red),
Container(height: 100, width: 100, color: Colors.blue),
Container(height: 100, width: 100, color: Colors.green),
],
),
)Row
用于水平排列其子组件的核心布局容器
本身不支持滚动,如果内容超出,需要使用 ListView 或者 SingleChildScrollView 包裹
明确尺寸约束,父组件的大小直接影响 Row 的最终大小和子组件的布局行为
dart
SizedBox(
width: double.infinity,
height: double.infinity,
child: Row(
// 设置每个子组件主轴的间距
spacing: 10,
// 决定 Row 本身在垂直方向上的尺寸策略:是占满所有可用空间(max),还是仅仅包裹子组件内容(min)
mainAxisSize: MainAxisSize.max,
// 设置主轴(横向)对齐方式,与 css 中 flex 布局类似
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// 设置交叉轴(辅轴,纵向)对齐方式,与 css 中 flex 布局类似
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(height: 100, width: 100, color: Colors.red),
Container(height: 100, width: 100, color: Colors.blue),
Container(height: 100, width: 100, color: Colors.green),
],
),
)弹性布局
Flex、Expanded
允许沿一个主轴(水平或垂直)排列其子组件,灵活地控制这些子组件在主轴上的尺寸比例和空间分配
Flex 的子组件常使用 Expanded 来控制空间分配
Flex 是 Column 和 Row 的结合体
Expanded 作为 Flex 的子组件通过 flex 属性来分配 Flex 组件空间
Expanded 组件强制子组件填满所有剩余空间,Flexible 组件根据自身大小调整,不强制占满空间
dart
Flex(
// 决定主轴的方向,这里是纵向
direction: Axis.vertical,
// 决定 Row 本身在垂直方向上的尺寸策略:是占满所有可用空间(max),还是仅仅包裹子组件内容(min)
mainAxisSize: MainAxisSize.max,
// 设置主轴对齐方式,与 css 中 flex 布局类似
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// 设置交叉轴(辅轴)对齐方式,与 css 中 flex 布局类似
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(height: 100, width: 100, color: Colors.red),
Expanded(
// 设置子组件在 Flex 组件中所占的比例。
// 这里只有这一个子组件使用了 Expanded 则可以实现
// 垂直方向上顶部和尾部组件高度固定,此组件高度动态占满 Flex 组件的效果
flex: 1,
child: Container(height: 100, width: 100, color: Colors.blue),
),
Container(height: 100, width: 100, color: Colors.green),
],
)流式布局
流式布局组件,当子组件在主轴方向上排列不下时,它会自动换行(或换列)
Column、Row、Flex内容超出均不会换行
Wrap 组件更像是 Flex 组件加了换行特性
dart
List<Widget> getList() {
// 生成 20 个相同的组件
return List.generate(
20,
(index) => Container(height: 50, width: 50, color: Colors.red),
);
}
Wrap(
// 决定主轴的方向
direction: Axis.horizontal,
// 子组件在主轴方向上的对齐方式,与 css 中 flex 布局类似
alignment: WrapAlignment.spaceBetween,
// 交叉轴(辅轴)方向上的对齐方式,与 css 中 flex 布局类似
runAlignment: WrapAlignment.center,
// 主轴方向上,子组件之间的间距
spacing: 10,
// 交叉轴(辅轴)方向上,行或列之间的间距
runSpacing: 10,
children: getList(),
)层叠布局
Stack/Positioned
允许将多个子组件按照 Z 轴(深度方向)进行叠加排列。
Positiohed 是 Stack 的黄金搭档,对子组件进行精确定位控制。Positioned 必须作为 Stack 的直接子组件。
dart
Container(
width: double.infinity,
height: double.infinity,
color: Colors.black,
// Stack 组件自身并不能设置宽高,它的大小由父组件决定,它会撑满父组件
child: Stack(
// 控制非定位子组件(未被 Positioned 包裹)在 Stack 内的对齐方式,默认左上角
alignment: AlignmentGeometry.topLeft,
// 定义非定位子组件(未被 Positioned 包裹)如何适应 Stack 的约束条件
// StackFit.expand 表示子组件强制扩展至 Stack 的最大约束尺寸。例如 Stack 约束为 300×500,子组件尺寸固定为 300×500。
fit: StackFit.expand,
// 控制子组件超出 Stack 边界时的裁剪方式
// Clip.none 表示不裁剪,超出部分完全可见。
clipBehavior: Clip.none,
// children 中的子组件会按照顺序排列,后添加的会覆盖在前面组件之上
children: [
Container(width: 50, height: 50, color: Colors.blue),
Container(width: 40, height: 40, color: Colors.red),
// Positioned 组件可以设置子组件在 Stack 内的位置
Positioned(
// 当同时设置水平(left、right)或垂直(top、bottom)定位时,会自动拉伸容器
left: 20,
top: 20,
child: Container(width: 30, height: 30, color: Colors.green),
),
],
),
)使用技巧
dart
Stack(
// 所有子组件会以 Stack 的中心点为基准进行对齐,实现水平 + 垂直双方向居中效果
alignment: FractionalOffset.center,
children: [
Container(width: 40, height: 40, color: Colors.red),
Positioned(
// 此时子组件再设置 bottom(left、top 均可)后可快速实现水平居中,且距离容器底部 20 的位置
bottom: 20,
child: Container(width: 30, height: 30, color: Colors.green),
),
],
),滚动组件
单个滚动组件
只能包含一个子组件,让单个子组件具备滚动能力。
此组件一次性构建所有子组件,如果嵌套的 Column 或 Row 中包含大量子项,可能会导致性能问题,建议使用 ListView
dart
// 定义控制器 _controller.jumpTo(100) 滚动到指定位置。
// _controller.jumpTo(0) 滚动到页面顶部
// _controllerr.position.maxScrollExten 滚动到页面底部
// 有过渡动画的滚动 如下代码表示会在 1s 内丝滑滚动到页面顶部
// _controller.animateTo(0, duration: Duration(milliseconds: 1), curve: Curves.easeIn);
final ScrollController _controller = ScrollController();
SingleChildScrollView(
// 滚动方向 默认为垂直方向(Axis.vertical),也可设置为水平方向(Axis.horizontal)
scrollDirection: Axis.vertical,
// 控制器,用于控制滚动位置
controller: _controller,
// 设置内边距
padding: EdgeInsets.only(left: 10, right: 10),
child: Column(
spacing: 20,
children: List.generate(10, (index) {
return Container(
width: double.infinity,
height: 100,
color: Colors.blue,
);
}),
),
)列表滚动组件
用于构建可滚动列表的核心部件,并提供流畅滚动体验
提供多种构造函数,如默认构造函数、ListView.builder、ListView.separated
采用按需渲染(懒加载),只构建当前可见区域的列表项,极大提升长列表性能
默认构造函数
会一次性构建所有子组件,适用于静态数量有限数据一次性构建所有表项
dart
final ScrollController _controller = ScrollController();
// 给控制器添加滚动事件
_controller.addListener(() {
// 如果页面滚动到底部时触发
if (_controller.position.pixels ==
_controller.position.maxScrollExtent) {}
});
ListView(
// 滚动效果,比如:禁用滚动、滚动回弹效果等等.这里是禁止滚动效果
physics: const NeverScrollableScrollPhysics(),
// 否根据子组件的总长度来设置 ListView 的长度。默认为 false
shrinkWrap: true,
// 滚动方向 默认为垂直方向(Axis.vertical),也可设置为水平方向(Axis.horizontal)
scrollDirection: Axis.vertical,
// 控制器,用于控制滚动位置
controller: _controller,
// 设置内边距
padding: EdgeInsets.only(left: 10, right: 10),
children: List.generate(10, (index) {
return Container(
width: double.infinity,
height: 100,
color: Colors.blue,
);
}),
)builder 模式
处理长列表或动态数据的首选和推荐方式
按需构建,不会一次性创建所有子组件,而是根据用户的滚动的可视区域,动态地创建和销毁列表项
dart
ListView.builder(
// 需要构建子组件的个数
itemCount: 10,
// 使用该属性代替给子组件设置高可以提高性能
// 通过提前告知 Flutter 每个项的精确尺寸,避免布局过程中动态计算尺寸,显著提升长列表的滚动性能(减少布局抖动和GPU重绘)
itemExtent: 100,
// 负责根据索引 index 动态生成列表项组件
itemBuilder: (context, index) {
return Container(
margin: EdgeInsets.only(bottom: 10),
width: double.infinity,
color: Colors.blue,
);
},
)separated 模式
在 ListView.builder 的基础上,额外提供了构建分割线的能力
dart
ListView.separated(
// 需要构建子组件的个数
itemCount: 10,
// 用于渲染列表的每个子组件
itemBuilder: (context, index) {
return Container(
margin: EdgeInsets.only(bottom: 10),
width: double.infinity,
color: Colors.blue,
);
},
// 设置分割线组件,会在每个子组件只见渲染此分割线内容
separatorBuilder: (context, index) {
return Container(
margin: EdgeInsets.only(bottom: 10),
width: double.infinity,
color: Colors.red,
);
},
)网格滚动组件
用于创建二维可滚动网格布局的核心组件
提供多种构建方式,GridView.count、GridView.extent、GridView.builder 等
count 模式
使用 GridView.count 创建固定列数网格
GridView.count 以列数为优先。指定网格多少列,Flutter 自动计算列的宽度,在空间内均匀排列
dart
GridView.count(
// 内边距
padding: EdgeInsets.only(left: 10, right: 10),
// 辅轴(默认横向)的子组件数量
crossAxisCount: 3,
// 主轴(默认纵向)的子组件间距
mainAxisSpacing: 10,
// 辅轴(默认横向)的子组件间距
crossAxisSpacing: 10,
// 每个子组件的长宽比
childAspectRatio: 0.5,
// 设置滚动方向,默认是纵向
scrollDirection: Axis.vertical,
children: List.generate(
9,
(index) => Container(color: Colors.blue),
),
)extent 模式
使用 GridView.extent 指定子项最大宽度或者高度,子组件在辅轴上展示个数不固定
GridView.extent 通过 maxCrossAxisExtent 设置子项最大宽度/高度来计算横向或者纵向有多少列
dart
GridView.extent(
// 设置辅轴(默认横向)的每个子组件最大宽度
maxCrossAxisExtent: 100,
// 主轴(默认纵向)的子组件间距
mainAxisSpacing: 10,
// 辅轴(默认横向)的子组件间距
crossAxisSpacing: 10,
// 每个子组件的长宽比
childAspectRatio: 1,
// 设置滚动方向,默认是纵向
scrollDirection: Axis.vertical,
children: List.generate(
20,
(index) => Container(color: Colors.blue),
),
)builder 模式
使用 GridView.builder 实现动态长网格(懒加载,只渲染可见区域)
dart
GridView.builder(
// 布局委托,用于控制子组件的排列方式
// SliverGridDelegateWithFixedCrossAxisCount:固定列数。和 GridView.count 一样
// SliverGridDelegateWithMaxCrossAxisExtent:最大宽度或高度。和 GridView.extent 一样
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 辅轴(默认横向)的子组件数量
crossAxisCount: 3,
// 主轴(默认纵向)的子组件间距
mainAxisSpacing: 10,
// 辅轴(默认横向)的子组件间距
crossAxisSpacing: 10,
// 每个子组件的长宽比
childAspectRatio: 4 / 3,
),
// 需要构建子组件的个数
itemCount: 20,
// 设置滚动方向,默认是纵向
scrollDirection: Axis.vertical,
itemBuilder: (context, index) {
return Container(color: Colors.blue);
},
)自定义滚动组件
用于组合多个可滚动组件(如列表、网格),实现统一协调的滚动效果
用法:通过 slivers 属性接收一个 Sliver 组件列表,此列表不能直接放入之前的组件,必须是 Sliver 组件
常用 Sliver 组件对应关系:
SliverList => ListView
SliverGrid => GridView SliverAppBar => AppBar SliverPadding => Padcing SliverToBoxAdapter => ToBoxAdapter(用于包裹普通Widget) SliverPersistentHeader(粘性吸顶)
dart
CustomScrollView(
// 切片列表。里面不可以直接放置 GridView、ListView、Container 等其他所有组件,否则会报错。
// 需要放置特定和其功能一样的组件,比如 SliverGrid、SliverList、SliverToBoxAdapter 等。
slivers: [
// slivers 中如果想使用其他普通组件(除 GridView、ListView 等)需要使用 SliverToBoxAdapter 包裹
SliverToBoxAdapter(child: Container()),
SliverList.builder(
itemCount: 10,
itemBuilder: (context, index) =>
Container(width: 50, height: 50, color: Colors.red),
),
SliverGrid.count(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
children: List.generate(
10,
(index) => Container(width: 50, height: 50, color: Colors.blue),
),
),
],
)分页组件
用于实现分页滚动视图的核心组件
提供多种构建方式,默认构造方式、PageView.builder 等
默认构造方式
会一次性构建所有子组件
dart
// 跳转到指定页码
// _pageController.jumpToPage(2);
// 带平滑滚动动画的跳到指定页码
_pageController.animateToPage(2,duration: Duration(seconds: 1),curve: Curves.ease);
// PageView 默认会占满父组件
PageView(
// 分页控制器
controller: _pageController,
// 翻页时触发
onPageChanged: (index) {
print('当前页码:$index');
},
// 滚动方向,默认 horizontal 横向滚动
scrollDirection: Axis.horizontal,
children: List.generate(
5,
(index) =>
Container(color: Colors.blue, child: Text('第$index个页面')),
),
)builder 模式
支持懒加载(按需渲染)
dart
PageView.builder(
// 滚动方向,默认 horizontal 横向滚动
scrollDirection: Axis.horizontal,
// 子组件的个数
itemCount: 5,
itemBuilder: (itemBuilder, index) =>
Container(color: Colors.blue, child: Text('第$index个页面')),
)吸顶组件
当页面滚动了一定距离后再往下滚动,此组件则会固定在页面顶部
dart
CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Container(height: 300, color: Colors.blue),
),
// 当在自定义滚动组件中使用时需要使用 SliverPersistentHeader 组件包裹
// 使用吸顶组件 pinned 表示是否吸顶
SliverPersistentHeader(delegate: MyPersistentHeaderDelegate(), pinned: true),
],
)
// 定义吸顶组件,需继承 SliverPersistentHeaderDelegate 类
class MyPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
// 此处放置吸顶组件内容
return Container(height: 44, color: Colors.black);
}
@override
// 定义吸顶视图最大高度
double get maxExtent => 44;
@override
// 定义吸顶视图最小高度
double get minExtent => 10;
@override
// 在视图吸顶时是否需要重新构建视图
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
// 参数 oldDelegate 表示当前视图是否发生变化
// 如果返回 true 表示在视图吸顶的时候需要重新构建吸顶视图
throw false;
}
}动画组件
过渡动画容器
dart
// 当样式发生变化时可以自动添加动画
AnimatedContainer(
// 过渡动画时长,这里是 300 毫秒
duration: Duration(milliseconds: 300),
// 当宽度发生变化时自动添加过渡动画
width: isShow ? 200 : 100,
decoration: BoxDecoration(
// 当背景色发生变化时自动添加过渡动画
color: isShow ? Colors.amber : Colors.black,
),
)平移动画
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 _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,
),
),
));
}
}主题组件
主题组件可以修改子组件的主题样式
dart
// 使用 Theme 组件修改 BottomNavigationBar 组件点击和长按时的背景色
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: Theme(
data: ThemeData(
// 设置点击时的背景颜色为透明
splashColor: Colors.transparent,
// 设置长按时的背景颜色为透明
highlightColor: Colors.transparent),
child: BottomNavigationBar(),
),
);
}导航组件
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
// 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
// AbsorbPointer 组件用于禁止组件事件()输入,absorbing 为 true 时表示禁止
// 如下写法可以阻止 TabBar 组件的点击事件
AbsorbPointer(absorbing: true,child: TabBar())组件通信
父传子
构造函数方式
子组件是无状态组件时
dart
class Child extends StatelessWidget {
// 1.在子组件中定义属性,用于接收父组件传递过来的信息
// 子组件定义接收属性需要使用 final 关键字。因为属性由父组件决定,子组件不能随意更改
final String txt;
// 2.子组件在构造函数中接收参数
const Child({super.key, required this.txt});
Widget build(BuildContext context) {
return Text('子组件$txt');
}
}
class Parent extends StatelessWidget {
Widget build(BuildContext context) {
return Child(txt: '父组件信息');
}
}子组件是有状态组件时
dart
// 父组件传递数据同上
class Child extends StatefulWidget {
// 1.在子组件中定义属性,用于接收父组件传递过来的信息
// 子组件定义接收属性需要使用 final 关键字。因为属性由父组件决定,子组件不能随意更改
final String txt;
// 2.子组件在构造函数中接收参数
const Child({super.key, required this.txt});
}
class _ChildState extends State<Child> {
Widget build(BuildContext context) {
// 3.子组件是有状态组件时,获取父组件传递的属性时需要通过 widget 来获取
return Text('子组件${widget.txt}');
}
}父组件直接调用子组件方法
GlobalKey 是一个方法可以创建一个 key 绑定到 Widget 部件上可以操作 Widget 部件
dart
// 如果子组件是有状态组件
// 注意继承自 State 的类需要改为非私有(即去掉下划线)
class HmMoreListState extends State<HmMoreList> {
clear() {
print('执行');
}
}
// 父组件
// 定义 GlobalKey 泛型设置为子组件继承自 State 的类即可
final GlobalKey<HmMoreListState> hmMoreListKey = GlobalKey<HmMoreListState>();
Future<void> onRefresh() async {
// 直接调用子组件的方法
hmMoreListKey.currentState?.clear();
}
Widget build(BuildContext context) {
// 将 GlobalKey 传递给子组件
return HmMoreList(controller: controller, key: hmMoreListKey);
}子传父
回调函数方式
dart
class Child extends StatefulWidget {
// 在子组件中定义函数,用于接收父组件传递过来的回调函数
// 子组件定义接收属性需要使用 final 关键字。因为属性由父组件决定,子组件不能随意更改
final Function req;
// 子组件在构造函数中接收参数
const Child({super.key, required this.req});
}
class _ChildState extends State<Child> {
Widget build(BuildContext context) {
return TextButton(
onPressed: () {
// 在子组件中使用父组件传递过来的回调函数并传递参数
widget.req('hello');
},
child: Text('子组件'),
);
}
}
class Parent extends StatelessWidget {
Widget build(BuildContext context) {
// 在父组件中传递一个函数给子组件
void req(String txt) {
// 从回调函数中接收从子组件传递过来的
print(txt);
}
// 将回调函数传递给子组件
return child: Child(req: req);
}
}网络请求
https://pub.flutter-io.cn/ 对标 js 中的 npm
安装 dio
可以直接通过命令的方式安装
bash
flutter pub add dio之后打开 pubspec.yaml 查看依赖项是否安装成功
当然也可以直接将需要安装的插件名称以及对应版本号直接写入,保存即可,flutter 会自行下载安装
yaml
dependencies:
# 如直接粘贴 dio 与对应版本号保存 flutter 会自行下载安装
dio: ^5.9.0基本使用
dart
void main() {
Dio()
.get("https://geek.itheima.net/v1_0/channels")
.then((res) => print(res))
.catchError((e) {});
}封装 dio
在 lib/utils/request.dart 写入
dart
class DioUtils {
// 定义私有属性
final Dio _dio = Dio(
BaseOptions(
// 基础路径
baseUrl: 'https://geek.itheima.net/v1_0/',
// 连接超时时间
connectTimeout: Duration(seconds: 10),
// 发送超时时间
sendTimeout: Duration(seconds: 10),
// 接收超时时间
receiveTimeout: Duration(seconds: 10)
),
);
// 在默认构造函数做些初始化操作
DioUtils() {
// 添加拦截器
_dio.interceptors.add(
InterceptorsWrapper(
// 请求拦截器
onRequest: (context, handler) {
// handLer.next(requestoptions)放过请求
// handLer.reject(error)拦截请求
handler.next(context);
},
// 响应拦截器
onResponse: (context, handler) {
// 请求成功
if (context.statusCode! >= 200 && context.statusCode! < 300) {
handler.next(context);
return;
}
// 请求失败时抛出异常
handler.reject(DioException(requestOptions: context.requestOptions));
},
// 错误拦截器
onError: (error, handler) {
// 抛出异常
handler.reject(
// 发生异常时将服务器返回的错误信息放入
DioException(
requestOptions: error.requestOptions,
message: error.response?.data['msg'] ?? '请求失败',
),
);
},
),
);
}
// 封装 get 方法
Future<Response<dynamic>> get(String url, {Map<String, dynamic>? params}) =>
_dio.get(url, queryParameters: params);
}
// 导出实例
final dioUtils = DioUtils();在组件中使用
dart
// 引入依赖
import 'package:flutterproject/utils/request.dart';
import 'package:dio/dio.dart' show Response;
void initState() {
super.initState();
_getChannels();
}
_getChannels() async {
// 使用
Response<dynamic> res = await dioUtils.get('channels').catchError(() {});
print(res);
}方法2
dart
// lib/utils/request.dart
final Dio dio = getDio();
// 初始化 dio
getDio() {
print('dio初始化');
// 此处省略对 dio 的基本配置与拦截器配置,详情可查看上一步
Dio dio = Dio();
dio.interceptors.add();
return dio;
}
// 封装工具方法
Future<dynamic> request(
String url, {
Map<String, dynamic>? params,
Map<String, dynamic>? data,
Options? options,
}) async {
return Future(() async {
// 捕获错误
try {
var res = await dio.request(
url,
// 作为请求体参数
data: data,
// 作为查询字符串参数,即参数会出现在接口地址之上
queryParameters: params,
// 可不用传递 method dio 默认是 get 请求
// options: Options(method: method)
options:options
);
// 解构数据(dio 会默认在数据外包一层 data)
return res.data;
} catch (e) {
// 直接写明错误,方便后续调用时不会报错
return {'code': '-1', 'msg': (e as DioException).message};
}
});
}dart
// lib/api/index.dart
// 导出接口函数
Future<dynamic> getBanner() => request('home/banner');dart
// 引入工具函数以及接口路径
import 'package:flutterproject/api/index.dart' show getBanner;
// 页面中调用
getinfo() async {
final res = await getBanner();
if (res['code'] != '1') return;
setState(() {
_bannerList = res['result'];
});
}浏览器端跨域
默认情况下,flutter 运行 web 端加载网络资源会报跨域提示错误。
1.在flutter/packages/flutter_tools/lib/src/web/chrome.dart 如下位置添加 '--disable-web-security',大概在 223 行的位置
dart
// chrome.dart
final args = <String>[
chromeExecutable,
// 添加此配置即可解决跨域问题
'--disable-web-security',
]2.删除 flutter/bin/cache/ 下 flutter_tools.snaphot 和 flutter_tools.stamp 文件并重新启动项目即可
路由管理
路由管理是构建多页面应用的核心,它通过 Navigator 和 Route 来管理页面栈,实现页面跳转和返回
通过 Navigator.push 入栈(跳转页面)、Navigator.pop 出栈(回退页面)
基本路由
基本路由适合页面不多、跳转逻辑简单的场景
无需提前注册路由,跳转时创建 MaterialPageRoute 实例即可
MaterialApp 是路由系统的组件,只能有一个 MaterialApp 包裹
dart
void main() {
// 使用 MaterialApp 渲染 A 页面
runApp(MaterialApp(home: ListPage()));
}
// A 页面
class ListPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: TextButton(
onPressed: () {
// 跳转页面
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetalsPage()),
);
},
child: Text('列表页'),
),
);
}
}
// B 页面
class DetalsPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: TextButton(
onPressed: () {
// 回退页面
Navigator.pop(context);
},
child: Text('详情页'),
),
);
}
}路由传参
基本路由传参和组件通信中通过构造函数方式父传子类似
dart
Navigator.push(
context,
// 传递路由参数
MaterialPageRoute(builder: (context) => DetalsPage(id:'1')),
);
// 接收参数
class DetalsPage extends StatelessWidget {
// 接收参数
final String id;
Widget build(BuildContext context) {
return Scaffold(
body: TextButton(
onPressed: () {
// 接收参数
print(id);
},
child: Text('详情页'),
),
);
}
}命名路由
应用页面增多后,使用命名路提升代码可维护性。
需要先在 MaterialApp 中注册一个路由表(routes)并设置 initialRoute(首页)
dart
void main() {
runApp(
MaterialApp(
// 默认展示的路由页面
initialRoute: '/detals',
// 注册路由表
routes: {
'/list': (context) => ListPage(),
'/detals': (context) => DetalsPage(),
}
),
);
}
class ListPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: TextButton(
onPressed: () {
// 跳转页面
Navigator.pushNamed(context, '/detals');
},
child: Text('列表页'),
),
);
}
}
class DetalsPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: TextButton(
onPressed: () {
// 跳转页面
Navigator.pushNamed(context, '/list');
},
child: Text('详情页'),
),
);
}
}路由传参
通过路由传递参数是实现页面间数据通信的常用方式
dart
// 跳转页面并添加路由参数
Navigator.pushNamed(context, '/detals', arguments: {'name': '张三'});
// 接收路由参数
void initState() {
// 在 initState 无法直接获取到路由参数,需要借助微任务"等待一下",就能接收到
Future.microtask(() {
// 创建一个微任务用来接收路由参数
var args = ModalRoute.of(context)?.settings.arguments;
if (args != null) {
print((args as Map<String, dynamic>)['name']);
}
});
}动态路由
更复杂的场景,如需根据参数动态生成页面,或实现路由拦截,可以使用 onGenerateRoute 和 onUnknownRoute
dart
void main() {
runApp(
MaterialApp(
initialRoute: '/detals',
// 注册路由表
routes: {
'/detals': (context) => DetalsPage(),
},
onGenerateRoute: (settings) {
// 如果路由表里没有找到路由,就会调用这个方法
// 如果返回值是 null,则表示没有匹配到路由,会报错
// 如果返回值是其他值,则表示匹配到了路由,会显示对应页面
// settings.name 可以获取当前要跳转的路径
if (settings.name == '/my') {
// 根据路由渲染对应组件
return MaterialPageRoute(builder: (context) => My());
}
return null;
},
// 如果跳转一个未在路由表中注册、也未在 onGenerateRoute 中处理的路由,会调用此回调。通常显示"404"页面
onUnknownRoute: (settings){
return MaterialPageRoute(builder: (context) => NotFound());
}
),
);
}
class My extends StatelessWidget {
Widget build(BuildContext context) {
return Text('我的');
}
}
class DetalsPage extends StatelessWidget {
Widget build(BuildContext context) {
return TextButton(
onPressed: () {
// 如果跳转的页面不在路由表里,则调用 onGenerateRoute
Navigator.pushNamed(context, '/my');
},
child: Text('详情页'),
);
}
}实用插件
轮播图插件
bash
# 安装
flutter pub add carousel_slider使用
dart
CarouselSlider(
// 轮播图图片项
items: List.generate(
5,
(int index) => Image.network(
'图片地址',
fit: BoxFit.cover,
width: double.infinity,
),
),
options: CarouselOptions(
// 图片高度
height: 300,
// 图片占容器的宽度比,即:图片宽度/容器宽度,这里设置为 1,即图片宽度等于容器宽度
viewportFraction: 1,
// 是否自动轮播
autoPlay: true,
// 自动轮播间隔时间
autoPlayInterval: Duration(seconds: 3),
// 当轮播图切换时出发
onPageChanged:(index, reason) {},
),
)持久化插件
bash
# 安装插件
flutter pub add shared_preferences1.写入 Storagese 类
dart
// storagese.dart
class Storagese {
// 获取本地存储实例
final _prefs = SharedPreferences.getInstance();
setInfo(String token) async {
final storagese = await _prefs;
// 写入数据
storagese.setString('token', token);
// storagese.getString('token'); 读取数据
// 写入复杂数据需要先调用 jsonEncode 将数据序列化才可存储
storagese.setString('name', jsonEncode({'name': '张三'}));
// 读取时需要反序列化
jsonDecode(storagese.getString('name'))['name']
_token = token;
}
}数据共享插件
GetX 插件是一个轻量级、高性能的全功能框架,提供路由管理、状态管理、依赖注入等核心功能
bash
# 安装插件
flutter pub add get简单使用
1.先创建数据共享类
dart
// lib/stores/index.dart
import 'package:get/get.dart';
// 这里放入需要共享的对象
class UserController extends GetxController {
// 如果需要响应式数据则需要在数据末尾加上 .obs
// 页面中使用时则需要使用 user.value 来读取值
var user1 = ''.obs;
var user = {'name': ''}.obs;
// 定义更新数据的函数
updataUser(info) {
user.value = info;
}
}2.在需要使用数据的地方调用 put 方法
dart
import 'package:get/get.dart';
import 'package:flutterproject/stores/index.dart';
// 在需要共享数据的地方先调用 Get.put 方法,表示将数据填入
final UserController _userController = Get.put(UserController());
Container(
// 在需要响应式数据的地方用 Obx 方法包裹
child: Obx(() {
return Text(
// 读取数据
// _userController.user1.value 读取简单数据
_userController.user.value['name']
);
}),
)3.在需要写入数据的地方调用 Get.find 方法
dart
// 在写入数据的地方调用 Get.find 方法
final UserController _userController = Get.find();
login() {
// 写入数据
_userController.updataUser('张三');
}其他技巧
获取屏幕宽度
dart
Widget build(BuildContext context) {
// 此方法需要在 build 函数中使用
final double screenWidth = MediaQuery.of(context).size.width;// 屏幕宽度
}项目中使用
自定义帧动画
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.3dart
// 在 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
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);
}
}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
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();
}内置图标库
定时器
dart
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_time <= 0) {
// 清除定时器
_timer.cancel();
return;
}
});