You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter响应式页面开发求助:无需MediaQuery的适配方案

Flutter响应式开发解决方案(禁用MediaQuery)

问题核心

当前代码里大量使用固定尺寸(如height:200、buttonSize: Size(100,60)),这是响应式失效的关键。禁用MediaQuery.of(context).size的情况下,我们可以通过基于父组件约束的百分比布局、LayoutBuilder、FractionallySizedBox等组件实现自适应,同时结合断点逻辑切换布局结构。

修改后的核心代码示例

以下是针对你的代码的关键修改点,替换原固定尺寸逻辑:

import 'package:flutter/material.dart';
import 'package:auto_size_text/auto_size_text.dart';

import "package:simple_coffee/models/buttons/ButtonsRow.dart";
import "package:simple_coffee/models/cards/CustomCard.dart";

import "package:simple_coffee/data/products_lists.dart";

import 'package:provider/provider.dart';
import 'package:simple_coffee/shared/providers/profile_information_cache.dart';

class HomeResponsive extends StatefulWidget {
  const HomeResponsive({Key? key}) : super(key: key);

  @override
  State<HomeResponsive> createState() => _HomeResponsiveState();
}

class _HomeResponsiveState extends State<HomeResponsive> {
  String selectedButton = "Cappuccinos";
  bool _passed = false;

  // 自定义断点:根据宽度区分设备区间
  bool isSmallScreen(double width) => width < 600;
  bool isMediumScreen(double width) => width >=600 && width < 1024;
  bool isLargeScreen(double width) => width >=1024;

  @override
  Widget build(BuildContext context) {
    final profileInformationCache = Provider.of<ProfileInformationCache>(context);

    if (!_passed) {
      profileInformationCache.loadUser();
      _passed = true;
    }

    return Scaffold(
      backgroundColor: Colors.white,
      // 用LayoutBuilder获取父组件约束,替代MediaQuery
      body: LayoutBuilder(
        builder: (context, constraints) {
          final screenWidth = constraints.maxWidth;
          final screenHeight = constraints.maxHeight;

          return Column(
            children: <Widget>[
              Stack(
                children: <Widget>[
                  // 顶部渐变容器:用百分比替代固定高度
                  FractionallySizedBox(
                    heightFactor: isSmallScreen(screenWidth) ? 0.25 : 0.2,
                    widthFactor: 1,
                    child: Container(
                      decoration: const BoxDecoration(
                        gradient: LinearGradient(
                          colors: [Color(0xFF131313), Color(0xFF313131)],
                          begin: Alignment.topRight,
                          end: Alignment.bottomLeft,
                        ),
                      ),
                      child: Padding(
                        padding: EdgeInsets.symmetric(horizontal: screenWidth * 0.05, vertical: screenHeight * 0.02),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.end,
                          children: <Widget>[
                            const Row(
                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
                              children: <Widget>[
                                Column(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: [
                                    Text(
                                      'Location',
                                      style: TextStyle(color: Colors.grey, fontSize: 12),
                                    ),
                                    Row(
                                      children: [
                                        Text(
                                          'Montpellier, France',
                                          style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
                                        ),
                                        Icon(Icons.keyboard_arrow_down, color: Colors.white),
                                      ],
                                    ),
                                  ],
                                ),
                                CircleAvatar(
                                  backgroundImage: AssetImage('assets/app/products_assets/avatar.png'),
                                ),
                              ],
                            ),
                            SizedBox(height: screenHeight * 0.01),
                            // 搜索栏:自适应宽度
                            Container(
                              decoration: BoxDecoration(
                                color: Colors.grey[800],
                                borderRadius: BorderRadius.circular(10),
                              ),
                              child: Row(
                                children: [
                                  SizedBox(width: screenWidth * 0.03),
                                  const Icon(Icons.search, color: Colors.white),
                                  SizedBox(width: screenWidth * 0.03),
                                  const Expanded(
                                    child: TextField(
                                      decoration: InputDecoration(
                                        hintText: 'Search coffee',
                                        hintStyle: TextStyle(color: Colors.grey),
                                        border: InputBorder.none,
                                      ),
                                    ),
                                  ),
                                  Container(
                                    height: screenHeight * 0.06,
                                    decoration: BoxDecoration(
                                      color: Theme.of(context).colorScheme.primary,
                                      borderRadius: BorderRadius.circular(8),
                                    ),
                                    child: const Padding(
                                      padding: EdgeInsets.all(10.0),
                                      child: Icon(Icons.filter_list, color: Colors.white),
                                    ),
                                  ),
                                ],
                              ),
                            ),
                          ],
                        ),
                      ),
                    ),
                  ),
                  // 促销卡片:基于父约束定位和尺寸
                  Padding(
                    padding: EdgeInsets.only(
                      top: isSmallScreen(screenWidth) ? screenHeight * 0.18 : screenHeight * 0.15,
                      left: screenWidth * 0.05,
                      right: screenWidth * 0.05,
                    ),
                    child: Center(
                      child: FractionallySizedBox(
                        heightFactor: isSmallScreen(screenWidth) ? 0.18 : 0.15,
                        widthFactor: 1,
                        child: ImageCard(
                          imagePath: "assets/app/products_assets/coffee_promo.png",
                          child: LayoutBuilder(
                            builder: (context, cardConstraints) {
                              return Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                mainAxisSize: MainAxisSize.max,
                                children: [
                                  Container(
                                    padding: EdgeInsets.symmetric(
                                      horizontal: cardConstraints.maxHeight * 0.02,
                                      vertical: cardConstraints.maxHeight * 0.01,
                                    ),
                                    decoration: BoxDecoration(
                                      color: Colors.red,
                                      borderRadius: BorderRadius.circular(10),
                                    ),
                                    child: const Text(
                                      'Promo',
                                      style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
                                    ),
                                  ),
                                  SizedBox(height: cardConstraints.maxHeight * 0.05),
                                  FractionallySizedBox(
                                    heightFactor: 0.5,
                                    widthFactor: isSmallScreen(screenWidth) ? 0.6 : 0.4,
                                    child: PromoTextWidget(
                                      text: "Get 50% off\non your first order",
                                    ),
                                  ),
                                ],
                              );
                            },
                          ),
                        ),
                      ),
                    ),
                  ),
                  // 分类按钮行:自适应按钮尺寸
                  Padding(
                    padding: EdgeInsets.only(
                      top: isSmallScreen(screenWidth) ? screenHeight * 0.35 : screenHeight * 0.3,
                      left: screenWidth * 0.05,
                      right: screenWidth * 0.05,
                    ),
                    child: LayoutBuilder(
                      builder: (context, btnConstraints) {
                        return ButtonsRow(
                          buttonNames: productsName,
                          activeColor: Theme.of(context).colorScheme.primary,
                          inactiveColor: Colors.white70,
                          borderRadius: 10,
                          onButtonPressed: (String name) {
                            setState(() {
                              selectedButton = name;
                            });
                          },
                          isActiveTextStyle: TextStyle(
                            fontSize: isSmallScreen(screenWidth) ? 14 : 16,
                            color: Colors.white
                          ),
                          isInactiveTextStyle: TextStyle(
                            fontSize: isSmallScreen(screenWidth) ? 14 : 16,
                            color: Colors.black
                          ),
                          // 按钮尺寸基于父宽度计算
                          buttonSize: Size(
                            (btnConstraints.maxWidth / productsName.length) - 10,
                            btnConstraints.maxHeight * 0.8,
                          ),
                        );
                      },
                    ),
                  ),
                  // 商品网格:动态调整列数
                  Padding(
                    padding: EdgeInsets.only(
                      top: isSmallScreen(screenWidth) ? screenHeight * 0.42 : screenHeight * 0.37,
                      left: screenWidth * 0.05,
                      right: screenWidth * 0.05,
                    ),
                    child: FractionallySizedBox(
                      heightFactor: 0.5,
                      widthFactor: 1,
                      child: GridView.builder(
                        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                          crossAxisCount: isSmallScreen(screenWidth) ? 2 : isMediumScreen(screenWidth) ? 3 : 4,
                          mainAxisSpacing: 15.0,
                          crossAxisSpacing: 25.0,
                          childAspectRatio: isSmallScreen(screenWidth) ? 0.6 : 0.7,
                        ),
                        itemCount: products[selectedButton]!.length,
                        itemBuilder: (context, index) {
                          return LayoutBuilder(
                            builder: (context, constraints) {
                              return CustomCard(
                                imagePath: products[selectedButton]![index]['imagePath']!,
                                rating: products[selectedButton]![index]['rating']!,
                                nameType: products[selectedButton]![index]['nameType']!,
                                description: products[selectedButton]![index]['description']!,
                                price: products[selectedButton]![index]['price']!,
                                buttonColor: Theme.of(context).colorScheme.primary,
                                parentConstraints: constraints,
                              );
                            },
                          );
                        },
                      ),
                    ),
                  ),
                ],
              ),
            ],
          );
        },
      ),
    );
  }
}

// 以下两个组件保持原逻辑,无需修改
class ImageCard extends StatelessWidget {
  final String imagePath;
  final Widget child;

  const ImageCard({
    Key? key,
    required this.imagePath,
    required this.child,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.only(bottom: 16),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(16),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.5),
            spreadRadius: 2,
            blurRadius: 5,
            offset: const Offset(0, 0),
          ),
        ],
      ),
      child: Stack(
        children: [
          ClipRRect(
            borderRadius: BorderRadius.circular(16),
            child: Image.asset(
              imagePath,
              width: double.infinity,
              fit: BoxFit.cover,
            ),
          ),
          Positioned.fill(
            child: Container(
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(16),
              ),
              padding: const EdgeInsets.all(16.0),
              alignment: Alignment.bottomLeft,
              child: child,
            ),
          ),
        ],
      ),
    );
  }
}

class PromoTextWidget extends StatelessWidget {
  final String text;

  const PromoTextWidget({super.key, required this.text});

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        return Stack(
          children: text.split("\n").asMap().entries.map((entry) {
            int index = entry.key;
            String line = entry.value;

            return Positioned(
              top: index * (constraints.maxHeight * 0.35),
              child: Container(
                width: constraints.maxWidth,
                padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
                color: Colors.black,
                child: AutoSizeText(
                  line,
                  style: const TextStyle(
                    fontSize: 30,
                    fontWeight: FontWeight.bold,
                    color: Colors.white,
                  ),
                  minFontSize: 10,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
              ),
            );
          }).toList(),
        );
      },
    );
  }
}

关键优化点说明

  • 用LayoutBuilder获取父组件的约束(宽度/高度),替代MediaQuery
  • 所有固定尺寸替换为百分比值,基于父组件约束动态计算
  • 新增自定义断点逻辑,根据屏幕宽度切换布局细节(如网格列数、字体大小)
  • 用FractionallySizedBox实现组件占父容器的比例布局

响应式开发资源推荐

  • 官方指南:Flutter官方文档的「响应式布局」章节,详细讲解LayoutBuilder、FractionallySizedBox等核心组件的用法,以及如何基于约束构建自适应界面
  • 书籍:《Flutter实战》(国内作者)中有专门章节讲解响应式布局适配不同屏幕;《Flutter Complete Reference》(可找中文译本)覆盖了响应式设计的全场景解决方案
  • 开源参考:你提到的Flutter响应式仓库,核心思路是用LayoutBuilder结合自定义断点类,根据屏幕宽度区间切换布局结构,这种模式完全适配禁用MediaQuery的场景

内容的提问来源于stack exchange,提问作者Asile34

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 11:54:53