一、文本与样式
文本是界面中最基础的元素。Flutter 提供了 Text 和 RichText 组件来展示文字内容,通过 TextStyle 控制字体样式。
1.1 Text 基础用法
Text 组件是最常用的文本展示方式,支持对齐、溢出处理、最大行数等属性。
// Text 组件的基础用法 Text( 'Hello, Flutter!', style: TextStyle( fontSize: 24, // 字号 fontWeight: FontWeight.bold, // 粗体 color: Colors.blue, // 颜色 letterSpacing: 2.0, // 字间距 height: 1.5, // 行高倍数 ), textAlign: TextAlign.center, // 文本居中 maxLines: 2, // 最多显示两行 overflow: TextOverflow.ellipsis, // 超出显示省略号 )
1.2 TextStyle 详解
TextStyle 提供了丰富的样式属性,可以精细控制文字的外观。
// TextStyle 的常用属性演示 Text( '样式丰富的文本', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, // 字重 100-900 fontStyle: FontStyle.italic, // 斜体 color: Color(0xFF333333), // 自定义颜色 backgroundColor: Colors.yellow.withOpacity(0.3), decoration: TextDecoration.underline, // 下划线 decorationColor: Colors.red, // 装饰线颜色 decorationStyle: TextDecorationStyle.dashed, // 虚线 shadows: [ Shadow( color: Colors.grey, offset: Offset(2, 2), blurRadius: 4, ), ], ), )
1.3 RichText 富文本
当需要在一段文字中使用不同样式时,使用 RichText 或 Text.rich。
// 使用 Text.rich 创建富文本 Text.rich( TextSpan( text: '欢迎来到 ', style: TextStyle(fontSize: 18, color: Colors.black), children: [ TextSpan( text: 'Flutter', style: TextStyle( color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 22, ), ), TextSpan(text: ' 的世界!'), // 可点击的文本片段 WidgetSpan( child: GestureDetector( onTap: () => print('点击了链接'), child: Text( '了解更多', style: TextStyle( color: Colors.orange, decoration: TextDecoration.underline, ), ), ), ), ], ), )
DefaultTextStyle 可以为子树中所有 Text 组件设置默认样式,避免重复定义。对于需要自定义字体的场景,在 pubspec.yaml 中声明字体文件,再通过 fontFamily 引用。
二、图片与图标
Flutter 支持多种图片加载方式,包括本地资源、网络图片和内存图片。Icon 组件则提供了丰富的矢量图标。
2.1 Image 组件
Flutter 通过不同的工厂构造函数加载不同来源的图片。
// 加载本地资源图片(需在 pubspec.yaml 中声明) Image.asset( 'assets/images/logo.png', width: 200, height: 200, fit: BoxFit.cover, // 图片填充方式 ) // 加载网络图片 Image.network( 'https://example.com/image.jpg', width: 300, height: 200, fit: BoxFit.contain, // 加载中的占位图 loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { if (loadingProgress == null) return child; return Center( child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, ), ); }, // 加载失败时显示的组件 errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) { return Icon(Icons.broken_image, size: 64, color: Colors.grey); }, )
2.2 CircleAvatar 与圆形图片
// 使用 CircleAvatar 展示圆形头像 CircleAvatar( radius: 50, backgroundImage: NetworkImage('https://example.com/avatar.jpg'), backgroundColor: Colors.grey[200], ) // 使用 ClipOval 裁剪为圆形 ClipOval( child: Image.network( 'https://example.com/photo.jpg', width: 100, height: 100, fit: BoxFit.cover, ), ) // 使用 ClipRRect 裁剪为圆角矩形 ClipRRect( borderRadius: BorderRadius.circular(16), child: Image.asset('assets/images/banner.png', fit: BoxFit.cover), )
2.3 Icon 图标
// Material Design 内置图标 Icon( Icons.favorite, color: Colors.red, size: 36, ) // 带背景的图标按钮 Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.blue.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon(Icons.star, color: Colors.blue, size: 32), ) // IconButton - 可点击的图标 IconButton( icon: Icon(Icons.share), iconSize: 28, color: Colors.teal, tooltip: '分享', onPressed: () => print('分享被点击'), )
cached_network_image 来缓存图片,提升加载性能。BoxFit 的常用值包括:cover(裁剪填满)、contain(完整显示)、fill(拉伸填满)。
三、按钮组件
Flutter 3.x 推荐使用 Material 3 的按钮体系:ElevatedButton(凸起按钮)、TextButton(文字按钮)、OutlinedButton(边框按钮)。
3.1 三种基础按钮
// ElevatedButton - 带阴影的凸起按钮,用于主要操作 ElevatedButton( onPressed: () { print('主要操作'); }, child: Text('确认'), ) // TextButton - 扁平文字按钮,用于次要操作 TextButton( onPressed: () { print('取消操作'); }, child: Text('取消'), ) // OutlinedButton - 带边框的按钮 OutlinedButton( onPressed: () { print('边框按钮'); }, child: Text('更多选项'), ) // 禁用按钮 - 将 onPressed 设为 null ElevatedButton( onPressed: null, // 禁用状态 child: Text('不可点击'), )
3.2 自定义按钮样式
// 使用 styleFrom 自定义按钮外观 ElevatedButton( onPressed: () {}, style: ElevatedButton.styleFrom( backgroundColor: Colors.deepPurple, // 背景色 foregroundColor: Colors.white, // 文字颜色 padding: EdgeInsets.symmetric( horizontal: 32, vertical: 16, ), shape: RoundedRectangleBorder( // 圆角 borderRadius: BorderRadius.circular(12), ), elevation: 6, // 阴影高度 textStyle: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), child: Text('自定义按钮'), ) // 带图标的按钮 ElevatedButton.icon( onPressed: () {}, icon: Icon(Icons.send), label: Text('发送消息'), )
3.3 渐变色按钮
// 使用 Container + InkWell 实现渐变按钮 Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.blue, Colors.purple], ), borderRadius: BorderRadius.circular(30), boxShadow: [ BoxShadow( color: Colors.blue.withOpacity(0.4), blurRadius: 12, offset: Offset(0, 4), ), ], ), child: Material( color: Colors.transparent, child: InkWell( onTap: () => print('渐变按钮被点击'), borderRadius: BorderRadius.circular(30), child: Padding( padding: EdgeInsets.symmetric(horizontal: 32, vertical: 14), child: Text( '渐变按钮', style: TextStyle(color: Colors.white, fontSize: 16), ), ), ), ), )
onPressed 为 null 时按钮自动变为禁用状态。建议使用 ButtonStyle 主题统一管理按钮样式,避免每个按钮单独设置。使用 FloatingActionButton 作为页面的主要悬浮操作按钮。
四、输入与表单
表单是用户交互的核心。Flutter 提供了 TextField、TextFormField 和 Form 来构建完整的表单体验。
4.1 TextField 基础输入
// 基础文本输入框 TextField( decoration: InputDecoration( labelText: '用户名', // 浮动标签 hintText: '请输入用户名', // 占位提示 prefixIcon: Icon(Icons.person), // 前缀图标 suffixIcon: Icon(Icons.clear), // 后缀图标 border: OutlineInputBorder( // 边框样式 borderRadius: BorderRadius.circular(12), ), filled: true, // 填充背景 fillColor: Colors.grey[50], ), onChanged: (String value) { print('输入内容: $value'); // 实时监听输入变化 }, ) // 密码输入框 TextField( obscureText: true, // 隐藏输入内容 decoration: InputDecoration( labelText: '密码', prefixIcon: Icon(Icons.lock), border: OutlineInputBorder(), ), )
4.2 TextEditingController 控制器
class SearchPage extends StatefulWidget { @override _SearchPageState createState() => _SearchPageState(); } class _SearchPageState extends State<SearchPage> { // 声明控制器 final _controller = TextEditingController(); @override void initState() { super.initState(); // 可以设置初始值 _controller.text = '默认搜索词'; // 监听文本变化 _controller.addListener(() { print('当前值: ${_controller.text}'); }); } @override void dispose() { _controller.dispose(); // 释放控制器,防止内存泄漏 super.dispose(); } @override Widget build(BuildContext context) { return TextField( controller: _controller, decoration: InputDecoration( hintText: '搜索...', suffixIcon: IconButton( icon: Icon(Icons.clear), onPressed: () => _controller.clear(), // 清空内容 ), ), ); } }
4.3 Form 表单验证
使用 Form + TextFormField 构建带验证功能的完整表单。
class RegistrationForm extends StatefulWidget { @override _RegistrationFormState createState() => _RegistrationFormState(); } class _RegistrationFormState extends State<RegistrationForm> { // 表单的全局 Key,用于验证和保存 final _formKey = GlobalKey<FormState>(); String _email = ''; String _password = ''; String _nickname = ''; void _submitForm() { // validate() 会触发所有 TextFormField 的 validator if (_formKey.currentState!.validate()) { _formKey.currentState!.save(); // 触发 onSaved print('注册信息: $_nickname, $_email'); // 提交到服务器... } } @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ // 昵称输入 TextFormField( decoration: InputDecoration( labelText: '昵称', prefixIcon: Icon(Icons.person), border: OutlineInputBorder(), ), validator: (String? value) { if (value == null || value.isEmpty) { return '请输入昵称'; } if (value.length < 2) { return '昵称至少需要2个字符'; } return null; // 返回 null 表示验证通过 }, onSaved: (value) => _nickname = value!, ), SizedBox(height: 16), // 邮箱输入 TextFormField( decoration: InputDecoration( labelText: '邮箱', prefixIcon: Icon(Icons.email), border: OutlineInputBorder(), ), keyboardType: TextInputType.emailAddress, validator: (value) { if (value == null || !value.contains('@')) { return '请输入有效的邮箱地址'; } return null; }, onSaved: (value) => _email = value!, ), SizedBox(height: 16), // 密码输入 TextFormField( decoration: InputDecoration( labelText: '密码', prefixIcon: Icon(Icons.lock), border: OutlineInputBorder(), ), obscureText: true, validator: (value) { if (value == null || value.length < 6) { return '密码至少需要6位'; } return null; }, onSaved: (value) => _password = value!, ), SizedBox(height: 24), // 提交按钮 SizedBox( width: double.infinity, height: 48, child: ElevatedButton( onPressed: _submitForm, child: Text('注册', style: TextStyle(fontSize: 18)), ), ), ], ), ); } }
TextEditingController 时务必在 dispose() 中释放,防止内存泄漏。Form 的 autovalidateMode 属性可设置为 AutovalidateMode.onUserInteraction 实现实时验证。
五、容器与装饰
Container 是 Flutter 最灵活的布局组件之一,结合 BoxDecoration 可以实现丰富的视觉效果。
5.1 Container 详解
// Container 的完整用法 Container( width: 300, height: 200, margin: EdgeInsets.all(16), // 外边距 padding: EdgeInsets.all(20), // 内边距 alignment: Alignment.center, // 子组件对齐方式 decoration: BoxDecoration( color: Colors.white, // 背景色 borderRadius: BorderRadius.circular(16), // 圆角 border: Border.all( // 边框 color: Colors.grey[300]!, width: 1, ), boxShadow: [ // 阴影 BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 10, spreadRadius: 2, offset: Offset(0, 4), ), ], ), child: Text('装饰容器', style: TextStyle(fontSize: 20)), )
5.2 BoxDecoration 渐变与图片背景
// 渐变背景 Container( width: double.infinity, height: 200, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Color(0xFF6A11CB), Color(0xFF2575FC), ], ), borderRadius: BorderRadius.circular(20), ), child: Center( child: Text( '渐变背景', style: TextStyle(color: Colors.white, fontSize: 24), ), ), ) // 图片背景容器 Container( width: 300, height: 180, decoration: BoxDecoration( image: DecorationImage( image: AssetImage('assets/images/bg.jpg'), fit: BoxFit.cover, colorFilter: ColorFilter.mode( Colors.black.withOpacity(0.3), // 半透明遮罩 BlendMode.darken, ), ), borderRadius: BorderRadius.circular(16), ), )
5.3 Padding、SizedBox 与 Card
// Padding - 仅添加内边距的轻量组件 Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Text('带内边距的文本'), ) // SizedBox - 固定尺寸或间距 Column( children: [ Text('第一行'), SizedBox(height: 16), // 常用作间距组件 Text('第二行'), SizedBox(height: 16), SizedBox( // 限制子组件的宽高 width: 200, height: 50, child: ElevatedButton(onPressed: () {}, child: Text('按钮')), ), ], ) // Card - 带圆角阴影的 Material 卡片 Card( elevation: 4, // 阴影高度 margin: EdgeInsets.all(12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('卡片标题', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), SizedBox(height: 8), Text('这是卡片的内容描述,Card 组件自带阴影和圆角效果。'), ], ), ), )
SizedBox 或 Padding,它们比 Container 更轻量。Container 内部实际上是多个组件的组合(Padding、DecoratedBox、ConstrainedBox 等),仅在需要装饰效果时使用。
六、线性布局
Row(水平排列)和 Column(垂直排列)是最基础的线性布局组件,搭配 Expanded、Flexible 实现弹性布局。
6.1 Row 与 Column 基础
// Row - 水平排列子组件 Row( // 主轴(水平方向)对齐方式 mainAxisAlignment: MainAxisAlignment.spaceBetween, // 交叉轴(垂直方向)对齐方式 crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(Icons.home, size: 32), Text('首页', style: TextStyle(fontSize: 18)), Icon(Icons.arrow_forward), ], ) // Column - 垂直排列子组件 Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, // 左对齐 children: [ Text('标题', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), SizedBox(height: 8), Text('副标题', style: TextStyle(fontSize: 16, color: Colors.grey)), SizedBox(height: 16), ElevatedButton(onPressed: () {}, child: Text('操作')), ], )
6.2 MainAxisAlignment 对齐方式
// MainAxisAlignment 的六种对齐方式示意 // start : [A][B][C] (默认) // end : [A][B][C] // center : [A][B][C] // spaceBetween : [A] [B] [C] (两端对齐) // spaceAround : [A] [B] [C] (均匀间距,两端为一半) // spaceEvenly : [A] [B] [C] (完全均匀间距) // 实际使用示例 - 底部操作栏 Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Column( mainAxisSize: MainAxisSize.min, children: [Icon(Icons.home), Text('首页')], ), Column( mainAxisSize: MainAxisSize.min, children: [Icon(Icons.search), Text('搜索')], ), Column( mainAxisSize: MainAxisSize.min, children: [Icon(Icons.person), Text('我的')], ), ], )
6.3 Expanded 与 Flexible
// Expanded - 填充剩余空间 Row( children: [ // 头像区域 - 固定宽度 CircleAvatar(radius: 25, child: Icon(Icons.person)), SizedBox(width: 12), // 文本区域 - 自动填满剩余空间 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('张三', style: TextStyle(fontWeight: FontWeight.bold)), Text('这是一条很长的消息,超出部分会自动换行或截断...', maxLines: 1, overflow: TextOverflow.ellipsis), ], ), ), // 时间 - 固定宽度 Text('14:30', style: TextStyle(color: Colors.grey)), ], ) // flex 比例分配空间 Row( children: [ Expanded( flex: 2, // 占 2/3 空间 child: Container(height: 80, color: Colors.blue[200]), ), SizedBox(width: 8), Expanded( flex: 1, // 占 1/3 空间 child: Container(height: 80, color: Colors.red[200]), ), ], ) // Flexible - 可以选择不填满剩余空间 Row( children: [ Flexible( fit: FlexFit.loose, // 子组件可以小于分配的空间 child: Container( width: 100, height: 50, color: Colors.green[200], ), ), Text('后面的内容'), ], )
Expanded 等价于 Flexible(fit: FlexFit.tight)。当 Row/Column 中的子组件总宽/高超出时会报溢出错误(黄黑条纹),此时可以用 Expanded 包裹让子组件自适应,或使用 SingleChildScrollView 让内容可滚动。mainAxisSize: MainAxisSize.min 可以让 Row/Column 仅占用子组件所需的空间。
七、层叠与流式布局
Stack 实现层叠布局(类似 CSS 的 absolute 定位),Wrap 实现自动换行的流式布局。
7.1 Stack 层叠布局
// Stack - 子组件层叠在一起 Stack( alignment: Alignment.center, // 未定位子组件的默认对齐方式 clipBehavior: Clip.none, // 允许子组件超出边界 children: [ // 底层 - 背景图片 Container( width: 300, height: 200, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), color: Colors.blue[100], ), ), // 中间层 - 文字 Text( '层叠布局', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), ), // 顶层 - 使用 Positioned 精确定位 Positioned( top: 10, right: 10, child: Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(12), ), child: Text('NEW', style: TextStyle(color: Colors.white, fontSize: 12)), ), ), ], )
7.2 Positioned 精确定位
// 使用 Positioned 的各种定位方式 Stack( children: [ Container(width: 300, height: 300, color: Colors.grey[200]), // 左上角定位 Positioned( top: 10, left: 10, child: Icon(Icons.star, color: Colors.amber), ), // 底部拉伸 - 同时设置 left 和 right Positioned( bottom: 0, left: 0, right: 0, child: Container( padding: EdgeInsets.all(12), color: Colors.black54, child: Text('底部标题栏', style: TextStyle(color: Colors.white), textAlign: TextAlign.center), ), ), // Positioned.fill - 充满整个 Stack Positioned.fill( child: Center(child: Text('居中内容')), ), ], )
7.3 Wrap 流式布局
// Wrap - 子组件超出一行时自动换行 Wrap( spacing: 8, // 水平间距 runSpacing: 8, // 行间距 alignment: WrapAlignment.start, children: [ 'Flutter', 'Dart', 'Widget', 'State', '布局', '动画', '导航', '网络请求', '状态管理', ].map((tag) => Chip( label: Text(tag), avatar: Icon(Icons.tag, size: 18), backgroundColor: Colors.blue[50], deleteIcon: Icon(Icons.close, size: 16), onDeleted: () => print('删除 $tag'), )).toList(), ) // Wrap 实现标签选择器 Wrap( spacing: 10, runSpacing: 10, children: List.generate(8, (index) { return ChoiceChip( label: Text('标签 $index'), selected: index == 0, // 选中状态 onSelected: (selected) {}, ); }), )
Stack 中未被 Positioned 包裹的子组件会根据 alignment 属性对齐。Positioned 必须是 Stack 的直接子组件。当需要实现类似"标签云"或"商品标签"等自适应换行的布局时,使用 Wrap 非常方便。
八、列表与滚动
列表是移动应用中最常见的界面模式。Flutter 提供了 ListView、GridView、CustomScrollView 等滚动组件。
8.1 ListView.builder 高性能列表
// ListView.builder - 按需构建列表项,适合长列表 ListView.builder( itemCount: 100, // 列表项总数 itemBuilder: (BuildContext context, int index) { return ListTile( leading: CircleAvatar( child: Text('${index + 1}'), ), title: Text('列表项 $index'), subtitle: Text('这是第 $index 项的描述'), trailing: Icon(Icons.chevron_right), onTap: () => print('点击了第 $index 项'), ); }, ) // ListView.separated - 带分隔线的列表 ListView.separated( itemCount: 50, separatorBuilder: (BuildContext context, int index) { return Divider(height: 1, indent: 72); // 分隔线 }, itemBuilder: (BuildContext context, int index) { return ListTile( leading: Icon(Icons.article), title: Text('文章标题 $index'), ); }, )
8.2 GridView 网格布局
// GridView.builder - 高性能网格列表 GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, // 每行 2 列 crossAxisSpacing: 12, // 列间距 mainAxisSpacing: 12, // 行间距 childAspectRatio: 0.75, // 宽高比 ), padding: EdgeInsets.all(12), itemCount: 20, itemBuilder: (BuildContext context, int index) { return Card( clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Container( color: Colors.primaries[index % 18][100], child: Center(child: Icon(Icons.image, size: 48)), ), ), Padding( padding: EdgeInsets.all(8), child: Text('商品 $index', style: TextStyle(fontWeight: FontWeight.bold)), ), Padding( padding: EdgeInsets.symmetric(horizontal: 8), child: Text('¥${(index + 1) * 29.9}', style: TextStyle(color: Colors.red)), ), SizedBox(height: 8), ], ), ); }, ) // 自适应列数的网格 GridView.builder( gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 180, // 每项最大宽度,列数自动计算 crossAxisSpacing: 10, mainAxisSpacing: 10, ), itemCount: 30, itemBuilder: (context, index) => Container(color: Colors.teal[100]), )
8.3 CustomScrollView 与 Slivers
CustomScrollView 允许组合多种 Sliver 组件,实现复杂的滚动效果。
// CustomScrollView - 组合多种滚动组件 CustomScrollView( slivers: [ // SliverAppBar - 可折叠的应用栏 SliverAppBar( expandedHeight: 200, floating: false, pinned: true, // 收缩后固定在顶部 flexibleSpace: FlexibleSpaceBar( title: Text('我的主页'), background: Image.network( 'https://example.com/header.jpg', fit: BoxFit.cover, ), ), ), // SliverToBoxAdapter - 放置普通组件 SliverToBoxAdapter( child: Padding( padding: EdgeInsets.all(16), child: Text('热门推荐', style: TextStyle(fontSize: 20)), ), ), // SliverGrid - 网格列表 SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, mainAxisSpacing: 8, crossAxisSpacing: 8, ), delegate: SliverChildBuilderDelegate( (context, index) => Container( color: Colors.primaries[index % 18], child: Center(child: Text('$index', style: TextStyle(color: Colors.white))), ), childCount: 9, ), ), // SliverList - 列表 SliverList( delegate: SliverChildBuilderDelegate( (context, index) => ListTile( title: Text('列表项 $index'), ), childCount: 20, ), ), ], )
8.4 PageView 与 RefreshIndicator
// PageView - 页面切换(轮播图) SizedBox( height: 200, child: PageView.builder( itemCount: 5, controller: PageController(viewportFraction: 0.85), // 显示相邻页的一部分 itemBuilder: (context, index) { return Padding( padding: EdgeInsets.symmetric(horizontal: 8), child: Container( decoration: BoxDecoration( color: Colors.primaries[index % 18][200], borderRadius: BorderRadius.circular(16), ), child: Center(child: Text('页面 ${index + 1}', style: TextStyle(fontSize: 24))), ), ); }, ), ) // RefreshIndicator - 下拉刷新 RefreshIndicator( onRefresh: () async { // 模拟网络请求 await Future.delayed(Duration(seconds: 2)); // 刷新数据... }, child: ListView.builder( itemCount: 30, itemBuilder: (context, index) => ListTile( title: Text('可下拉刷新的列表项 $index'), ), ), )
ListView.builder 和 GridView.builder 采用懒加载机制,只会构建可见区域内的组件,非常适合长列表。避免在 ListView 内嵌套 ListView,如需要请对内层列表设置 shrinkWrap: true 和 physics: NeverScrollableScrollPhysics()。复杂滚动页面推荐使用 CustomScrollView。
九、弹窗与提示
弹窗和提示是与用户交互的重要方式。Flutter 提供了 AlertDialog、BottomSheet、SnackBar 等组件。
9.1 AlertDialog 对话框
// 显示确认对话框 void showConfirmDialog(BuildContext context) { showDialog( context: context, barrierDismissible: false, // 点击外部不关闭 builder: (BuildContext context) { return AlertDialog( title: Text('确认删除'), content: Text('删除后将无法恢复,确定要删除吗?'), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), // 关闭弹窗 child: Text('取消'), ), ElevatedButton( onPressed: () { // 执行删除操作 Navigator.pop(context); print('已删除'); }, style: ElevatedButton.styleFrom(backgroundColor: Colors.red), child: Text('删除'), ), ], ); }, ); } // 获取对话框返回值 Future<void> showChoiceDialog(BuildContext context) async { final result = await showDialog<String>( context: context, builder: (context) => SimpleDialog( title: Text('选择语言'), children: [ SimpleDialogOption( onPressed: () => Navigator.pop(context, '中文'), child: Text('中文'), ), SimpleDialogOption( onPressed: () => Navigator.pop(context, 'English'), child: Text('English'), ), ], ), ); print('选择了: $result'); }
9.2 BottomSheet 底部弹出面板
// 模态底部面板 void showBottomOptions(BuildContext context) { showModalBottomSheet( context: context, shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (BuildContext context) { return Container( padding: EdgeInsets.all(20), child: Column( mainAxisSize: MainAxisSize.min, // 高度自适应内容 children: [ // 顶部拖动指示条 Container( width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(2), ), ), SizedBox(height: 20), ListTile( leading: Icon(Icons.camera_alt, color: Colors.blue), title: Text('拍照'), onTap: () => Navigator.pop(context), ), ListTile( leading: Icon(Icons.photo_library, color: Colors.green), title: Text('从相册选择'), onTap: () => Navigator.pop(context), ), ListTile( leading: Icon(Icons.delete, color: Colors.red), title: Text('删除图片'), onTap: () => Navigator.pop(context), ), ], ), ); }, ); }
9.3 SnackBar 底部提示条
// 显示 SnackBar 提示 void showSnackMessage(BuildContext context) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('操作成功!'), duration: Duration(seconds: 3), // 显示时长 backgroundColor: Colors.green, behavior: SnackBarBehavior.floating, // 悬浮样式 shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), margin: EdgeInsets.all(16), action: SnackBarAction( label: '撤销', textColor: Colors.white, onPressed: () { print('撤销操作'); }, ), ), ); }
showDialog 返回 Future,可以通过 await 获取弹窗的返回值。SnackBar 使用 ScaffoldMessenger 来显示,确保页面有 Scaffold 组件。底部面板可以设置 isScrollControlled: true 来实现全屏或可拖拽的效果。
十、响应式布局
为了适配不同屏幕尺寸,Flutter 提供了 MediaQuery 和 LayoutBuilder 来获取屏幕信息和约束条件。
10.1 MediaQuery 获取屏幕信息
// 使用 MediaQuery 获取屏幕尺寸 @override Widget build(BuildContext context) { // 获取屏幕尺寸 final screenSize = MediaQuery.of(context).size; final screenWidth = screenSize.width; final screenHeight = screenSize.height; // 获取安全区域(避开刘海、底部导航条等) final padding = MediaQuery.of(context).padding; final topPadding = padding.top; // 状态栏高度 final bottomPadding = padding.bottom; // 底部安全区 // 获取设备像素比 final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; // 横竖屏判断 final isLandscape = screenWidth > screenHeight; // 根据屏幕宽度决定布局 final isTablet = screenWidth > 600; return Scaffold( body: isTablet ? _buildTabletLayout() // 平板布局 : _buildPhoneLayout(), // 手机布局 ); }
10.2 LayoutBuilder 响应约束变化
// LayoutBuilder - 根据父组件给出的约束条件来决定布局 LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { // constraints.maxWidth 是父组件给出的最大宽度 if (constraints.maxWidth > 900) { // 大屏幕 - 三栏布局 return Row( children: [ SizedBox(width: 250, child: _buildSidebar()), Expanded(child: _buildMainContent()), SizedBox(width: 300, child: _buildRightPanel()), ], ); } else if (constraints.maxWidth > 600) { // 中等屏幕 - 两栏布局 return Row( children: [ SizedBox(width: 250, child: _buildSidebar()), Expanded(child: _buildMainContent()), ], ); } else { // 小屏幕 - 单栏布局 return _buildMainContent(); } }, ) // 实用示例:自适应网格列数 LayoutBuilder( builder: (context, constraints) { // 根据宽度动态计算列数 int crossAxisCount = (constraints.maxWidth / 180).floor(); crossAxisCount = crossAxisCount.clamp(2, 6); // 限制 2-6 列 return GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, crossAxisSpacing: 12, mainAxisSpacing: 12, ), itemCount: 20, itemBuilder: (context, index) => Card( child: Center(child: Text('Item $index')), ), ); }, )
10.3 完整响应式页面示例
class ResponsivePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('响应式布局')), // 仅在小屏幕显示侧边栏抽屉 drawer: MediaQuery.of(context).size.width < 600 ? Drawer(child: _buildNavMenu()) : null, body: SafeArea( child: LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth >= 600) { // 大屏 - 左侧导航 + 右侧内容 return Row( children: [ NavigationRail( selectedIndex: 0, destinations: [ NavigationRailDestination( icon: Icon(Icons.home), label: Text('首页'), ), NavigationRailDestination( icon: Icon(Icons.settings), label: Text('设置'), ), ], onDestinationSelected: (index) {}, ), VerticalDivider(width: 1), Expanded(child: _buildContent()), ], ); } // 小屏 - 底部导航 return _buildContent(); }, ), ), // 小屏幕时显示底部导航 bottomNavigationBar: MediaQuery.of(context).size.width < 600 ? BottomNavigationBar( items: [ BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'), BottomNavigationBarItem(icon: Icon(Icons.settings), label: '设置'), ], ) : null, ); } }
LayoutBuilder 获取的是父组件给出的约束,而 MediaQuery 获取的是整个屏幕的信息。优先使用 LayoutBuilder,因为它能正确响应组件在不同位置时的宽度变化。SafeArea 组件可以自动避开状态栏、底部导航条等系统区域。
十一、实践练习
通过以下练习巩固本章学习的组件和布局知识。
练习一:个人名片卡
使用 Card、Row、Column、CircleAvatar 等组件,构建一个包含头像、姓名、职位和联系方式的个人名片卡。
要求:
- 左侧显示圆形头像
- 右侧显示姓名(大号加粗)、职位(灰色)和邮箱
- 底部有两个操作按钮:"发消息"和"打电话"
- 卡片带圆角和阴影效果
// 参考实现 Card( elevation: 4, margin: EdgeInsets.all(16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( padding: EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ CircleAvatar( radius: 36, backgroundImage: NetworkImage('https://example.com/avatar.jpg'), ), SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('张三', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), SizedBox(height: 4), Text('高级 Flutter 开发工程师', style: TextStyle(color: Colors.grey[600])), SizedBox(height: 4), Text('zhangsan@example.com', style: TextStyle(color: Colors.blue)), ], ), ), ], ), SizedBox(height: 16), Divider(), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ TextButton.icon( icon: Icon(Icons.message), label: Text('发消息'), onPressed: () {}, ), TextButton.icon( icon: Icon(Icons.phone), label: Text('打电话'), onPressed: () {}, ), ], ), ], ), ), )
练习二:商品列表页
使用 GridView.builder 和 Card 构建一个商品列表页面。
要求:
- 每行显示两个商品卡片
- 每个卡片包含商品图片、名称、价格
- 价格用红色显示
- 使用
LayoutBuilder在宽屏时显示更多列
// 参考实现 - 自适应商品网格 LayoutBuilder( builder: (context, constraints) { int columns = (constraints.maxWidth / 200).floor().clamp(2, 4); return GridView.builder( padding: EdgeInsets.all(12), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: columns, crossAxisSpacing: 12, mainAxisSpacing: 12, childAspectRatio: 0.7, ), itemCount: 10, itemBuilder: (context, index) { return Card( clipBehavior: Clip.antiAlias, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 商品图片占位 Expanded( child: Container( width: double.infinity, color: Colors.grey[200], child: Icon(Icons.shopping_bag, size: 48, color: Colors.grey), ), ), Padding( padding: EdgeInsets.fromLTRB(8, 8, 8, 4), child: Text('精选商品 ${index + 1}', style: TextStyle(fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis), ), Padding( padding: EdgeInsets.fromLTRB(8, 0, 8, 8), child: Text('¥${((index + 1) * 49.9).toStringAsFixed(1)}', style: TextStyle(color: Colors.red, fontSize: 16, fontWeight: FontWeight.bold)), ), ], ), ); }, ); }, )
练习三:登录页面
综合运用 Form、Container、Column 等组件构建完整的登录页面。
要求:
- 顶部显示应用 Logo 和标题
- 包含邮箱和密码两个输入框,带验证功能
- 包含"记住密码"复选框和"忘记密码"文字按钮
- 渐变色的登录按钮
- 底部有"注册账号"的入口
// 参考实现 - 登录页面 class LoginPage extends StatefulWidget { @override _LoginPageState createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> { final _formKey = GlobalKey<FormState>(); bool _rememberMe = false; @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Center( child: SingleChildScrollView( padding: EdgeInsets.all(32), child: Form( key: _formKey, child: Column( children: [ // Logo Icon(Icons.flutter_dash, size: 80, color: Colors.blue), SizedBox(height: 16), Text('欢迎回来', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)), SizedBox(height: 8), Text('请登录您的账号', style: TextStyle(color: Colors.grey)), SizedBox(height: 40), // 邮箱输入 TextFormField( decoration: InputDecoration( labelText: '邮箱', prefixIcon: Icon(Icons.email_outlined), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), ), validator: (v) => v != null && v.contains('@') ? null : '请输入有效邮箱', ), SizedBox(height: 16), // 密码输入 TextFormField( obscureText: true, decoration: InputDecoration( labelText: '密码', prefixIcon: Icon(Icons.lock_outlined), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), ), validator: (v) => v != null && v.length >= 6 ? null : '密码至少6位', ), SizedBox(height: 8), // 记住密码 & 忘记密码 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Checkbox( value: _rememberMe, onChanged: (v) => setState(() => _rememberMe = v!), ), Text('记住密码'), ], ), TextButton( onPressed: () {}, child: Text('忘记密码?'), ), ], ), SizedBox(height: 24), // 渐变登录按钮 SizedBox( width: double.infinity, height: 50, child: Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.blue, Colors.purple], ), borderRadius: BorderRadius.circular(12), ), child: ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { print('登录成功'); } }, style: ElevatedButton.styleFrom( backgroundColor: Colors.transparent, shadowColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: Text('登 录', style: TextStyle(fontSize: 18, color: Colors.white)), ), ), ), SizedBox(height: 24), // 注册入口 Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('还没有账号?', style: TextStyle(color: Colors.grey)), TextButton( onPressed: () {}, child: Text('立即注册'), ), ], ), ], ), ), ), ), ), ); } }