Flutter优化求助:ListView数据过多引发卡顿崩溃问题
Flutter ListView 卡顿优化方案
一、移除ItemBuilder内的重复创建逻辑
每次列表项重建时,重复定义方法、获取参数、创建实例会造成不必要的内存开销和性能损耗,需统一复用:
- 将
addBookmark移至State类作为全局方法,避免每次item重建都重新定义 - 全局初始化Dio实例、获取屏幕宽度,不在item内重复创建
- 提取单独的item构建方法,简化
itemBuilder逻辑
class _NewJobListState extends State<NewJobList> { late PostFeeProvider postFeeProvider; final controller = ScrollController(); bool isLoading = false; int pageLoad = 1; final dio = Dio(); late double currentWidth; @override void initState() { super.initState(); postFeeProvider = Provider.of<PostFeeProvider>(context, listen: false); postFeeProvider.getAllFeePost(pageLoad, true); controller.addListener(_scrollListener); } @override void didChangeDependencies() { super.didChangeDependencies(); currentWidth = MediaQuery.of(context).size.width; } @override void dispose() { controller.dispose(); super.dispose(); } Future<void> addBookmark(String postId) async { String current_user_id = widget.userId; try { var response = await dio.post( "$baseUrl/saveBookmark/addBookmark/$current_user_id/$postId", data: {}); if (response.statusCode == 200) { final snackBar = SnackBar( elevation: 0, behavior: SnackBarBehavior.floating, backgroundColor: Colors.transparent, content: AwesomeSnackbarContent( title: 'Thêm Bookmark thành công', message: 'Bây giờ, Bạn có thể nhìn thấy bookmark đã lưu', contentType: ContentType.success, ), ); ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(snackBar); } } catch (e) { print(e); } } void _scrollListener() { if (controller.position.pixels == controller.position.maxScrollExtent && !isLoading) { setState(() { isLoading = true; pageLoad++; }); postFeeProvider.getAllFeePost(pageLoad, false).then((_) { setState(() { isLoading = false; }); }); } } @override Widget build(BuildContext context) { return Consumer<PostFeeProvider>( builder: (context, postAllData, _) { return ListView.builder( controller: controller, physics: const AlwaysScrollableScrollPhysics(), shrinkWrap: false, itemCount: postAllData.postFee.length + (isLoading ? 1 : 0), itemBuilder: (context, index) { if (index == postAllData.postFee.length) { return const Center(child: CircularProgressIndicator()); } final post = postAllData.postFee[index]; return GestureDetector( onTap: () async { await Navigator.of(context).push( MaterialPageRoute( builder: (_) => JobDetail(post: post), ), ); await updateView(post.id); postFeeProvider.updatePostView(post.id); }, child: buildPostItem(post)); }, ); }, ); } Widget buildPostItem(PostAll post) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ avatar(post), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ nameAndTime(post), titleAndprice(post), description(post), post.images.isNotEmpty ? ListImages(post: post, indexColumn: index) : const SizedBox(), Padding( padding: const EdgeInsets.only(top: 7, bottom: 30, right: 20), child: Row( children: [ SizedBox( height: 22, width: 20.84, child: Image.asset( 'assets/images/workpost_icon.png', fit: BoxFit.cover, ), ), SizedBox(width: currentWidth * 0.01), Text( post.applyCount.toString(), style: const TextStyle( fontFamily: 'AvertaStdCY-Semibold', ), ), SizedBox(width: currentWidth * 0.03), SizedBox( height: 22, width: 26.57, child: Image.asset( 'assets/images/eyepost_icon.png', fit: BoxFit.cover, ), ), SizedBox(width: currentWidth * 0.01), Text( post.view.toString(), style: const TextStyle( fontFamily: 'AvertaStdCY-Semibold', ), ), const Spacer(), GestureDetector( onTap: () => addBookmark(post.id), child: SizedBox( height: 23, width: 24.64, child: Image.asset( 'assets/images/savepost_icon.png', fit: BoxFit.cover, ), ), ), const SizedBox(width: 16), PopupMenuButton<int>( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10.0), ), onSelected: (value) { if (value == 1) { showGeneralDialog( barrierLabel: "Barrier", barrierDismissible: true, barrierColor: Colors.transparent, transitionDuration: const Duration(milliseconds: 400), context: context, pageBuilder: (_, __, ___) { return BlurredDialog( dialogContent: ReportPostDialog( nameMe: widget.nameMe, userId: widget.userId, postId: post.id, )); }, transitionBuilder: (_, anim, __, child) { Tween<Offset> tween = Tween( begin: const Offset(0, 1), end: Offset.zero); return SlideTransition( position: tween.animate( CurvedAnimation( parent: anim, curve: Curves.easeInOut), ), child: child, ); }); } else if (value == 2) { // 处理举报用户逻辑 } else if (value == 3) { print('3'); } }, itemBuilder: (BuildContext context) => [ PopupMenuItem<int>( value: 1, child: Row( children: [ Image.asset( "assets/images/warning_ic.png", width: 30, ), const Text( ' Báo cáo bài viết', style: TextStyle( fontFamily: 'AvertaStdCY-Bold', fontSize: 15, color: Color(0xff3C3F42), ), ), ], ), ), PopupMenuItem<int>( value: 2, child: Row( children: [ Image.asset( "assets/images/report_user_ico.png", width: 30, ), Text( ' Báo cáo ${post.name}', style: const TextStyle( fontFamily: 'AvertaStdCY-Bold', fontSize: 15, color: Color(0xff3C3F42), ), ), ], ), ), ], child: SizedBox( height: 23 * 0.8, width: 24.64 * 0.8, child: Image.asset( 'assets/images/ellipsis_icon.png', fit: BoxFit.cover, ), ), ) ], ), ), ], ), ), ], ); } }
二、图片加载优化
原生Image.network无缓存,滚动时重复下载图片,改用cached_network_image库实现缓存:
import 'package:cached_network_image/cached_network_image.dart'; Widget avatar(PostAll post) { return Padding( padding: const EdgeInsets.only(left: 10, right: 20, top: 10), child: Column( children: [ SizedBox( width: 60, height: 60, child: Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(50), child: CachedNetworkImage( imageUrl: '${post.avatar}', fit: BoxFit.cover, width: 50, height: 50, placeholder: (context, url) => const CircularProgressIndicator(), errorWidget: (context, url, error) => const Icon(Icons.person), ), ), Positioned( bottom: 3, left: 27, child: Lottie.asset("${post.emoji}", height: 30)), ], ), ), const Expanded( child: DottedDashedLine( height: double.infinity, width: 5, axis: Axis.vertical)) ], ), ); }
三、简化布局嵌套
移除Stack+IntrinsicHeight+SingleChildScrollView的冗余嵌套:
- 用
Row横向排列头像和内容,减少布局计算量 - 移除嵌套滚动组件,依赖ListView本身的滚动能力
- 去掉
IntrinsicHeight,改用CrossAxisAlignment.start控制对齐
四、优化数据更新逻辑
点击item后不再全量刷新列表,仅更新单个post的浏览量:
class PostFeeProvider extends ChangeNotifier { List<PostAll> postFee = []; void updatePostView(String postId) { final index = postFee.indexWhere((post) => post.id == postId); if (index != -1) { postFee[index].view += 1; notifyListeners(); } } Future<void> getAllFeePost(int page, bool isRefresh) async { // 原有逻辑:isRefresh为true时清空列表,否则追加数据 } }
五、其他优化建议
- 提取全局TextStyle常量,避免重复创建样式对象
- 限制Lottie动画的帧率和缓存,减少滚动时的性能消耗
- 设置分页加载的最大页数,避免无限加载导致内存溢出
- 静态Widget(如SizedBox、Text的style)尽量使用
const构造函数,减少重建
内容的提问来源于stack exchange,提问作者UITHCM
相关产品推荐
相关产品推荐

