基础组件
Text
创建文字,并设置其样式。示例如下
Text('this is Text Widget',
style: TextStyle(
fontSize: 30,
fontFamily: 'Futura',
color: Colors.blue,
)
)
Icon
创建图形符号,Flutter 将会为 Material 和 Cupertino 的应用提前加载 icon。示例如下
Icon(
Icons.widgets,
size: 50,
color: Colurs.blue,
)
Image
加载图片
web 端会出现跨域问题
Image():通用方法,使用ImageProvider实现,下面方法相当于别名,本质上也是使用这个方法Image.asset():加载资源图片,需配置资源路径,见下文Image.file():加载本地图片文件Image.network():加载网络图片Image.memory():加载Uint8List资源图片
配置资源路径
- 在工程根目录下创建一个
images目录,并将图片avatar.png拷贝到该目录。也可以设置其他自定义目录,如lib下面的images,方便管理。 - 在
pubspec.yaml中的flutter部分添加如下内容(yaml 文件对缩进严格,必须按照每一层两个空格缩进的方式进行缩进):assets: # 指定全目录 - assets/images/ # 指定文件 - assets/images/avatar.png
各种方法使用示例
Image(
image: AssetImage("assets/images/avatar.png"),
width: 100.0
);
Image.asset("assets/images/avatar.png",
width: 100.0,
)
Image(
image: NetworkImage(
"https://avatars2.githubusercontent.com/u/20411648"),
width: 100.0,
)
Image.network(
"https://avatars2.githubusercontent.com/u/20411648?s=460&v=4",
width: 100.0,
)
注意点
Image的width、height参数,当不指定宽高时,图片会根据当前父容器的限制,尽可能的显示其原始大小。 如果只设置width、height的其中一个,那么另一个属性默认会按比例缩放,可以通过fit属性来指定适应规则,具体介绍参考文档Image缓存,Image加载过得图片只在内存中会有缓存,无法缓存本地。一旦应用关闭,这个缓存就没有了,下次重新启动还会从网络下载并缓存- 不能直接将
assets目录设置为资源目录,要在里面新建文件夹
Card
Card 的通常用于是包裹其他元素,其只接收一个子元素,但可以为 Row 或 Column
ListTile
ListTile 是一个内置的列表样式组件,其包含 3 行文本(其实只有两行,第三行是 subtitle 的换行),以及可选的前后图标
若其效果无法满足,也可通过 Row 来手动实现
注意点
ListTile 自带点击水波纹效果,如果将其放在 Container 内部,且 Container 设置了背景色时,水波纹效果将会不显示。如果想设置背景色但是又不想遮挡水波纹效果,可将其放在 Ink 组件下,通过 Ink 组件设置背景色
Container(
// 此组件不设置背景色
child: Ink(
decoration: BoxDecoration(
color: Colors.pink[50],
borderRadius: BorderRadius.circular(10.0),
),
child: ListTile()
)
)
Wrap
Row 是不会自动换行的,要实现自动换行,可使用 Wrap
Row 和 Column
Row 和 Column 是基于 web 的 flexbox 布局模型设计的
mainAxisSize
mainAxisSize 属性决定了它们两个能在主轴上占据多大的空间,可选值如下
MainAxisSize.max:默认值,占据主轴上所有空间MainAxisSize.min:仅根据它们的children所需要的空间
mainAxisAlignment
当 mainAxisSize: MainAxisSize.max 时,它们会使用额外空间来对齐其 children
mainAxisAlignment 属性决定了它们如何在额外空间中的对齐方式。可选值如下(参考 web 的 justify-content)
MainAxisAlignment.start:默认值,在主轴起点处对齐MainAxisAlignment.end:在主轴终点处对齐MainAxisAlignment.center:主轴中心对齐MainAxisAlignment.spaceBetween:两端对齐,其余平均分配MainAxisAlignment.spaceEvenly:所有空间平均分配MainAxisAlignment.spaceAround:第一个child之前和最后一个child之后的空间为其他空间的一半
crossAxisAlignment
crossAxisAlignment 属性决定了它们如何在其交叉轴上对齐其 children。可选值如下(参考 web 的 align-items)
注意交叉轴默认跟随子元素的尺寸,若全员尺寸一样,则无法看到效果
CrossAxisAlignment.start:交叉轴靠前CrossAxisAlignment.end:交叉轴靠后CrossAxisAlignment.center:默认值,居中CrossAxisAlignment.stretch:在交叉轴上进行拉伸填充CrossAxisAlignment.baseline:根据基线对齐(仅限Text,且要求textBaseline属性设置为TextBaseline.alphabetic)
Flexible
Flexible 包裹一个 Widget,可以让这个 Widget 变得可以调整大小。此时这个 Widget 就成为了 Flexible 的子节点,并被视为 flexible。可通过调整其 flex 和 fit 属性来调整大小
flex:将自身的flex因子与其他的比较,以决定自身占剩余空间的比例fit:决定Flexible的Widget是否能够填充所有剩余空间。有以下两个可选值FlexFit.loose:默认值,使用自身作为首选大小FlexFit.tight:强制充满所有剩余空间
Expanded
Expanded 与 Flexible 类似,不过它是强制占满剩余空间
SizedBox
SizedBox 用于创建精确的尺寸,其 widget 和 height 属性用于创建大小
- 当其包裹有子元素时,子元素会根据
SizedBox的尺寸变化 - 没有子元素时,则创建一个空的空间
Spacer
Spacer 和 SizedBox 类似,也能创建空间,不过它是基于 flex 属性创建的,且其无法包裹子元素
Container
可以添加 padding、margin、border、background 等属性,只有一个子元素,但这个子元素可以是 Row、Column、根 widget
其子元素将会自动充满所有剩余空间,即使是 Text 也一样
其装饰属性通过 decoration 属性表示,其中的 borderRadius 与 shape: BoxShape.circle 同时使用,因为会冲突
示例如下
Widget A(){
return Container{
decoration: const BoxDecoration(
// 其背景色通过 color 表示
color: Colors.pink,
border: Border.all(width: 10, color: Colors.blue),
borderRadius: const BorderRadius.all(Radius.circular(8))
),
margin: const EdgeInsets.all(4),
padding: const EdgeInsets.all(4),
child: Text('123')
}
}
GridView
用于创建网格,其支持滚动。基础用法示例如下
GridView(
gridDelegate: ***,
children: []
),
其中主要的参数 gridDelegate 有两个可选值
SliverGridDelegateWithFixedCrossAxisCount:用于固定列数的场景,其包含的参数如下crossAxisCount:列数,即一行有几个子元素mainAxisSpacing:主轴方向上的间距crossAxisSpacing:交叉轴方向上的间距childAspectRatio:子元素的宽高比例,当子元素宽高比不为 1 时,需要设置
SliverGridDelegateWithMaxCrossAxisExtent:用于子元素有最大宽度限制的场景,其包含的参数如下maxCrossAxisExtent:子元素最大的宽度mainAxisSpacing:主轴方向上的间距crossAxisSpacing:交叉轴方向上的间距childAspectRatio:子元素的宽高比例,当子元素宽高比不为 1 时,需要设置
GridView.count 和 GridView.extent
分别为上面两个参数的简便写法。示例如下
GridView.count(
crossAxisCount: 5,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
children: []
)
GridView.extent(
maxCrossAxisExtent: 100,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
children: []
)
GridView.builder()
用于生成大量网格的场景,其会对列表进行优化。基本用法示例如下
GridView.builder(
gridDelegate: ***,
itemCount: 10000,
itemBuilder: (BuildContext context, index) {
return Container(
decoration: BoxDecoration(
color: Colors.pink,
border: Border.all(4)
)
)
}
)
ListView
与 Column 类似,但其支持滚动
基本参数如下
scrollDirection:列表滚动方向,可选值为Axis.horizontal(默认值)、Axis.horizontalcontroller:控制器,与列表滚动相关,如监听列表的滚动事件physics:列表滚动到边缘后继续拖动的物理效果。当列表的长度没有填满屏幕时,会无法触发滑动效果,从而导致无法上拉和下拉,此时需要手动将该值设置为以下一项,建议设置为AlwaysScrollableScrollPhysicsClampingScrollPhysics:Android平台默认效果BouncingScrollPhysics:iOS平台默认效果AlwaysScrollableScrollPhysics:跟随各自平台效果NeverScrollableScrollPhysics:禁用拖动效果
shrinkWrap:决定列表的长度是否仅包裹其内容的长度。当ListView嵌在一个无限长的容器中时,必须设置为truepadding:内边距itemExtent:子元素长度。当每一项的长度是固定时,可以指定该值,有助于提高性能cacheExtent:预渲染区域长度。ListView会在其可视区域的两边留一个cacheExtent长度的区域作为预渲染区域(对于ListView.build或ListView.separated构造函数创建的列表,不在可视区域和预渲染区域内的子元素不会被创建或会被销毁)children:子元素
ListView.builder()
用于生成大量列表的场景,会对长列表进行优化。基本用法示例如下
ListView.builder(
itemCount: 10000,
itemBuilder: (BuildContet context, index) {
return Container(
decoration: BoxDecoration(
color: Colors.pink,
border: Border.all()
)
)
}
)
ListView.separated()
带分割线的 ListView
与 ListView.builder() 一样,不过多了一个 separatorBuilder 必填参数,用于定义分割线组件。示例如下
ListView.separated(
itemCount: 10000,
itemBuilder: (BuildContext context, index) {
return Container(
decoration: BoxDecoration(
color: Colors.pink,
border: Border.all(4)
)
)
},
separatorBuilder: (BuildContext context, index) {
return Divider(
height: 0.5,
indent: 75,
color: Colors.blur
)
}
)
下拉刷新
使用内置的 RefreshIndicator 组件。基本用法示例如下
RefreshIndicator(
onRefresh: onRefresh,
child: ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, index) {
return Container(
decoration: BoxDecoration(
color: Colors.pink,
border: Border.all(4)
)
)
}
)
)
上拉加载
没有内置的组件,需要自己完成,需要用到前面提到的 controller 属性。基本代码示例如下
ListView.separated(
controller: scrollController, // 监听事件
itemCount: list.length + 1, // 元素加多 1 个,用于渲染加载提示
separatorBuilder: (BuildContext context, index) {
return Divider(height: .5, color: Colors.pink);
},
itemBuilder: (BuildContext context, index) {
if (index < list.length) {
return NewsCard(data: list[index]);
} else {
// 手动加入加载提示
return renderBottom();
}
},
);
Stack
被 Stack 组件包裹的子元素将会堆叠在一起,排在后面的元素的会在上面,可通过 alignment 属性调整除第一个元素外的其他元素的堆叠位置
Positioned
Stack 通常搭配 Positioned 使用,Positioned 用于设置其子元素的位置
FractionallySizedBox
可以将一个组件的尺寸设置为其父级的百分比,虽然 Center、Align 都有相应的缩放属性,但是它们两个容易受到父级约束的影响,因此尽量使用 FractionallySizedBox
FractionallySizedBox(
widthFactor: 0.5, // 将子级的宽度设置为父级宽度的 50%
heightFactor: 0.5, // 将子级的高度设置为父级高度的 50%
child: Container(
color: Colors.red
)
)
Visibility
该组件可根据某个变量来设置其子组件是否显示
Visibility(
visible: true,
child: Text(
'Your text here',
style: TextStyle(fontSize: 16),
),
)
RefreshIndicator
该组件用于下拉刷新
一个很常见的情况是,下拉的图标已消失,但是数据还未返回,原因是 setState 和 onRefresh 是互斥的,此时需要通过 StreamBuilder 来实现
import 'dart:async';
import 'package:flutter/material.dart';
class DiyBuilder extends StatefulWidget {
final Function() getData;
final Widget Function(BuildContext context, dynamic data, int index) onData;
const DiyBuilder({
super.key,
required this.getData,
required this.onData,
});
State<DiyBuilder> createState() => _DiyBuilderState();
}
class _DiyBuilderState extends State<DiyBuilder> {
var listData = [];
var isLoading = false;
var loadStatus = 'more';
var pageNo = 1;
var totalPages = 1;
var isRefresh = false;
final StreamController<List> _streamController = StreamController<List>();
void _beforeData() {
if (isLoading) {
return;
}
isLoading = true;
}
void _setData(data) {
if (isRefresh) {
listData = data['records'];
} else {
listData = [...listData, ...data['records']];
}
totalPages = data['totalPages'];
if (pageNo >= totalPages) {
loadStatus = 'no';
} else {
loadStatus = 'more';
}
_streamController.sink.add(listData);
isRefresh = false;
isLoading = false;
}
Future<void> _getData() async {
_beforeData();
Map<String, dynamic> data = await widget.getData();
_setData(data);
}
// 必须与 _getData() 分开写,否则下拉刷新的图标会立即消失
Future<void> _onRefresh() async {
_beforeData();
isRefresh = true;
pageNo = 1;
Map<String, dynamic> data = await widget.getData();
_setData(data);
}
void initState() {
super.initState();
_getData();
}
void dispose() async {
// 为避免退出页面时仍发送中的请求,需要使用 Future.wait 等待
// 后续发现,即使不添加也不会导致报错,不知为何原因
// 如确需取消 Future,建议使用 async 包(非 dart 自带的 async),示例见后面章节
// await Future.wait([_getData(), _onRefresh()]);
_streamController.close();
super.dispose();
}
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: _onRefresh,
child: StreamBuilder(
stream: _streamController.stream,
builder: (context, snapshot) {
final List? data = snapshot.data;
// 判断数据是否存在
if (snapshot.hasData && data != null) {
return ListView.builder(
...
);
}
return const Text('加载中...');
},
),
);
}
}
Notification
Notification 用于实现组件之间的通信,不需要通过父子组件传递数据。当一个 Notification 被触发时,Flutter 会自动地沿着当前组件树向上传递通知,直到遇到处理这个通知的组件为止。用法示例如下
// 定义通知
class MyNotification extends Notification {
final String message;
MyNotification(this.message);
}
// 接收通知的组件
class MyWidget extends StatelessWidget {
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
MyNotification('Hello').dispatch(context);
},
child: Container(
color: Colors.white,
child: Center(
child: Text('Tap here'),
),
),
);
}
}
// 处理通知的组件
class MyListenerWidget extends StatelessWidget {
Widget build(BuildContext context) {
return NotificationListener<MyNotification>(
onNotification: (notification) {
// 处理通知
print(notification.message);
return true;
},
child: Container(
color: Colors.white,
child: Center(
child: Text('Notification Listener'),
),
),
);
}
}
// 在父组件中组合 MyWidget 和 MyListenerWidget
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
children: [
MyWidget(),
MyListenerWidget(),
],
),
),
);
}
}
Form
Form 是一个内置的表单组件,通过给它的 key 属性配置一个值,然后可以通过该值调用其中的一些方法,如表单验证
class FormWidget extends StatefulWidget {
const FormWidget({super.key});
State<FormWidget> createState() => _FormWidgetState();
}
class _FormWidgetState extends State<FormWidget> {
final _formKey = GlobalKey<FormState>();
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
TextFormField(),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
DiyToast.showToast('提交成功');
}
},
child: const Text('提交'),
)
),
)
}
}
TextFormField
该组件通常搭配 Form 组件使用,注意它自带的验证信息会影响布局,因此用的时候要根据实际情况考虑是否自定义验证
TextFormField(
focusNode: _focusNode,
controller: _controller,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'placeholder 文本',
hiteStyle: TextStyle(),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入手机号码';
}
return null;
},
errorStyle: TextStyle(), // 可通过将验证失败提示文本的大小设置为 0 来避免布局变换
)
)
BottomNavigationBar
该组件用来实现底部的 tabbar 效果,通过 IndexedStack 来管理页面的堆栈,这样,每次切换选项时,只有当前页面会被重新构建,而其他页面会保持在堆栈中
import 'package:flutter/material.dart';
import './pages/order/main/index.dart';
import './pages/service/main/index.dart';
import './pages/home/main/index.dart';
import './pages/user/main/index.dart';
class TabbarWidget extends StatefulWidget {
const TabbarWidget({super.key});
State<TabbarWidget> createState() => _TabbarWidgetState();
}
class _TabbarWidgetState extends State<TabbarWidget> {
int _selectedIndex = 0;
static const List<Widget> _widgetOptions = <Widget>[
HomePage(),
OrderPage(),
ServicePage(),
UserPage(),
];
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _selectedIndex,
children: _widgetOptions,
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: '首页',
),
BottomNavigationBarItem(
icon: Icon(Icons.list_alt),
label: '订单',
),
BottomNavigationBarItem(
icon: Icon(Icons.face),
label: '服务',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: '我的',
)
],
currentIndex: _selectedIndex,
selectedItemColor: Theme.of(context).primaryColor,
unselectedItemColor: Colors.grey,
onTap: _onItemTapped,
type: BottomNavigationBarType.fixed,
),
);
}
}
SafeArea
用于处理屏幕上的安全区域。安全区域是指屏幕边缘周围的区域,用于避免内容与设备边缘重叠,例如刘海、屏幕凹口、状态栏和底部导航栏等
它会自动适应设备的安全区域,并将其内部的内容放置在安全区域内,以确保内容不会被遮挡或覆盖。它通常用作其他组件的父级组件,以确保这些组件在安全区域内进行布局
WillPopScope
WillPopScope 组件,用于拦截返回按钮,用法示例如下
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
// 返回 false 以阻止页面返回
return false;
},
child: const Scaffold(
body: Center(
child: Text('登录页面'),
),
),
);
}
}
SingleChildScrollView
当软键盘弹起导致页面超出范围时,使用改组件包裹页面其他组件即可
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Keyboard Overflow Example'),
),
body: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
// 页面内容
TextField(
decoration: InputDecoration(
labelText: 'Input 1',
),
),
TextField(
decoration: InputDecoration(
labelText: 'Input 2',
),
),
// 添加更多的页面内容
],
),
),
),
),
);
}
}
TabBar
该组件用于实现常见的 tabs 切换效果。基础示例如下
import 'package:flutter/material.dart';
class TabWidget extends StatefulWidget {
const TabWidget({super.key});
State<TabWidget> createState() => _TabWidgetState();
}
class _TabWidgetState extends State<TabWidget> with TickerProviderStateMixin {
late TabController _tabController;
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
}
void dispose() {
_tabController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Column(
children: [
TabBar(
controller: _tabController,
tabs: [
Tab(text: '首页'),
Tab(text: '资讯'),
Tab(text: '推荐'),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
Text('首页'),
Text('资讯'),
Text('推荐'),
],
),
),
],
);
}
}