4.2 KiB
4.2 KiB
【Demo】响应式布局:一套代码适配手机 / Pad
源文件:
lib/pages/demo/responsive_demo.dart| 分类:基础布局 一句话:屏幕适配 ≠ 响应式布局 —— 一个是"等比缩放",一个是"改变结构"。
一、这个 Demo 解决什么问题
同一个页面,手机单列列表、Pad 双列卡片墙,一套代码自动切换。 新手最容易混淆的概念在这里讲清楚。
两个维度(务必分清)
| 概念 | 手段 | 解决什么 |
|---|---|---|
| 屏幕适配 | flutter_screenutil(.w/.h/.sp) |
不同屏幕比例不变形(尺寸等比缩放) |
| 响应式布局 | 断点 + LayoutBuilder(本 Demo) | 不同屏幕宽度改变布局结构(单列/双列) |
两者配合使用:尺寸用
.w/.h/.sp缩放,结构用断点切换。
二、怎么运行 / 演示内容
runApp(const MaterialApp(home: ResponsiveDemoPage()));
- 顶部模式条实时显示"手机模式 / Pad 模式 + 当前宽度 dp";
- 手机(<600dp)→ 单列 ListTile 列表;
- Pad(≥600dp)→ 多列卡片网格(列数按宽度自适应)。
三、核心知识点(新手向)
1. 断点标准(Material 官方)
| 宽度 | 设备 |
|---|---|
| < 600dp | 手机 |
| 600 ~ 839dp | 小平板 / 折叠屏 |
| >= 840dp | 大平板 / 桌面 |
⚠ 关键:判断的是屏幕宽度,不是 Platform! iOS 也有 iPad,Android 也有平板 —— 用平台判断永远分不清它们。
2. 两种拿宽度的方式
MediaQuery.sizeOf(context).width // 整个屏幕宽度
LayoutBuilder(...) // 父容器实际给你的可用宽度
- 页面铺满屏幕时两者一样;
- 页面内有可伸缩区域(侧栏收起/展开)时必须用
LayoutBuilder; - 本 Demo 用
LayoutBuilder,因为能拿到最真实的可用宽度。
3. 核心切换逻辑
LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
final isTablet = width >= _tabletBreakpoint; // 600dp
// 列数自适应:每列至少 280dp 宽,最多 4 列
final columns = (width / _minCardWidth).floor().clamp(1, 4);
return isTablet
? _buildGrid(columns) // Pad:多列网格
: _buildList(); // 手机:单列列表
},
)
四、关键代码剖析
// 手机列表项
ListView.separated(
padding: EdgeInsets.all(12.w),
itemCount: _items.length,
separatorBuilder: (_, _) => SizedBox(height: 8.h),
itemBuilder: (context, i) => _ListTileItem(item: _items[i]),
)
// Pad 卡片网格(同一个数据源 _items,只是换了一种展示容器)
GridView.builder(
padding: EdgeInsets.all(12.w),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns, // 由宽度算出来的列数
mainAxisSpacing: 12.w,
crossAxisSpacing: 12.w,
childAspectRatio: 1.4, // 宽高比,数字大了卡片变矮
),
itemCount: _items.length,
itemBuilder: (context, i) => _CardItem(item: _items[i]),
)
关键思想:数据不变,变的是"容器" —— 同一份 _items,手机渲染成列表项,Pad 渲染成卡片。
组件只负责"怎么显示一条数据",页面负责"按宽度选容器"。
五、踩坑记录
- 别用
Platform.isAndroid判断手机/Pad —— 永远用宽度断点; MediaQuery.sizeOf(context).width拿的是全屏宽,页面嵌在可伸缩容器里会算错 —— 用 LayoutBuilder;(width / 280).floor()在窄屏可能算出 0 列 —— 必须.clamp(1, 4)兜底;- 断点值用常量集中管理(
_tabletBreakpoint = 600),别散落在各页面写死魔法数字。
六、扩展思考
- 官方推荐:
flutter/foundation的defaultTargetPlatform+ 宽度断点结合;或引入responsive_builder包; - 平板横竖屏切换:竖屏 8.4" ≈ 800dp,横屏 1280dp —— 断点要覆盖两种情况;
- 进阶模式:
LayoutBuilder+CustomScrollView(sliver_demo.md)组合做"响应式长列表"; - 本项目 theme_demo 的
PageTheme按页面层级给配色,响应式按宽度给结构 —— 两个维度正交,可自由组合。