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

Flutter优化求助:ListView数据过多引发卡顿崩溃问题

Flutter ListView 卡顿优化方案

一、移除ItemBuilder内的重复创建逻辑

每次列表项重建时,重复定义方法、获取参数、创建实例会造成不必要的内存开销和性能损耗,需统一复用:

  1. 将addBookmark移至State类作为全局方法,避免每次item重建都重新定义
  2. 全局初始化Dio实例、获取屏幕宽度,不在item内重复创建
  3. 提取单独的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 09:47:02