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

