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

4.2 KiB
Raw Blame History

【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 渲染成卡片。 组件只负责"怎么显示一条数据",页面负责"按宽度选容器"。


五、踩坑记录

  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)组合做"响应式长列表";
  • 本项目 theme_demo 的 PageTheme 按页面层级给配色,响应式按宽度给结构 —— 两个维度正交,可自由组合。