Flutter中ScrollController调用addListener报Too many elements错误
解决Flutter中ScrollController的"Bad state: Too many elements"错误
问题背景
在使用ScrollController.addListener时触发Bad state: Too many elements错误,错误发生在多次调用的fetchRewardLadder方法内。全局声明的ScrollController被每次调用方法时重新初始化,核心代码如下:
ScrollController rwScrollController = ScrollController(); fetchRewardLadder( String code, { bool fromFab = false, }) async { rwScrollController = ScrollController(); // 每次调用重新初始化 GccApiResponse response = await _repository.fetchRewardLadder( Variables$Query$fetchRewardLadder( code: code, ), ); switch (response.status) { case Status.ERROR: showExceptionSnackbar(message: response.message); break; default: try { Query$fetchRewardLadder$fetchRewardLadder reward = Query$fetchRewardLadder$fetchRewardLadder.fromJson( response.data["fetchRewardLadder"], ); safeEmit( state.copyWith( rewardLadder: reward, isLoading: false, ), ); rwScrollController.addListener( () { // 此处触发报错 if (rwScrollController.position.pixels > (0.95 * rwScrollController.position.maxScrollExtent) && !state.isLoading && state.leaderboard == null) { fetchLeaderBoard(code); } }, ); } catch (e) { showExceptionSnackbar( message: "Formatting failed" + e.toString(), ); } } }
崩溃日志:
Fatal Exception: io.flutter.plugins.firebase.crashlytics.FlutterError:
Bad state: Too many elements. Error thrown Instance of
'ErrorDescription'.
at _GrowableList.single(dart:core)
at ScrollController.position(scroll_controller.dart:109)
at RewardLadderCubit.fetchRewardLadder.(reward_ladder_cubit.dart:136)
...(后续调用栈省略)
错误原因
- 控制器生命周期管理混乱:每次调用
fetchRewardLadder都创建新的ScrollController实例,但旧实例未被dispose,如果对应的ScrollView未销毁,旧控制器会持续持有ScrollPosition引用,导致多个ScrollPosition关联到控制器(或新旧控制器的positions列表累积元素)。 position属性的限制:ScrollController.position内部调用positions.single,要求控制器只能关联一个ScrollPosition,当positions列表存在多个元素时,直接抛出"Too many elements"异常。- 监听器重复添加:每次调用方法都添加新监听器,未移除旧监听器,导致监听器累积,加剧内存泄漏和逻辑混乱。
解决方案
方案一:单一实例+正确生命周期管理(推荐)
只初始化一次ScrollController,并在类销毁时正确释放:
- 全局/类成员变量仅初始化一次:
final ScrollController rwScrollController = ScrollController();
- 在类的销毁方法中释放控制器(比如Cubit的
close方法):
@override Future<void> close() { rwScrollController.dispose(); return super.close(); }
- 抽离监听器方法,添加前先移除旧监听器:
fetchRewardLadder(String code, {bool fromFab = false}) async { // 先移除旧监听器,避免重复添加 rwScrollController.removeListener(_scrollListener); // 添加新监听器 rwScrollController.addListener(_scrollListener); // 后续API请求和状态更新代码... } // 单独抽离监听器方法 void _scrollListener() { // 先判断positions状态,避免异常 final position = rwScrollController.positions.firstOrNull; if (position != null && position.pixels > (0.95 * position.maxScrollExtent) && !state.isLoading && state.leaderboard == null) { fetchLeaderBoard(state.rewardLadder?.code ?? ''); } }
方案二:重置控制器时确保旧实例被释放
如果必须每次调用方法时重置控制器,需先销毁旧实例:
fetchRewardLadder(String code, {bool fromFab = false}) async { // 先销毁旧控制器,避免内存泄漏 rwScrollController.dispose(); // 创建新控制器 rwScrollController = ScrollController(); // API请求和状态更新代码... // 添加监听器前先检查positions状态 rwScrollController.addListener(() { final position = rwScrollController.positions.firstOrNull; if (position != null && position.pixels > (0.95 * position.maxScrollExtent) && !state.isLoading && state.leaderboard == null) { fetchLeaderBoard(code); } }); }
额外建议
- 避免在业务逻辑类(如Cubit)中持有
ScrollController,建议将控制器放在UI层的StatefulWidget中,通过回调传递滚动事件,符合单一职责原则。 - 访问控制器位置时,优先使用
positions.firstOrNull替代position属性,避免因多ScrollPosition导致的异常。
内容的提问来源于stack exchange,提问作者Firdous nath
相关产品推荐
相关产品推荐

