《Flutter全栈开发实战指南:从零到高级》- 21 -响应式设计与适配

101 阅读10分钟

引言

当我们谈论"一次编写,处处运行"时,真正考验开发者的是:如何在不同尺寸、不同形态的设备上都能让用户拥有丝滑的体验?今天,我们将从底层原理出发一起深入Flutter响应式设计的核心。

一:屏幕适配的本质

1.1 设备像素

实际是从物理到逻辑的映射,让我们来看一个现象:为什么1920×1080的手机屏幕比同样分辨率的电视更清晰?

// 像素密度
void demonstratePixelLogic() {
  // 以iPhone 13 Pro为例:
  // - 物理尺寸:5.4英寸
  // - 物理分辨率:2532×1170像素
  // - 像素密度:460 PPI
  // 这意味着:每英寸有460个物理像素点,Flutter设计的巧妙之处是它为你创建了一个虚拟画布,
  // 你再上面用逻辑像素绘图,Flutter引擎负责将逻辑像素转换为物理像素
}

像素映射关系图

flowchart LR
    subgraph 输入源
        Hardware["设备硬件<br/>2532×1170物理像素"]
    end
    
    subgraph 转换过程
        Ratio["devicePixelRatio<br/>= 3.0"]
        Formula["计算公式<br/>逻辑 = 物理 ÷ Ratio"]
    end
    
    subgraph 框架处理
        Flutter["Flutter框架<br/>执行像素转换"]
        MediaQuery["MediaQuery API<br/>封装逻辑尺寸"]
    end
    
    subgraph 输出结果
        Context["BuildContext<br/>包含尺寸信息"]
        Code["开发者代码<br/>Container(width: 100)"]
        Render["实际渲染<br/>300×300物理像素"]
    end
    
    Hardware --> Ratio
    Ratio --> Formula
    Formula --> Flutter
    Flutter --> MediaQuery
    MediaQuery --> Context
    Context --> Code
    Code --> Render
    
    style Hardware fill:#ffcdd2
    style Ratio fill:#ffccbc
    style Formula fill:#ffe0b2
    style Flutter fill:#c8e6c9
    style MediaQuery fill:#b2ebf2
    style Context fill:#bbdefb
    style Code fill:#e1bee7
    style Render fill:#d1c4e9

1.2 适配方案

不要盲目选择适配方案,要先理解每种方案的原理:

// 方案分析:百分比、缩放、断点
class AdaptationMath {
  // 1. 百分比方案
  static double percentageAdaptation(double screenWidth, double percentage) {
    // 公式:实际宽度 = 屏幕宽度 × 百分比
    // 优点:简单直接
    // 缺点:线性关系,无法处理非线性需求
    return screenWidth * percentage;
  }
  
  // 2. 缩放方案
  static double scaleAdaptation(
    double designValue, 
    double screenWidth, 
    double designWidth
  ) {
    // 公式:实际值 = 设计值 × (屏幕宽度 / 设计基准宽度)
    // designWidth ──────────────── designValue
    //      │                           │
    //      │ 缩放比例 = screenWidth/designWidth
    //      │                           │
    //      ↓                           ↓
    // screenWidth ─────────────── 实际值
    return designValue * (screenWidth / designWidth);
  }
  
  // 3. 断点方案
  static String breakpointAdaptation(double screenWidth) {
    // 分段函数
    // f(x) = {
    //   手机布局,  x < 600
    //   平板布局,  600 ≤ x < 900
    //   桌面布局,  x ≥ 900
    // }
    if (screenWidth < 600) return 'mobile';
    if (screenWidth < 900) return 'tablet';
    return 'desktop';
  }
}

适配方案选择

stateDiagram-v2
    [*] --> Initialization : 开始适配
    
    state Initialization {
        [*] --> Analyzing
        Analyzing --> TypeDetermined : 识别应用类型
    }
    
    TypeDetermined --> ContentApp : 内容型应用
    TypeDetermined --> ToolApp : 工具型应用
    TypeDetermined --> ComplexApp : 复杂型应用
    
    state ContentApp {
        [*] --> PercentBased
        PercentBased --> ContentReason : 流式填充需求
        ContentReason --> PrecisionCheck1
    }
    
    state ToolApp {
        [*] --> ScaleBased
        ScaleBased --> ToolReason : 设计稿还原需求
        ToolReason --> PrecisionCheck2
    }
    
    state ComplexApp {
        [*] --> BreakpointBased
        BreakpointBased --> ComplexReason : 多交互模式需求
        ComplexReason --> PrecisionCheck3
    }
    
    PrecisionCheck1 --> MixedStrategy : 需要像素级精确
    PrecisionCheck2 --> MixedStrategy : 需要像素级精确
    PrecisionCheck3 --> MixedStrategy : 需要像素级精确
    
    PrecisionCheck1 --> PureStrategy : 不需要像素级精确
    PrecisionCheck2 --> PureStrategy : 不需要像素级精确
    PrecisionCheck3 --> PureStrategy : 不需要像素级精确
    
    MixedStrategy --> Implementation
    PureStrategy --> Implementation
    
    state Implementation {
        [*] --> Coding
        Coding --> Testing : 代码完成
        Testing --> Evaluating : 测试运行
    }
    
    Evaluating --> Completed : 测试通过
    Evaluating --> ReEvaluation : 测试失败
    
    Completed --> [*] : 流程结束
    ReEvaluation --> Analyzing : 重新分析

二:横竖屏切换

2.1 横竖屏原理

为什么横竖屏切换不是简单的宽高交换?

// 理解横竖屏的差异
class OrientationPhysics {
  // 竖屏
  static Map<String, dynamic> portraitCharacteristics() {
    return {
      '握持方式': '单手或双手纵向握持',
      '视觉焦点': '垂直方向滚动为主',
      '交互区域': '屏幕底部60%最易操作',
      '典型场景': '浏览、阅读、聊天',
      '用户注意力': '集中在上半部分',
    };
  }
  
  // 横屏
  static Map<String, dynamic> landscapeCharacteristics() {
    return {
      '握持方式': '双手横向握持',
      '视觉焦点': '水平方向扩展',
      '交互区域': '两侧和中间区域',
      '典型场景': '游戏、视频、多任务',
      '用户注意力': '分散在整个屏幕',
    };
  }
}

横竖屏状态

stateDiagram-v2
    [*] --> Portrait: 应用启动
    
    Portrait --> Landscape: 设备旋转90°
    Landscape --> Portrait: 设备旋转-90°
    
    state Portrait {
        [*] --> P_LayoutBuilt: 构建竖屏布局
        P_LayoutBuilt --> P_UserInteracting: 用户交互
        P_UserInteracting --> P_StatePreserved: 保存状态
        P_StatePreserved --> P_LayoutBuilt: 界面更新
        
        note right of P_UserInteracting
            竖屏交互特点:
            - 拇指在底部操作
            - 垂直滚动为主
            - 信息层级深度优先
        end note
    }
    
    state Landscape {
        [*] --> L_LayoutBuilt: 构建横屏布局
        L_LayoutBuilt --> L_UserInteracting: 用户交互
        L_UserInteracting --> L_StatePreserved: 保存状态
        L_StatePreserved --> L_LayoutBuilt: 界面更新
        
        note left of L_UserInteracting
            横屏交互特点:
            - 双手操作
            - 水平空间扩展
            - 信息广度优先
        end note
    }
    
    note right of Portrait
        竖屏设计原则:
        1. 重要内容在上1/3
        2. 操作按钮在底部
        3. 利用垂直空间
    end note
    
    note left of Landscape
        横屏设计原则:
        1. 左右平衡布局
        2. 避免内容拉伸
        3. 利用水平空间
    end note

2.2 状态保持原理

为什么有的应用旋转后数据会丢失?

// 实现状态保持
class StatePreservationDemo extends StatefulWidget {
  @override
  _StatePreservationDemoState createState() => _StatePreservationDemoState();
}

class _StatePreservationDemoState extends State<StatePreservationDemo> 
    with WidgetsBindingObserver, RestorationMixin {
  // 关键点1:RestorationMixin的机制
  // 当屏幕旋转时,Flutter会:
  // 1. 销毁当前Widget树
  // 2. 重新创建新的Widget树
  // 3. 恢复之前保存的状态
  
  final RestorableInt _counter = RestorableInt(0);
  final RestorableDouble _scrollPosition = RestorableDouble(0.0);
  final RestorableTextEditingController _textController = 
      RestorableTextEditingController();
  
  @override
  String get restorationId => 'state_preservation_demo';
  
  @override
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    // 注册需要恢复的状态
    registerForRestoration(_counter, 'counter');
    registerForRestoration(_scrollPosition, 'scroll_position');
    registerForRestoration(_textController, 'text_controller');
  }
  
  // 关键点2:WidgetsBindingObserver的生命周期
  @override
  void didChangeMetrics() {
    // 屏幕旋转、键盘弹出等都会触发
    super.didChangeMetrics();
    
    final orientation = MediaQuery.of(context).orientation;
    print('屏幕方向变化: $orientation');
    
    // 注意:这里setState会触发重建
    // 但状态已经被RestorationMixin保存
  }
  
  @override
  void dispose() {
    _counter.dispose();
    _scrollPosition.dispose();
    _textController.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        controller: ScrollController(initialScrollOffset: _scrollPosition.value),
        slivers: [
          SliverAppBar(
            title: Text('状态保持 - ${_counter.value}'),
            floating: true,
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (context, index) {
                return ListTile(
                  title: TextField(
                    controller: _textController.value,
                    decoration: InputDecoration(
                      hintText: '输入内容,旋转后不会丢失',
                    ),
                  ),
                  trailing: IconButton(
                    icon: Icon(Icons.add),
                    onPressed: () {
                      setState(() {
                        _counter.value++;
                      });
                    },
                  ),
                );
              },
              childCount: 20,
            ),
          ),
        ],
      ),
    );
  }
}

下面我用一张图来加深理解状态保持的原理

sequenceDiagram
    participant U as 用户
    participant D as 设备传感器
    participant FW as Flutter框架
    participant WM as Widget树管理器
    participant SM as 状态管理器
    participant R as Restoration系统
    
    U->>D: 旋转设备
    D->>FW: 发送orientationChange事件
    FW->>WM: 触发didChangeMetrics
    
    Note over WM,R: 关键阶段1:保存状态
    WM->>SM: 请求保存当前状态
    SM->>R: 序列化状态数据
    R-->>SM: 返回状态ID
    SM-->>WM: 确认状态保存
    
    Note over WM,R: 关键阶段2:销毁旧Widget树
    WM->>WM: 标记旧Widget为dispose
    WM->>WM: 清理Element树
    
    Note over WM,R: 关键阶段3:重建新Widget树
    WM->>WM: 根据新constraints重建
    WM->>R: 请求恢复状态
    R-->>WM: 返回序列化的状态数据
    WM->>SM: 反序列化恢复状态
    SM-->>WM: 状态恢复完成
    
    WM->>FW: 重建完成,更新UI
    FW->>U: 显示旋转后的界面<br/>(状态完全保留)

三:Pad适配

3.1 平板的特性

平板不是大号手机,它有独特的交互模式:

// 平板交互
class TabletInteractionPattern {
  static void analyzeTabletCharacteristics(BuildContext context) {
    final size = MediaQuery.of(context).size;
    final diagonal = _calculateDiagonalInches(context);
    
    print('''
平板交互分析:
=====================
1. 尺寸特性:
   - 屏幕对角线: ${diagonal.toStringAsFixed(1)} 英寸
   - 宽高比: ${(size.width / size.height).toStringAsFixed(2)}
   - 手持距离: 通常30-50cm

2. 交互特性:
   - 操作方式: 双手 + 可能的外接键盘
   - 触控精度: 比手机低(手指更粗)
''');
  }
  
  // 平板布局架构
  static Widget buildTabletArchitecture({
    required Widget navigation,
    required Widget primaryContent,
    required Widget secondaryContent,
    required Widget utilityPanel,
  }) {
    // 三栏布局
    return Scaffold(
      body: Row(
        children: [
          // 左侧导航栏
          Container(
            width: 72, 
            decoration: BoxDecoration(
              border: Border(right: BorderSide(color: Colors.grey.shade300)),
            ),
            child: navigation,
          ),
          
          // 主内容区
          Expanded(
            flex: 3,
            child: Column(
              children: [
                // 顶部工具栏
                Container(
                  height: 56,
                  decoration: BoxDecoration(
                    border: Border(bottom: BorderSide(color: Colors.grey.shade300)),
                  ),
                  child: utilityPanel,
                ),
                // 主内容
                Expanded(
                  child: Row(
                    children: [
                      // 主内容列表
                      Expanded(
                        flex: 2,
                        child: primaryContent,
                      ),
                      // 详情面板
                      Expanded(
                        flex: 3,
                        child: secondaryContent,
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
          
          // 右侧工具面板
          Container(
            width: 320,
            decoration: BoxDecoration(
              border: Border(left: BorderSide(color: Colors.grey.shade300)),
            ),
            child: utilityPanel,
          ),
        ],
      ),
    );
  }
  
  static double _calculateDiagonalInches(BuildContext context) {
    final size = MediaQuery.of(context).size;
    final pixelRatio = MediaQuery.of(context).devicePixelRatio;
    
    // 转换为物理尺寸
    final widthInches = size.width / pixelRatio / 96; 
    final heightInches = size.height / pixelRatio / 96;
    
    // 计算对角线
    return sqrt(pow(widthInches, 2) + pow(heightInches, 2));
  }
}

平板适配

22.png

3.2 主从布局

// 主从布局
class MasterDetailLayout extends StatefulWidget {
  final List<Item> items;
  final Widget Function(Item?) detailBuilder;
  
  const MasterDetailLayout({
    Key? key,
    required this.items,
    required this.detailBuilder,
  }) : super(key: key);
  
  @override
  _MasterDetailLayoutState createState() => _MasterDetailLayoutState();
}

class _MasterDetailLayoutState extends State<MasterDetailLayout> {
  Item? _selectedItem;
  bool _isTablet = false;
  
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // 检测是否为平板
    _isTablet = MediaQuery.of(context).size.width > 600;
  }
  
  // 布局切换
  Widget _buildLayout() {
    if (!_isTablet) {
      // 手机模式
      return _buildMobileLayout();
    } else {
      // 平板模式
      return _buildTabletLayout();
    }
  }
  
  Widget _buildMobileLayout() {
    if (_selectedItem == null) {
      // 状态1:显示主列表
      return _buildMasterList();
    } else {
      // 状态2:显示详情,带返回按钮
      return Scaffold(
        appBar: AppBar(
          leading: IconButton(
            icon: Icon(Icons.arrow_back),
            onPressed: () {
              setState(() {
                _selectedItem = null;
              });
            },
          ),
          title: Text(_selectedItem!.title),
        ),
        body: widget.detailBuilder(_selectedItem),
      );
    }
  }
  
  Widget _buildTabletLayout() {
    // 平板模式
    return Scaffold(
      appBar: AppBar(
        title: Text(_selectedItem?.title ?? '请选择项目'),
      ),
      body: Row(
        children: [
          // 左侧主列表
          Container(
            width: 320,
            decoration: BoxDecoration(
              border: Border(right: BorderSide(color: Colors.grey.shade300)),
              color: Colors.grey.shade50,
            ),
            child: _buildMasterList(),
          ),
          
          // 右侧详情
          Expanded(
            child: _selectedItem == null
                ? Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Icon(Icons.touch_app, size: 64, color: Colors.grey.shade300),
                        SizedBox(height: 16),
                        Text(
                          '选择左侧项目查看详情',
                          style: TextStyle(color: Colors.grey.shade500),
                        ),
                      ],
                    ),
                  )
                : widget.detailBuilder(_selectedItem),
          ),
        ],
      ),
    );
  }
  
  Widget _buildMasterList() {
    return ListView.builder(
      itemCount: widget.items.length,
      itemBuilder: (context, index) {
        final item = widget.items[index];
        final isSelected = _selectedItem?.id == item.id;
        
        return ListTile(
          title: Text(item.title),
          subtitle: Text(item.subtitle),
          leading: Icon(item.icon),
          trailing: _isTablet && isSelected
              ? Icon(Icons.chevron_right, color: Colors.blue)
              : null,
          tileColor: isSelected ? Colors.blue.shade50 : null,
          onTap: () {
            setState(() {
              _selectedItem = item;
            });
            
            // 如果是手机,需要导航到详情页
            if (!_isTablet) {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => Scaffold(
                    appBar: AppBar(
                      leading: BackButton(
                        onPressed: () {
                          Navigator.pop(context);
                          setState(() {
                            _selectedItem = null;
                          });
                        },
                      ),
                      title: Text(item.title),
                    ),
                    body: widget.detailBuilder(item),
                  ),
                ),
              );
            }
          },
        );
      },
    );
  }
  
  @override
  Widget build(BuildContext context) {
    return _buildLayout();
  }
}

四:响应式布局框架

4.1 搭建响应式框架

// 响应式实现
abstract class ResponsiveBreakpoint {
  static const double xs = 360;  // 手机
  static const double sm = 600;  // 平板
  static const double md = 900;  // 桌面
  static const double lg = 1200; // 大桌面
  static const double xl = 1536; // 超大屏幕
}

// 响应式配置
class ResponsiveConfig {
  final Map<ScreenSize, LayoutConfig> configs;
  
  ResponsiveConfig({
    required this.configs,
  });
  
  factory ResponsiveConfig.defaultConfig() {
    return ResponsiveConfig(
      configs: {
        ScreenSize.xs: LayoutConfig(
          columns: 4,
          gutter: 16,
          margin: 16,
          maxWidth: null,
        ),
        ScreenSize.sm: LayoutConfig(
          columns: 8,
          gutter: 24,
          margin: 24,
          maxWidth: null,
        ),
        ScreenSize.md: LayoutConfig(
          columns: 12,
          gutter: 32,
          margin: 32,
          maxWidth: 1200,
        ),
        ScreenSize.lg: LayoutConfig(
          columns: 12,
          gutter: 32,
          margin: 32,
          maxWidth: 1400,
        ),
        ScreenSize.xl: LayoutConfig(
          columns: 12,
          gutter: 32,
          margin: 48,
          maxWidth: null,
        ),
      },
    );
  }
}

// 布局配置
class LayoutConfig {
  final int columns;      // 网格列数
  final double gutter;    // 列间距
  final double margin;    // 边距
  final double? maxWidth; // 最大宽度
  
  LayoutConfig({
    required this.columns,
    required this.gutter,
    required this.margin,
    this.maxWidth,
  });
  
  // 计算列宽
  double columnWidth(double availableWidth) {
    final totalGutter = gutter * (columns - 1);
    final contentWidth = availableWidth - (margin * 2);
    return (contentWidth - totalGutter) / columns;
  }
}

// 响应式构建
class ResponsiveBuilder extends StatelessWidget {
  final Widget Function(
    BuildContext context,
    ScreenSize size,
    LayoutConfig config,
  ) builder;
  
  final ResponsiveConfig config;
  
  const ResponsiveBuilder({
    Key? key,
    required this.builder,
    this.config = const ResponsiveConfig.defaultConfig(),
  }) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final screenSize = _getScreenSize(constraints.maxWidth);
        final layoutConfig = config.configs[screenSize]!;
        
        return builder(context, screenSize, layoutConfig);
      },
    );
  }
  
  ScreenSize _getScreenSize(double width) {
    if (width < ResponsiveBreakpoint.xs) return ScreenSize.xs;
    if (width < ResponsiveBreakpoint.sm) return ScreenSize.sm;
    if (width < ResponsiveBreakpoint.md) return ScreenSize.md;
    if (width < ResponsiveBreakpoint.lg) return ScreenSize.lg;
    return ScreenSize.xl;
  }
}

响应式框架的架构图

graph LR
    subgraph "应用层"
        direction LR
        A[业务页面]
        B[业务组件]
    end
    
    subgraph "响应式框架层"
    	    direction LR
        C[ResponsiveBuilder]
        D[ResponsiveConfig]
        E[LayoutConfig]
        F[Breakpoint系统]
    end
    
    subgraph "Flutter框架层"
        direction LR
        G[LayoutBuilder]
        H[MediaQuery]
        I[Constraints]
    end
    
    subgraph "设备层"
        direction LR
        J[物理设备]
        K[屏幕传感器]
    end
    
    J --> K
    K --> H
    H --> I
    I --> G
    G --> C
    C --> D
    D --> E
    F --> C
    C --> A
    C --> B
    
    style A fill:#e1f5fe
    style B fill:#e1f5fe
    style C fill:#f3e5f5
    style D fill:#f3e5f5
    style E fill:#f3e5f5
    style F fill:#f3e5f5
    style G fill:#e8f5e8
    style H fill:#e8f5e8
    style I fill:#e8f5e8
    style J fill:#fff3e0
    style K fill:#fff3e0

4.2 网格系统

为什么网格系统是响应式设计的核心?

// 实现网格系统
class GridSystem {
  // 黄金比例
  static const double goldenRatio = 1.61803398875;
  
  // 计算黄金比例列宽
  static List<double> goldenColumns(double totalWidth, int columns) {
    final widths = <double>[];
    double remainingWidth = totalWidth;
    
    for (int i = 0; i < columns; i++) {
      if (i == columns - 1) {
        widths.add(remainingWidth);
      } else {
        // 黄金分割:当前列宽 = 剩余宽度 / (1 + φ)
        final width = remainingWidth / (1 + goldenRatio);
        widths.add(width);
        remainingWidth -= width;
      }
    }
    
    return widths;
  }
  
  // 8pt网格系统
  static double snapToGrid(double value) {
    // 8的倍数
    return (value / 8).round() * 8.0;
  }
  
  // 流体网格计算
  static double fluidValue({
    required double minValue,
    required double maxValue,
    required double minScreenWidth,
    required double maxScreenWidth,
    required double currentScreenWidth,
  }) {
    // 线性插值公式:
    // value = minValue + (current - minScreen) * (maxValue - minValue) / (maxScreen - minScreen)
    
    if (currentScreenWidth <= minScreenWidth) return minValue;
    if (currentScreenWidth >= maxScreenWidth) return maxValue;
    
    final progress = (currentScreenWidth - minScreenWidth) / 
                    (maxScreenWidth - minScreenWidth);
    
    return minValue + (maxValue - minValue) * progress;
  }
  
  // 响应式间距计算
  static EdgeInsets responsivePadding({
    required BuildContext context,
    double xs = 16,
    double sm = 24,
    double md = 32,
    double lg = 40,
    double xl = 48,
  }) {
    final width = MediaQuery.of(context).size.width;
    
    double padding;
    if (width < ResponsiveBreakpoint.xs) {
      padding = xs;
    } else if (width < ResponsiveBreakpoint.sm) {
      padding = sm;
    } else if (width < ResponsiveBreakpoint.md) {
      padding = md;
    } else if (width < ResponsiveBreakpoint.lg) {
      padding = lg;
    } else {
      padding = xl;
    }
    
    return EdgeInsets.all(padding);
  }
}

五:响应式架构案例

5.1 以电商为例:从需求到实现

下面我们来设计一个电商App响应式架构:

// 电商App的响应式架构
class ECommerceArchitecture {
  // 1. 定义断点
  static const Map<ScreenSize, ECommerceLayoutStrategy> strategies = {
    ScreenSize.xs: MobileLayoutStrategy(),
    ScreenSize.sm: TabletLayoutStrategy(),
    ScreenSize.md: DesktopLayoutStrategy(),
    ScreenSize.lg: DesktopLayoutStrategy(),
    ScreenSize.xl: WideDesktopLayoutStrategy(),
  };
  
  // 2. 核心页面
  static Widget buildProductListingPage({
    required List<Product> products,
    required FilterState filterState,
    required CartState cartState,
  }) {
    return ResponsiveBuilder(
      builder: (context, screenSize, layoutConfig) {
        final strategy = strategies[screenSize]!;
        
        return strategy.buildProductListing(
          context: context,
          products: products,
          filterState: filterState,
          cartState: cartState,
          layoutConfig: layoutConfig,
        );
      },
    );
  }
  
  // 3. 产品详情页
  static Widget buildProductDetailPage({
    required Product product,
    required List<Product> relatedProducts,
    required ReviewState reviewState,
  }) {
    return Scaffold(
      appBar: ResponsiveAppBar(
        title: product.name,
        showBackButton: true,
        actions: _buildAppBarActions(screenSize),
      ),
      body: ResponsiveBuilder(
        builder: (context, screenSize, layoutConfig) {
          if (screenSize == ScreenSize.xs || screenSize == ScreenSize.sm) {
            return _buildMobileProductDetail(
              product,
              relatedProducts,
              reviewState,
            );
          } else {
            return _buildDesktopProductDetail(
              product,
              relatedProducts,
              reviewState,
              layoutConfig,
            );
          }
        },
      ),
      bottomNavigationBar: ResponsiveBuilder(
        builder: (context, screenSize, _) {
          if (screenSize == ScreenSize.xs) {
            return _buildMobileBottomBar(product);
          }
          return const SizedBox.shrink();
        },
      ),
    );
  }
}

// 模式实现
abstract class ECommerceLayoutStrategy {
  Widget buildProductListing({
    required BuildContext context,
    required List<Product> products,
    required FilterState filterState,
    required CartState cartState,
    required LayoutConfig layoutConfig,
  });
}

class MobileLayoutStrategy implements ECommerceLayoutStrategy {
  @override
  Widget buildProductListing({
    required BuildContext context,
    required List<Product> products,
    required FilterState filterState,
    required CartState cartState,
    required LayoutConfig layoutConfig,
  }) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('商品列表'),
        actions: [
          IconButton(
            icon: const Icon(Icons.filter_list),
            onPressed: () => _showFilterSheet(context, filterState),
          ),
        ],
      ),
      body: CustomScrollView(
        slivers: [
          // 搜索栏
          const SliverPadding(
            padding: EdgeInsets.all(16),
            sliver: SliverToBoxAdapter(
              child: SearchBar(),
            ),
          ),
          
          // 分类标签
          SliverPadding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            sliver: SliverToBoxAdapter(
              child: CategoryChips(
                selectedCategory: filterState.selectedCategory,
                onCategorySelected: filterState.selectCategory,
              ),
            ),
          ),
          
          // 商品网格
          SliverPadding(
            padding: EdgeInsets.all(layoutConfig.margin),
            sliver: SliverGrid(
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                crossAxisSpacing: layoutConfig.gutter,
                mainAxisSpacing: layoutConfig.gutter,
                childAspectRatio: 0.75,
              ),
              delegate: SliverChildBuilderDelegate(
                (context, index) {
                  return ProductCard(
                    product: products[index],
                    onAddToCart: () => cartState.addItem(products[index]),
                    compact: true,
                  );
                },
                childCount: products.length,
              ),
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _goToCart(context, cartState),
        child: Badge(
          count: cartState.itemCount,
          child: const Icon(Icons.shopping_cart),
        ),
      ),
    );
  }
}

电商App响应式架构的组件图

graph TB
    subgraph "用户界面层"
        A1[ProductListingPage]
        A2[ProductDetailPage]
        A3[CartPage]
        A4[CheckoutPage]
    end
    
    subgraph "响应式组件层"
        B1[ResponsiveAppBar]
        B2[ResponsiveProductGrid]
        B3[ResponsiveProductCard]
        B4[ResponsiveNavigation]
    end
    
    subgraph "布局策略层"
        C1[MobileLayoutStrategy]
        C2[TabletLayoutStrategy]
        C3[DesktopLayoutStrategy]
        C4[WideDesktopStrategy]
    end
    
    subgraph "状态管理层"
        D1[ProductState]
        D2[CartState]
        D3[FilterState]
        D4[UIState]
    end
    
    subgraph "数据层"
        E1[ProductRepository]
        E2[CartRepository]
        E3[UserRepository]
    end
    
    A1 --> B2
    A1 --> B1
    A2 --> B3
    A3 --> B4
    
    B1 --> C1
    B1 --> C2
    B1 --> C3
    B1 --> C4
    
    B2 --> C1
    B2 --> C2
    B2 --> C3
    B2 --> C4
    
    C1 --> D1
    C1 --> D2
    C1 --> D3
    C1 --> D4
    
    D1 --> E1
    D2 --> E2
    D3 --> E1
    D4 --> E3
    
    style A1 fill:#e3f2fd
    style A2 fill:#e3f2fd
    style A3 fill:#e3f2fd
    style A4 fill:#e3f2fd
    style B1 fill:#f3e5f5
    style B2 fill:#f3e5f5
    style B3 fill:#f3e5f5
    style B4 fill:#f3e5f5
    style C1 fill:#e8f5e8
    style C2 fill:#e8f5e8
    style C3 fill:#e8f5e8
    style C4 fill:#e8f5e8
    style D1 fill:#fff3e0
    style D2 fill:#fff3e0
    style D3 fill:#fff3e0
    style D4 fill:#fff3e0
    style E1 fill:#ffebee
    style E2 fill:#ffebee
    style E3 fill:#ffebee

六:性能优化

6.1 常见问题与优化方法

// 性能优化
class ResponsivePerformance {
  // 问题1:过度重建
  static Widget avoidOverRebuild(BuildContext context) {
    // 错误做法:直接在build中计算
    // Widget build(BuildContext context) {
    //   final isTablet = MediaQuery.of(context).size.width > 600;
    //   // 每次build都重新计算
    // }
    
    // 正确做法:使用StatefulWidget缓存
    return _PerformanceOptimizedWidget();
  }
  
  // 问题2:布局计算
  static Widget optimizeLayoutCalculations() {
    // 错误做法:在build中做复杂计算
    // Widget build(BuildContext context) {
    //   final itemWidth = (MediaQuery.of(context).size.width - 32) / 3;
    //   // 每次build都重新计算
    // }
    
    // 正确做法:使用LayoutBuilder延迟计算
    return LayoutBuilder(
      builder: (context, constraints) {
        // 只在约束变化时计算
        final itemWidth = (constraints.maxWidth - 32) / 3;
        
        return GridView.builder(
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3,
            childAspectRatio: 0.75,
            mainAxisSpacing: 8,
            crossAxisSpacing: 8,
          ),
          itemCount: 100,
          itemBuilder: (context, index) {
            // 使用const减少重建
            return const ProductCard();
          },
        );
      },
    );
  }
  
  // 优化技巧:预计算布局信息
  static class LayoutCache {
    static final Map<String, Map<String, double>> _cache = {};
    
    static double getCachedValue(
      BuildContext context,
      String key,
      double Function() calculate,
    ) {
      final sizeKey = '${MediaQuery.of(context).size.width}x${MediaQuery.of(context).size.height}';
      
      if (!_cache.containsKey(sizeKey)) {
        _cache[sizeKey] = {};
      }
      
      if (!_cache[sizeKey]!.containsKey(key)) {
        _cache[sizeKey]![key] = calculate();
      }
      
      return _cache[sizeKey]![key]!;
    }
  }
}

// 性能优化的Widget实现
class _PerformanceOptimizedWidget extends StatefulWidget {
  @override
  __PerformanceOptimizedWidgetState createState() => __PerformanceOptimizedWidgetState();
}

class __PerformanceOptimizedWidgetState extends State<_PerformanceOptimizedWidget> {
  late bool _isTablet;
  late double _cachedItemWidth;
  
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    
    // 只在屏幕尺寸变化时更新
    final isTabletNow = MediaQuery.of(context).size.width > 600;
    if (_isTablet != isTabletNow) {
      setState(() {
        _isTablet = isTabletNow;
        _cachedItemWidth = _calculateItemWidth();
      });
    }
  }
  
  double _calculateItemWidth() {
    final width = MediaQuery.of(context).size.width;
    return _isTablet ? (width - 48) / 4 : (width - 32) / 2;
  }
  
  @override
  Widget build(BuildContext context) {
    // 使用缓存值,避免重复计算
    return GridView.builder(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: _isTablet ? 4 : 2,
        childAspectRatio: 0.75,
        mainAxisSpacing: 8,
        crossAxisSpacing: 8,
      ),
      itemCount: 100,
      itemBuilder: (context, index) {
        return const ProductCard(); 
      },
    );
  }
}

总结

至此响应式涉及与适配就全部介绍完了,通过本篇文章的深度探讨,我们总结出响应式设计的几个核心原则:

  • 内容优先:设计围绕内容,而非设备
  • 移动优先:从小屏幕开始,逐步增强
  • 相对单位:避免绝对像素,使用相对单位
  • 断点内容:基于内容需要设置断点
  • 渐进增强:为大屏添加功能,不为小屏削减功能

知识点

主题概念最佳实践常见问题
像素适配逻辑像素 vs 物理像素使用MediaQuery获取逻辑尺寸硬编码物理像素值
横竖屏方向感知布局使用OrientationBuilder忽略状态保持
平板适配主从布局模式策略模式实现不同布局把平板当大手机
响应框架断点系统基于内容的断点设计基于设备的断点
性能优化重建控制缓存计算结果,使用const频繁触发重建

响应式设计不是一项功能,而是一种思维方式。它要求我们从用户的实际使用场景出发,考虑他们如何与我们的应用交互。最好的响应式设计是用户感受不到的。用户不会说"这个响应式做得真好",他们会说"这个应用用起来真舒服"。

如果这篇文章对你有帮助,别忘了一键三连(点赞、关注、收藏)支持一下吧!!!有问题欢迎在评论区交流讨论~