Flutter中Column内放置ListView.builder的布局报错修复方法
问题现象
在Column组件内直接放置ListView.builder时触发以下布局报错:
RenderFlex children have non-zero flex but incoming height constraints are unbounded.
ListView用于通过API拉取图片数据,添加组件后整体布局损坏,报错截图:布局报错截图,控制台输出如下额外错误信息:
RenderBox was not laid out: RenderShrinkWrappingViewport#6c1e2 relayoutBoundary=up12 NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE
'package:flutter/src/rendering/box.dart':
Failed assertion: line 1979 pos 12: 'hasSize'
需求为将通过ListView.builder加载接口返回图片数据的ProfileImages组件,放入Editprofile组件的Column子组件列表中,原有问题代码如下:
import 'package:flutter/material.dart'; import 'package:mindmatch/utils/widget_functions.dart'; import 'package:mindmatch/screens/Notification.dart'; import 'package:getwidget/getwidget.dart'; import 'package:mindmatch/screens/Sidebar.dart'; import 'package:mindmatch/screens/Footer.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import 'dart:io'; import 'package:mindmatch/utils/Auth.dart'; import 'package:http_parser/http_parser.dart'; class Editprofile extends StatefulWidget { Editprofile({Key? key}) : super(key: key); @override _Editprofile createState() => _Editprofile(); } class _Editprofile extends State<Editprofile> { var UsrID = Auth.prefs?.getString('usrid'); var data; @override void initState() { super.initState(); getData(); } getData() async { var res = await http.get(Uri.https('www.*******.net', '/index.php', {'act':'profile', 'UsrID': '${UsrID}'})); data = jsonDecode(res.body); setState(() {}); } @override Widget build(BuildContext context) { final Size size = MediaQuery.of(context).size; final double padding = 25; final sidePadding = EdgeInsets.symmetric(horizontal: padding); return Scaffold( appBar: AppBar( titleSpacing: 3, backgroundColor: Colors.white, elevation: 0, title: Text('Edit Profile ${UsrID}', style: TextStyle(color: Colors.black, fontSize: 15,),), leading: Builder( builder: (BuildContext context) { return Padding(padding: EdgeInsets.fromLTRB(15, 0, 0, 0), child: IconButton( icon: SvgPicture.asset(width: 30,'assets/images/Menu.svg',height: 30,), onPressed: () { Scaffold.of(context).openDrawer(); }, tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip, ), ); }, ), actions: <Widget>[ Padding( padding: sidePadding, child: Row( children: [SvgPicture.asset(width: 30,'assets/images/search.svg',height: 30,)], ) ) ], ), backgroundColor: Color(0xff8f9df2), body: Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topRight, end: Alignment.bottomLeft, colors: [Colors.white, Colors.white] ), ), width: size.width, height: size.height, child: data != null?SingleChildScrollView( child: Padding( padding: sidePadding, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ ProfileImages(), ], ), ), ): const Center(child: CircularProgressIndicator(),), ), drawer: Sidebar(), persistentFooterButtons: [Footer()], ); } } class ProfileImages extends StatefulWidget { ProfileImages({Key? key}) : super(key: key); @override _ProfileImages createState() => _ProfileImages(); } class _ProfileImages extends State<ProfileImages> { var UsriD = Auth.prefs?.getString('usrid'); var Imagedata; @override void initState() { super.initState(); getImageData(); } getImageData() async { var res = await http.get(Uri.https('www.*******.net', '/index.php', {'act': 'usrPhotos', 'Usrid': '${UsriD}'})); Imagedata = jsonDecode(res.body); setState(() {}); } @override Widget build(BuildContext context) { return Imagedata != null? ListView.builder( shrinkWrap: true, scrollDirection: Axis.horizontal, itemCount: Imagedata.length, itemBuilder: (BuildContext context, int index) { return Stack( children: [ ClipRRect( borderRadius: BorderRadius.all(Radius.circular(8.0)), child: Image.network( "https://www.*******.net/files/images/${Imagedata[index]['image']}", fit: BoxFit.fill, ), ), Positioned( top: 9, right: 9, child: InkWell( onTap: () {}, child: SvgPicture.asset(width: 30,'assets/images/close.svg',height: 30,), ), ) ], ); } ): const Center(child: CircularProgressIndicator(),); } }
错误原因
Flutter布局组件强依赖明确的宽高约束:横向滚动的ListView仅处理水平方向的滚动逻辑,垂直方向没有默认高度,嵌套在Column、SingleChildScrollView这类本身不提供固定高度边界的组件中时,无法完成自身尺寸计算,即使设置了shrinkWrap: true也只能让ListView适配内容高度,在外层无高度约束的场景下依然会触发无边界错误。
同时原有代码中Image.network没有设置固定宽高、列表项没有边距约束,也会导致图片加载时出现布局抖动。
修复方案
- 给横向
ListView.builder外层包裹固定高度的SizedBox/Container,明确垂直方向的布局边界,高度值可根据实际图片展示需求调整 - 给列表内的图片项设置固定宽高,避免图片加载过程中尺寸跳变影响布局
- 若需要保留ListView的横向滑动能力,可将
physics设置为ClampingScrollPhysics(),和外层滚动组件的滑动行为兼容;不需要内部横向滑动则设置为NeverScrollableScrollPhysics()即可
修复后的_ProfileImages组件build方法代码如下:
@override Widget build(BuildContext context) { return Imagedata != null // 核心修改:给横向ListView明确高度约束 ? SizedBox( height: 120, // 可根据需求调整为100/150等数值 child: ListView.builder( shrinkWrap: true, scrollDirection: Axis.horizontal, physics: const ClampingScrollPhysics(), // 兼容滑动行为 itemCount: Imagedata.length, itemBuilder: (BuildContext context, int index) { // 给每个列表项设置固定宽度和边距 return Container( width: 100, margin: const EdgeInsets.only(right: 10), child: Stack( children: [ ClipRRect( borderRadius: BorderRadius.all(Radius.circular(8.0)), child: Image.network( "https://www.*******.net/files/images/${Imagedata[index]['image']}", fit: BoxFit.cover, width: 100, height: 120, ), ), Positioned( top: 9, right: 9, child: InkWell( onTap: () {}, child: SvgPicture.asset( width: 30, 'assets/images/close.svg', height: 30, ), ), ) ], ), ); } ), ) : const Center( child: CircularProgressIndicator(), ); }
内容的提问来源于stack exchange,提问作者RAF Algowid

