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',
// 来设置整个应用的主题 ThemeData.dark() 设置暗黑主题
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(
// 当前选中的页面图标索引
currentIndex: _currentIndex,
onTap:(index){
setState(() {
// 修改展示的页面
_currentIndex = value;
});
},
// 栏目列表
items: [
BottomNavigationBarItem(label: '首页'),
BottomNavigationBarItem(label: '分类'),
BottomNavigationBarItem(label: '购物车'),
BottomNavigationBarItem(label: '我的')
],
)
)
);PageView 组件
同样可用于构建首页选项卡切换场景。使用和 IndexedStack 组件类似
仅渲染当前页面,滑动时按需加载,可配合 AutomaticKeepAliveClientMixin 缓存页面状态,缓存的页面不会销毁,再次切换时不会重新构建。
dart
// 初始化实例并设置初始展示的页面索引,不设置则默认展示页面列表中的第一个页面
final PageController _controller = PageController(initialPage: 1)
int _currentIndex = 0;
Scaffold(
body: SafeArea(
child: PageView(
controller: _controller,
// 禁用滚动
physics: const NeverScrollableScrollPhysics(),
// 放入页面
children: [Text('首页'), Text('分类'), Text('购物车'), Text('我的')],
),
bottomNavigationBar: BottomNavigationBar(
// 当前选中的页面图标索引
currentIndex: _currentIndex,
onTap:(index){
setState(() {
// 修改展示的页面
_currentIndex = value;
// 控制 PageView 跳转到指定页面
_controller.jumpToPage(i);
});
},
// 栏目列表
items: [
BottomNavigationBarItem(label: '首页'),
BottomNavigationBarItem(label: '分类'),
BottomNavigationBarItem(label: '购物车'),
BottomNavigationBarItem(label: '我的')
],
)
)
);缓存页面
dart
// 需要缓存的页面
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
// 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 组件'))比例大小组件
用于按比例调整子组件尺寸的布局组件。
可将子组件的大小设置为父容器可用空间的特定比例(如宽度 80%、高度 50% 等)
dart
// FractionallySizedBox 的父组件必须具有明确的尺寸约束,否则无法计算比例
FractionallySizedBox(
// 相当于父组件容器一样的宽度
widthFactor: 1,
// 相当于父组件容器 50% 的高度
heightFactor: 0.5,
// 设置子组件的对齐方式
alignment: Alignment.center,
child: Container(),
)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
// 第二个参数表示需要推入的页面
// 第三个参数表示如何删除路由栈里的路由(true:不再清空路由,false: 清空所有路由)
// 顺序是先删除路由再推入第二个参数的页面
// ModalRoute.withName('/') 表示除首页保留其他全清除
Navigator.pushAndRemoveUntil(context,
CupertinoPageRoute(builder: (context) {
return const AccountLoginPage();
}), ModalRoute.withName('/'));滚动自适应组件
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 文件并重新启动项目即可
路由管理
Flutter提供了两种路由管理模式:命令式路由(Navigator 1.0)和声明式路由(Navigator 2.0),适用于不同场景。
1.0 适合简单应用,如无复杂导航逻辑的项目
2.0 适合复杂应用,如电商APP(多标签页+商品详情+购物车嵌套)、社交APP(主页面+聊天窗口+用户资料嵌套),Navigator 2.0 需 Flutter 2.0+
核心差异对比
| 维度 | Navigator 1.0 | Navigator 2.0 |
|---|---|---|
| 编程范式 | 命令式(通过push/pop等显式调用) | 声明式(通过路由配置与状态同步驱动) |
| 路由管理方式 | 隐式路由栈(由Flutter内部维护) | 显式路由栈(开发者可完全控制) |
| 状态同步 | 依赖BuildContext,状态与路由解耦较弱 | 路由状态与应用状态深度绑定(如通过Riverpod) |
| 深度链接 | 需手动处理(如onGenerateInitialRoutes) | 原生支持(通过RouteInformationParser自动解析) |
| 嵌套路由 | 需自定义实现(如NestedNavigator) | 内置支持(通过Router嵌套配置) |
| 学习成本 | 低(适合新手快速上手) | 高(需理解RouteInformation、RouterDelegate等概念) |
| 性能开销 | 较小(直接操作路由栈) | 较大(需频繁解析路由状态并同步UI) |
Navigator 1.0
路由管理是构建多页面应用的核心,它通过 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:支持Material Design的转场动画(如Android的左右滑动)
// CupertinoPageRoute:支持iOS风格的转场动画(如上下滑动)
MaterialPageRoute(
builder: (context) => DetalsPage(),
// 保持页面状态(避免重复重建)
maintainState: true
),
);
},
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('详情页'),
);
}
}Navigator 2.0
Navigator 2.0 是Flutter中基于声明式路由管理的框架,旨在解决传统 Navigator 1.0 在复杂路由场景下的局限性(如嵌套路由、深度链接、Web支持等)
Navigator 2.0 基于三个核心组件:Router、RouteInformationParser 和 RouterDelegate。
1.修改 MaterialApp 配置
dart
// lib/routes/index.dart
// MaterialApp.router 是使用 Navigator 2.0 的入口
MaterialApp.router(
// routerDelegate 负责根据路由配置构建页面
routerDelegate: AppRouterDelegate(),
// routeInformationParser 负责解析 URL 为路由配置
routeInformationParser: BiliRouteInformationParser()
)2.创建页面路由常量以及路由配置
dart
// 定义路由路径
class BiliRoutePath {
final String path;
// 路由参数
final Map<String, dynamic>? ueryParameter;
BiliRoutePath(this.path, {this.ueryParameter});
// 用于路由解析,并将路由参数传递给对应路由
static BiliRoutePath fromPath(Uri path) => BiliRoutePath(
// 处理已知路由,配配不上则跳转到 404 路由
[RoutePaths.home, RoutePaths.login].contains(path.path)
? path.path
: '/404',
ueryParameter: path.queryParameters,
);
// 获取路由路径
String toPath() => Uri(path: path, queryParameters: ueryParameter).toString();
@override
int get hashCode => path.hashCode ^ ueryParameter.hashCode;
// 用于比较,确保 Page key 的唯一性
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BiliRoutePath &&
runtimeType == other.runtimeType &&
path == other.path &&
ueryParameter == other.ueryParameter;
}
// 路由常量
class RoutePaths {
static const String home = '/';
static const String login = '/login';
// static const String notFound = '/404';
// 私有化构造函数,防止实例化
const RoutePaths._();
}
// 路由表
final Map<String, Widget> _routes = {'/': MainPage(), '/login': LoginPage()};2.创建路由代理
dart
// lib/routes/index.dart
// 创建路由代理
class AppRouterDelegate extends RouterDelegate<BiliRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BiliRoutePath> {
BiliRoutePath? _currentPage;
// 创建路由堆栈
final List<Page<dynamic>> _stack = [];
// 为 Navigator 设置一个 key
// 必要的时候可以通过 navigatorKey.currentState 来获取到 NavigatorState 对象
@override
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
AppRouterDelegate() {
// 初始化路由堆栈
_stack.add(pageWrap(BiliRoutePath(RoutePaths.home)));
}
// 处理路由变化
void _setNewRoute(BiliRoutePath page) {
_currentPage = page;
// 通知 Navigator 重建
notifyListeners();
}
// 创建页面函数
pageWrap(BiliRoutePath page) => MaterialPage(
key: ValueKey('${page.path}${page.ueryParameter ?? ''}'),
child: _routes[page.path] ?? Text('404'),
);
// 自定义跳转方法
navigateTo(BiliRoutePath page) {
_stack.add(pageWrap(page));
notifyListeners();
}
@override
Widget build(BuildContext context) {
// 处理路由堆栈信息
return Navigator(
key: navigatorKey,
pages: _stack,
// 移除页面后触发
onDidRemovePage: (page) {
// 当前页面是否是 MaterialPage 类型,避免对其他类型页面(如iOS风格 CupertinoPage 页面)误操作
if (page is MaterialPage) {
// 提取该路由下实际承载的业务组件
final screen = page.child;
// 执行页面移除后的操作(如状态清理),可对不同页面做不同处理
// if (screen is MyHomePageState) {
// screen.controller.dispose(); // 释放动画控制器
// screen.streamSubscription.cancel(); // 取消流订阅
// }
}
// 注意:此处无法直接获取 pop 操作的结果
},
);
}
@override
// 当前路由配置发生变化时触发
Future<void> setNewRoutePath(BiliRoutePath configuration) {
_setNewRoute(configuration);
return Future.value();
}
}3.创建路由解析器,可选
dart
// 路由解析器,可缺省,
// 主要应用于 web,可以将其解析为我们定义的数据类型
class BiliRouteInformationParser extends RouteInformationParser<BiliRoutePath> {
@override
Future<BiliRoutePath> parseRouteInformation(
RouteInformation routeInformation,
) async {
// 路由映射,将 URL 解析为路由配置
// routeInformation.uri 可获取 URL 路径
return BiliRoutePath.fromPath(routeInformation.uri);
}
@override
RouteInformation? restoreRouteInformation(BiliRoutePath configuration) {
// 将路由配置转换为 URL
return RouteInformation(uri: Uri.parse(configuration.toPath()));
// 也可以针对某一个路由进行特殊处理,如传递页面参数
// if (configuration.path == RoutePaths.home && configuration.ueryParameter != null) {
// return RouteInformation();
// }
// return RouteInformation();
}
}4.页面中使用
dart
Widget build(BuildContext context) {
final router = Router.of(context).routerDelegate as AppRouterDelegate;
return TextButton(
onPressed: () {
// 调用自定义方法跳转
router.navigateTo(
BiliRoutePath(RoutePaths.login, ueryParameter: {'id': '123'}),
);
},
child: Text('去登陆'),
)
);
}GoRouter
官方推荐方案,文档完善、社区活跃,且持续更新以适配最新 Flutter 版本,适合大多数项目。
基于 Navigator 2.0 构建,提供声明式路由 API,支持 Web URL 路由、嵌套路由、参数传递、路由守卫(拦截器)、404 页面处理等
基本使用
安装
bash
flutter pub add go_router创建路由配置
dart
// lib/routes/index.dart
// 路由配置的入口
final GoRouter _router = GoRouter(
// 当用户访问一个不存在的路径时,会显示这个页面
errorBuilder: (context, state) => Text('404'),
// 定义应用的每个页面
routes: [
GoRoute(
path: RoutePaths.home,
builder: (context, state) => MainPage(),
),
GoRoute(
path: RoutePaths.login,
builder: (context, state) => LoginPage(),
),
],
);
// 路由常量
class RoutePaths {
static const String home = '/';
static const String login = '/login';
// static const String notFound = '/404';
// 私有化构造函数,防止实例化
const RoutePaths._();
}集成到 MaterialApp
dart
void main() {
runApp(
MaterialApp.router(
// 集成到 MaterialApp
routerConfig: _router,
)
);
}跳转
push:将新路由推入栈顶,保留当前路由历史。例如从
/homepush到/detail,栈变为[home, detail];若再push到/modal(非/detail子路由),栈变为[home, detail, modal]。默认不更新浏览器地址栏go:根据路由层级智能调整栈。若目标路由是当前路由的子路由(如
/home→/home/details),行为类似push;若非子路由(如/detail→/modal),则替换栈顶为modal,栈变为[home, modal]。默认更新地址栏,符合Web端用户预期。pop:移除栈顶路由,返回上一页。自动同步地址栏变化。
dart
Widget build(BuildContext context) {
return TextButton(onPressed: (){
// 跳转
context.go(RoutePaths.login);
}, child: Text('跳转')
}go_router 在 Web 环境下默认 push 方法不会更新地址栏,而 go 方法会更新。
即在浏览器环境下在路径 '/my' 使用 context.push('/login') 跳转时,页面正常跳转但地址栏上仍显示 'my'。
dart
// 可使用如下方法解决此问题
// 方案一:使用 go 方法跳转
context.go('/login')
// 方案二:在 main.dart 的 runApp() 前添加配置
void main() {
GoRouter.optionURLReflectsImperativeAPIs = true; // 关键配置
runApp(MyApp());
}方法对比
| 维度 | push | go | pop |
|---|---|---|---|
| 路由栈管理 | 严格入栈,保留完整历史 | 智能替换/入栈(基于路由层级) | 移除栈顶,返回上一页 |
| URL更新 | 默认不更新(需配置) | 默认更新 | 自动同步 |
| 适用场景 | 表单流程、模态框、多步骤操作 | 主路由切换、深度链接、Tab导航 | 返回操作、页面关闭 |
| 性能影响 | 可能增加栈深度,需注意内存管理 | 优化栈结构,减少冗余历史 | 无额外开销 |
路由传参
路径参数
适用于传递简单的
ID或标识符
dart
// lib/routes/index.dart
// 修改路由配置
GoRoute(
// 定义参数
path: '${RoutePaths.login}/:id',
builder: (context, state) {
// 通过 state.pathParameters 获取路径参数
final String id = state.pathParameters['id'] ?? '0';
return LoginPage(id: id);
},
)dart
// 跳转时传递参数
context.push('${RoutePaths.login}/123');dart
// 页面中使用(有状态组件)
class LoginPage extends StatefulWidget {
final String id;
// 接收参数
const LoginPage({super.key, required this.id});
}
Widget build(BuildContext context) {
// 获取参数
return Text('登陆${widget.id}')
}查询参数
适用于传递过滤、搜索等非必需参数。
dart
// 路由配置不需要改变。跳转时,像写 URL 一样附加参数
context.push('${RoutePaths.login}/123?source=homepage&sort=asc');dart
// 修改路由配置
builder: (context, state) {
final String id = state.pathParameters['id'] ?? '0';
// 获取查询参数
final String? source = state.uri.queryParameters['source'];
// 需要在组件中声明接收获取
return LoginPage(id: id, source: source);
}extra 参数
适用于传递一个复杂的自定义数据
dart
class Product {
final String name;
final String price;
Product(this.name, this.price);
}
// 修改路由配置
builder: (context, state) {
// state.extra 是 Object? 类型,需要进行类型转换
// 也可以直接传递一个普通的 Map
final Product product = state.extra as Product;
// 需要在组件中声明接收获取
return LoginPage(product: product);
}dart
// 跳转时添加 extra 参数
context.push(RoutePaths.login,extra: Product('name参数', 'price参数'));dart
// 组件中直接获取参数
print('${widget.product.name}-${widget.product.price}');嵌套路由
修改路由配置
dart
final GoRouter _router = GoRouter(
// 初始页面
initialLocation: '/home',
errorBuilder: (context, state) => Text('404'),
routes: [
// 定义联套路由
StatefulShellRoute.indexedStack(
// 返回 Shell UI (例如,一个带 BottomNavigationBar 的 Scaffold)
// 后续路由组件会在此壳组件中展示
builder: (context, state, navigationShell) =>
// 将 navigationShell 传递给壳组件
MainPage(navigationShell: navigationShell),
branches: [
StatefulShellBranch(
routes: [
GoRoute(path: '/home', builder: (context, state) => HomeView()),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/category',
builder: (context, state) => CategoryView(),
routes: [
// category 页面下的子页面
GoRoute(
// 路径会拼接为 /category/details/:id
path: 'details/:id',
builder: (context, state) =>
DetailsPage(id: state.pathParameters['id']!),
),
],
),
],
),
// 其他路由
],
),
// 定义不在壳组件中展示的路由
GoRoute(
path: '/login',
// 可通过 pageBuilder 渲染 MaterialPage 类型或者 ios 风格 的 CupertinoPage 的页面
pageBuilder: (context, state) =>
MaterialPage(key: state.pageKey, child: LoginPage()),
),
],
);创建壳组件
dart
class MainPage extends StatelessWidget {
// 接收 navigationShell
final StatefulNavigationShell navigationShell;
const MainPage({super.key, required this.navigationShell});
Widget build(BuildContext context) {
return Scaffold(
// 显示当前选中的栏目路由对应的页面
// 路由组件会被展示到这
body: navigationShell,
bottomNavigationBar: BottomNavigationBar(
// 同步当前选中索引
currentIndex: navigationShell.currentIndex,
// 通知 go_router 切换分支
onTap: (index) => navigationShell.goBranch(index),
// 栏目列表
items: [
BottomNavigationBarItem(icon: Icon(Icons.home_filled),label: '首页')
// 其他路由栏目...
],
),
);
}
}至此只要切换底部栏目,路由也会同步变化,展示对应的页面组件。
一开始壳组件中只会渲染当前路由的页面(其他栏目不会渲染,即按需渲染),之后当切换路由时,每个路由组件(StatefulShellRoute.indexedStack 中的路由)会保留其状态,不会重建(即不会再执行 build 方法)。
但如果跳转到子路由则会触发重建。即当前路由为 '/category' 若跳转到其子路由 '/category/details' 父子路由都会重建,返回 '/category' 时会使该路由对应的组件重建。
重定向
可利用
redirect实现路由守卫
dart
// 修改路由配置,添加 redirect
final GoRouter _router = GoRouter(
routes: [
GoRoute(
path: '/login',
builder: (context, state) => LoginPage(),
// 局部重定向
redirect: (context, state) => isLoggedIn() ? '/' : null,
)
],
// 全局重定向
redirect: (context, state) {
// 模拟一个登录状态
final bool loggedIn = false;
// 检查用户是否在访问登录页
// state.matchedLocation 可获取当前要去的路由
final bool isLoggingIn = state.matchedLocation == '/login';
// 如果用户未登录且不在登录页,重定向到登录页
if (!loggedIn && !isLoggingIn) {
return '/login';
}
// 如果用户已登录且在访问登录页,重定向到首页
if (loggedIn && isLoggingIn) {
return '/';
}
// 其他情况,不重定向
return null;
},
);路由监听
页面中可通过 RouteInformationProvider 来监听路由变化
dart
class _CartViewState extends State<CartView> {
// 定义监听器对象
late final RouteInformationProvider _routeInformationProvider;
@override
void initState() {
super.initState();
// 给监听器对象赋值
_routeInformationProvider = GoRouter.of(context).routeInformationProvider;
// 添加监听 每当页面路由发生变化时都会执行 _handleRouteChange 方法
_routeInformationProvider.addListener(_handleRouteChange);
}
@override
void dispose() {
// 移除监听
_routeInformationProvider.removeListener(_handleRouteChange);
super.dispose();
}
void _handleRouteChange() {
// value.uri 可获取当前页面路由
if (_routeInformationProvider.value.uri.toString() == '/cart') {
print(_routeInformationProvider.value.uri);
}
}
@override
Widget build(BuildContext context) {
return 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
// 使用 runZonedGuarded 包裹 runApp() 即可捕获整个应用没有捕获的异常
void main() {
runZonedGuarded((){
runApp();
}, (error, stackTrace)=>{
print('error: $error');
print('stackTrace: $stackTrace');
});
}打包相关
设置app启动图
可借助 flutter_native_splash 插件完成
bash
# 安装依赖 flutter_native_splash 是开发依赖
flutter pub add --dev flutter_native_splash打开 pubspec.yaml
yaml
# 在与 flutter 同级写入如下内容,注意缩进
flutter_native_splash:
color: "#ffffff" # 启动页背景色
image: "lib/assets/launch_image.png" # 启动图路径
android: true # 启用Android平台
ios: true # 启用iOS平台
fullscreen: true # 全屏展示(推荐设为true)
# 可选深色模式配置(如需要)
# color_dark: "#000000"
# image_dark: assets/launch_dark.png执行如下命令,重启项目即可
bash
flutter pub run flutter_native_splash:create清理构建缓存
若未生效可按如下步骤
bash
# 彻底删除 build/、.dart_tool/、pubspec.lock 及各平台原生构建目录
flutter clean
# 重新安装依赖
flutter pub get修改app名称
修改安卓端打开 android/app/src/main/AndroidManifest.xml
xml
<application android:label="你的新应用名称" ...>修改 IOS 端打开 ios/Runner/Info.plist 设置 CFBundleDisplayName(用户主屏显示名)和CFBundleName(内部标识)
xml
<key>CFBundleDisplayName</key>
<string>新应用名称</string>
<key>CFBundleName</key>
<string>NewAppName</string>设置应用在手机多任务界面展示的名称
dart
void main() {
runApp(MaterialApp(
// 设置应用在手机多任务界面展示的名称
title: '名称',
));
}修改app图标
可借助插件来完成
bash
# 安装依赖 flutter_launcher_icons 是开发依赖
flutter pub add --dev flutter_launcher_iconsyaml
# 在与 flutter 同级写入如下内容,注意缩进
flutter_launcher_icons:
android: true
ios: true
image_path: "lib/assets/icon.jpg"
adaptive_icon_background: "#FFFFFF" # Android适应性图标背景色
# adaptive_icon_foreground: "assets/3.png" # Android适应性图标
remove_alpha_ios: true # 移除iOS图标透明通道(避免显示异常)
# min_sdk_android: 21 # 最低Android SDK版本
# web: true 适配其他端
# windows:
# generate: true
# image_path: "path/to/image.png"
# icon_size: 48 # min:48, max:256, default: 48
# macos:
# generate: true
# image_path: "path/to/image.png"bash
# 运行命令自动配置图标
dart run flutter_launcher_icons设置appID
打开 android/app/build.gradle.kts 找到 defaultConfig 下的 applicationId 即可设置,也可以使用默认的(应用上架时最好还是设置一下)。
kts
defaultConfig {
applicationId = "com.example.flutterproject"
}签名APP
keytool 是一个用于管理密钥和证书的命令行工具,通常与Java开发工具包(JDK)一起使用。
key.jks 是生成的 Keystore 文件名。
RSA 是签名算法。
2048 是密钥长度。
10000 是证书的有效期(单位:天)
key 是密钥别名。
bash
# 执行如下命令 Windows 环境下
# 按照提示一步一步填写信息即可,最终
keytool -genkey -v -keystore D:\project\my\flutterproject\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key
# 为了避免出现问题使用 -dname 直接填写信息,避免交互提示
keytool -genkey -v -keystore D:\project\my\flutterproject\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key -dname "CN=Your Name, OU=Your Unit, O=Your Org, L=Your City, ST=Your State, C=CN"命令执行完毕后会在指定地址生成一个 key.jks 注意此文件非常重要
后所有版本更新必须使用同一个keystore签名,失后将无法给已安装用户推送升级
将生成好的 key.jks 文件放入到项目中 android/app 目录下,并在 android 目录下新建一个 key.properties 的文件(如果有则不需要创建,此文件存在敏感性信息,建议添加至 .gitignore 中不要上传至仓库中)
properties
# 上一步命令行设置的密钥库密码
storePassword=123456
# 上一步命令行设置的密钥密码
keyPassword=123456
# 上一步命令行设置的密钥别名
keyAlias=key
# 上一步命令行设置的密钥库文件路径,此路径
storeFile=key.jks打开 android/app/build.gradle.kts(或者 android/app/build.gradle)找到 android 块写入如下内容。(注意复制如下内容时删除所有注释)修改后运行一下 flutter clean 防止缓存的版本影响签名过程。
properties
# 在文件顶部添加这段代码
import java.util.Properties
import java.io.FileInputStream
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
# 添加 signingConfigs 块写入如下代码
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String
}
}
buildTypes {
# 在 buildTypes 块写入如下代码
getByName("release") {
signingConfig = signingConfigs.getByName("release")
# 启用代码压缩与混淆。删除未使用的类、方法、字段,优化字节码,提升运行效率
# 将类/方法/字段名重命名为无意义的短名称(如a, b),增加反编译难度,保护知识产权
isMinifyEnabled = true
# 启用资源压缩。移除未被代码引用的资源文件,合并重复资源
isShrinkResources = true
}
# 在 buildTypes 块写入如下代码
getByName("debug") {
isMinifyEnabled = false
isShrinkResources = false
}
}
}在打包 release 版本的 APK 时代码混淆和压缩会默认开启。如果不想启用代码混淆和压缩可运行如下命令
bash
flutter build apk --release --no-shrink打包
分为三种模式
flutter build apk 生成的 APK 默认包含所有支持的 CPU 架构,包体积较大,适合通用发布(兼容所有设备)
bash
# 生成调试版 APK (未签名,用于测试)
# 未优化代码,支持热重载和热重启,代码修改可秒级生效,适合开发迭代
flutter build apk --debug
# 代码部分优化,性能在 debug 于 release 之间。接近 Release 模式的编译方式,但保留部分调试功能用于性能
# 分析,体积略大于 Release。安卓可不配置签名,IOS 必须配置
flutter build apk --profile
# 生成发布版 APK (需要签名配置)
# 代码编译为机器码,启动速度和运行效率最优,应用包最小,无法连接调试器,适合生产环境
flutter build apk --release
# 生成调试版 IOS 应用,快速构建。未签名,用于测试
flutter build ios --debug
# 生成发布版 IOS 应用,优化体积与性能。需要签名配置
flutter build ios --release安卓端构建不同架构。打包单一架构的 APK ,适用于优化下载体积的情况
bash
# 生成按 ABI 拆分的 APK(默认包含 armeabi-v7a/arm64-v8a/x86_64 )
flutter build apk --split-per-abi --release
# 指定目标架构打包(单架构)现代64位设备(占比>90%)
flutter build apk --target-platform=android-arm64 --release
# 32位 ARM 32位设备 (如中国低端市场)
flutter build apk --target-platform=android-arm --release
# x86_64 模拟器/Chromebook
flutter build apk --target-platform=android-x64 --release项目中使用
自定义帧动画
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()]);
}
}打印
瀑布流插件
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;
}图片插件
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 _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;
}
});