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

为何Flutter中ListView.builder未实现懒加载,页面跳转卡顿?

Flutter ListView.builder 页面跳转卡顿问题

点击按钮跳转至包含ListView.builder的页面时,应用卡顿数秒,列表需加载400+自定义Widget,即便用轻量组件(比如简单文本)也会变慢,自定义Widget卡顿更明显。

已知ListView.builder是懒加载组件,理论上能优化加载,但试过FutureBuilder和分页方案都没解决问题。

以下是对应页面的Widget代码:

class ExercisesListAdd extends ConsumerStatefulWidget {
  const ExercisesListAdd({
    super.key,
    required this.workout,
    required this.indexI,
    required this.exercises,
  });

  final Workout workout;
  final int? indexI;
  final List<Exercise> exercises;

  @override
  ConsumerState<ExercisesListAdd> createState() => _ExercisesListAddState();
}

class _ExercisesListAddState extends ConsumerState<ExercisesListAdd> {
  List<Exercise> get exercises => widget.exercises;

  @override
  Widget build(BuildContext context) {
    final userP = ref.watch(userStateNotifier);
    final exerciseP = ref.watch(exercisesProvider);
    final exercisePnot = ref.watch(exercisesProvider.notifier);
    final int? ind = widget.indexI;

    return SingleChildScrollView(
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 15),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            if (userP.customExercises.isNotEmpty)
              Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const SizedBox(height: 10),
                  Text(
                    'Custom Exercises',
                    style: Theme.of(context).textTheme.bodyLarge!.copyWith(
                          color: Theme.of(context).colorScheme.onBackground,
                          fontWeight: FontWeight.w600,
                        ),
                  ),
                  const SizedBox(height: 5),
                  ListView.builder(
                    shrinkWrap: true,
                    physics: NeverScrollableScrollPhysics(),
                    itemBuilder: (context, index) {
                      return Column(
                        children: [
                          const SizedBox(height: 5),
                          exerciseTile(
                            context,
                            userP.customExercises[index],
                            userP,
                            userP.customExercises[index].exType,
                            ind,
                          ),
                          const SizedBox(height: 5)
                        ],
                      );
                    },
                    itemCount: userP.customExercises.length,
                  ),
                ],
              ),
            const SizedBox(height: 10),
            Text(
              'All Exercises',
              style: Theme.of(context).textTheme.bodyLarge!.copyWith(
                    color: Theme.of(context).colorScheme.onBackground,
                    fontWeight: FontWeight.w600,
                  ),
            ),
            const SizedBox(height: 5),
            ListView.builder(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              itemBuilder: (context, index) {
                return exerciseTile(
                  context,
                  exerciseP[index],
                  userP,
                  exerciseP[index].exType,
                  ind,
                );
              },
              itemCount: exerciseP.length,
            ),
          ],
        ),
      ),
    );
  }

  ListTile exerciseTile(BuildContext context, Exercise exercise, AppUser user,
      ExerciseTypes type, int? ind) {
    return ListTile(
      contentPadding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
      title: Text(
        exercise.title,
        style: Theme.of(context).textTheme.bodyLarge!.copyWith(
              color: Theme.of(context).colorScheme.onBackground,
            ),
      ),
      trailing: SizedBox(
        width: MediaQuery.sizeOf(context).width / 3.2,
        child: Text(
          exercise.categories.join(', '),
          textAlign: TextAlign.end,
          style: Theme.of(context).textTheme.bodySmall!.copyWith(
                color:
                    Theme.of(context).colorScheme.onBackground.withOpacity(.6),
              ),
        ),
      ),
      onTap: () {
        if (ind == null) {
          widget.workout.exerciseList.add(
            exercise.toPersonalExercise(type, user),
          );
        } else {
          widget.workout.exerciseList.insert(
            ind,
            exercise.toPersonalExercise(type, user),
          );
        }

        Navigator.pop(context, exercise);
      },
    );
  }
}
优化方案

1. 修复懒加载失效问题

当前页面用SingleChildScrollView嵌套两个设置了shrinkWrap: true的ListView.builder,这会迫使ListView一次性计算所有子项的高度,完全丧失懒加载特性。改用CustomScrollView结合SliverList实现真正的懒加载:

@override
Widget build(BuildContext context) {
  final userP = ref.watch(userStateNotifier);
  final exerciseP = ref.watch(exercisesProvider);
  final exercisePnot = ref.read(exercisesProvider.notifier); // 改为ref.read,避免不必要重建
  final int? ind = widget.indexI;

  return CustomScrollView(
    slivers: [
      // 自定义练习标题部分
      SliverPadding(
        padding: const EdgeInsets.symmetric(horizontal: 15),
        sliver: SliverList.list(
          children: [
            if (userP.customExercises.isNotEmpty) ...[
              const SizedBox(height: 10),
              Text(
                'Custom Exercises',
                style: Theme.of(context).textTheme.bodyLarge!.copyWith(
                      color: Theme.of(context).colorScheme.onBackground,
                      fontWeight: FontWeight.w600,
                    ),
              ),
              const SizedBox(height: 5),
            ],
          ],
        ),
      ),
      // 自定义练习列表
      if (userP.customExercises.isNotEmpty)
        SliverPadding(
          padding: const EdgeInsets.symmetric(horizontal: 15),
          sliver: SliverList.builder(
            itemCount: userP.customExercises.length,
            itemBuilder: (context, index) {
              final exercise = userP.customExercises[index];
              return Column(
                children: [
                  const SizedBox(height: 5),
                  exerciseTile(
                    context,
                    exercise,
                    userP,
                    exercise.exType,
                    ind,
                  ),
                  const SizedBox(height: 5)
                ],
              );
            },
          ),
        ),
      // 全部练习标题部分
      SliverPadding(
        padding: const EdgeInsets.symmetric(horizontal: 15),
        sliver: SliverList.list(
          children: [
            const SizedBox(height: 10),
            Text(
              'All Exercises',
              style: Theme.of(context).textTheme.bodyLarge!.copyWith(
                    color: Theme.of(context).colorScheme.onBackground,
                    fontWeight: FontWeight.w600,
                  ),
            ),
            const SizedBox(height: 5),
          ],
        ),
      ),
      // 全部练习列表
      SliverPadding(
        padding: const EdgeInsets.symmetric(horizontal: 15),
        sliver: SliverList.builder(
          itemCount: exerciseP.length,
          itemBuilder: (context, index) {
            final exercise = exerciseP[index];
            return exerciseTile(
              context,
              exercise,
              userP,
              exercise.exType,
              ind,
            );
          },
        ),
      ),
    ],
  );
}

2. 提取列表项为独立StatelessWidget

把exerciseTile提取成单独的StatelessWidget,让Flutter更好地缓存Widget,减少重复创建开销:

class ExerciseTile extends StatelessWidget {
  final Exercise exercise;
  final AppUser user;
  final ExerciseTypes type;
  final int? ind;
  final Workout workout;

  const ExerciseTile({
    super.key,
    required this.exercise,
    required this.user,
    required this.type,
    required this.ind,
    required this.workout,
  });

  @override
  Widget build(BuildContext context) {
    // 缓存宽度计算结果,避免每次build重复计算
    final trailingWidth = MediaQuery.sizeOf(context).width / 3.2;
    
    return ListTile(
      contentPadding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
      title: Text(
        exercise.title,
        style: Theme.of(context).textTheme.bodyLarge!.copyWith(
              color: Theme.of(context).colorScheme.onBackground,
            ),
      ),
      trailing: SizedBox(
        width: trailingWidth,
        child: Text(
          exercise.categories.join(', '),
          textAlign: TextAlign.end,
          style: Theme.of(context).textTheme.bodySmall!.copyWith(
                color: Theme.of(context).colorScheme.onBackground.withOpacity(.6),
              ),
        ),
      ),
      onTap: () {
        final personalExercise = exercise.toPersonalExercise(type, user);
        if (ind == null) {
          workout.exerciseList.add(personalExercise);
        } else {
          workout.exerciseList.insert(ind, personalExercise);
        }
        Navigator.pop(context, exercise);
      },
    );
  }
}

使用时替换原exerciseTile调用:

ExerciseTile(
  key: ValueKey(exercise.id), // 添加key帮助Flutter识别Widget
  exercise: exercise,
  user: userP,
  type: exercise.exType,
  ind: ind,
  workout: widget.workout,
)

3. 优化Provider监听

  • 仅监听必要状态:如果不需要监听exercisesProvider的notifier状态,用ref.read替代ref.watch,避免不必要的Widget重建。
  • 添加select筛选监听:比如ref.watch(userStateNotifier.select((state) => state.customExercises)),只监听自定义练习列表的变化,减少build触发次数。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 19:34:51