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
相关产品推荐
相关产品推荐

