常用组件:文本、图片与按钮
日常写页面 80% 的时间都在用三样东西:Text、Image、按钮。它们看着简单,细节却最多——文本缩放会不会撑破布局、网络图失败怎么兜底、按钮什么时候算禁用。本章逐个讲清常用参数与踩坑点,代码可直接复制。
Text:样式与富文本
Text(
'Flutter 文本示例',
style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.teal),
maxLines: 2,
overflow: TextOverflow.ellipsis, // 超出两行显示省略号,避免溢出
textAlign: TextAlign.start,
)
| 需求 | 写法 | 说明 |
|---|---|---|
| 局部加粗或变色 | Text.rich(TextSpan(children: [...])) | 自动继承默认样式,推荐 |
| 跟随系统字号缩放 | 默认行为,读 MediaQuery.textScalerOf(context) | textScaleFactorOf 已废弃 |
富文本用 Text.rich:价格大字号、说明灰色小字,互不影响。
Text.rich(
const TextSpan(children: [
TextSpan(text: '¥', style: TextStyle(fontSize: 14, color: Colors.red)),
TextSpan(text: '199', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.red)),
TextSpan(text: ' 限时优惠', style: TextStyle(fontSize: 12, color: Colors.grey)),
]),
textScaler: MediaQuery.textScalerOf(context), // 读取系统字号缩放
)
Image:三种来源与加载状态
| 构造方式 | 数据来源 | 适用场景 |
|---|---|---|
Image.network | URL | 服务端图片(需联网权限) |
Image.asset | pubspec.yaml 声明的资源 | 本地图标、占位图 |
Image.file | 文件路径 | 相机/相册结果、缓存文件 |
Image.network(
'https://picsum.photos/600/400',
width: double.infinity,
height: 200,
fit: BoxFit.cover, // cover 裁切填满,contain 完整显示,fill 会变形
loadingBuilder: (context, child, progress) =>
progress == null ? child : const Center(child: CircularProgressIndicator()),
errorBuilder: (context, error, stack) =>
const ColoredBox(color: Color(0xFFEEEEEE), child: Center(child: Icon(Icons.broken_image))),
)
Image.asset('assets/images/logo.png', width: 96, height: 96) // 先在 pubspec.yaml 声明资源
fit 速查:cover 填满可能裁切、contain 完整可能留白、fitWidth 按宽铺满、fill 一定变形——列表封面图基本都用 cover。列表里的网络图建议改用 cached_network_image,省流量也避免滚动时反复请求:
flutter pub add cached_network_image
CachedNetworkImage(
imageUrl: 'https://picsum.photos/600/400',
fit: BoxFit.cover,
placeholder: (context, url) => const ColoredBox(color: Color(0xFFEEEEEE)),
errorWidget: (context, url, error) => const Icon(Icons.error_outline),
)
Icon 与按钮家族
Icon(Icons.favorite, size: 20, color: Colors.red);
Icon(Icons.star, semanticLabel: '评分'); // 读屏会念出来
| 组件 | 视觉层级 | 典型用途 |
|---|---|---|
FilledButton | 最高,实心强调 | 页面主操作(提交、下单) |
ElevatedButton | 高,带阴影 | 通用主要按钮 |
OutlinedButton | 中,描边 | 次要操作 |
TextButton | 低,纯文字 | 取消、跳转、行内操作 |
IconButton | 图标按钮 | 工具栏、列表行操作 |
onPressed: null 就是禁用,视觉变灰且不响应点击,不需要额外的 enabled 参数:
Row(
children: [
Expanded(
child: FilledButton(onPressed: canSubmit ? _submit : null, child: const Text('提交')),
),
const SizedBox(width: 12),
OutlinedButton.icon(onPressed: _cancel, icon: const Icon(Icons.close), label: const Text('取消')),
IconButton(onPressed: _toggle, icon: const Icon(Icons.bookmark_border), tooltip: '收藏'),
],
)
容器类组件
| 组件 | 用途 | 关键参数 |
|---|---|---|
Card | 卡片容器,自带圆角阴影 | elevation、shape、margin |
Divider | 分隔线 | height、thickness、indent |
Chip | 标签、筛选项 | avatar、onDeleted、selected |
Badge | 角标(消息数) | label、isLabelVisible |
Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(children: [
const ListTile(
leading: CircleAvatar(child: Text('瑞')),
title: Text('瑞林'),
subtitle: Text('Flutter 学习者'),
trailing: Badge(label: Text('3'), child: Icon(Icons.notifications_none)),
),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.all(12),
child: Wrap(spacing: 8, children: const [Chip(label: Text('Dart')), Chip(label: Text('Flutter'))]),
),
]),
)
常见坑与调试方法
| 现象 | 原因 | 排查方式 |
|---|---|---|
| 文本溢出或截断 | 写死高度、系统字号被放大 | 去掉固定 height,设 maxLines + overflow |
| 图片一片空白 | 忘了声明 assets 或地址 404 | 检查 pubspec.yaml 并补 errorBuilder |
| 列表滚动掉帧 | 大图未限制解码尺寸 | 加 cacheWidth/cacheHeight 或改用缩略图 |
| 按钮点了没反应 | onPressed 传了 null 或抛异常 | 断点确认是否真的进入回调 |
小结:Text 优先用主题样式加 copyWith,并留意系统字号缩放导致的溢出;网络图必须配 loadingBuilder/errorBuilder 或改用 cached_network_image;按钮用 onPressed: null 表达禁用,主次操作按 FilledButton/OutlinedButton/TextButton 分层;Card、ListTile、Chip、Badge 组合起来能覆盖大部分列表与详情页。