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