Files
App.K12.Student.Miniapp_Flu…/docs/demos/responsive_demo.md
T
2026-09-04 15:22:20 +08:00

119 lines
4.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 【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` 按页面层级给配色,响应式按宽度给结构 —— 两个维度正交,可自由组合。