Flutter使用ScrollController的jumpTo方法触发栈溢出错误的解决
解决Flutter滚动组件同步滚动时的栈溢出问题
我来帮你分析这个问题并给出可行的解决方案。
错误原因
你的代码里用了一个NotificationListener包裹两个滚动组件,它会监听所有子组件的所有滚动事件——包括通过jumpTo强制触发的滚动。
当你触摸标注“FOLLOW”的组件(由c2控制)滚动时,会触发以下无限循环:
- 滚动操作触发
ScrollNotification,进入回调函数 - 执行
c2.jumpTo(c1.offset),强制c2滚动到c1的位置 - 这个强制滚动又会再次触发
ScrollNotification,回调函数再次执行 - 往复循环最终导致栈溢出(Stack Overflow)
另外你的同步逻辑也有缺陷:不管哪个组件滚动,都只让c2跳转到c1的偏移量,这会导致“FOLLOW”组件的手动滚动被强制覆盖,也没实现真正的双向同步。
修复方案
我们改用ScrollController的addListener方法监听滚动,同时添加一个标记避免循环触发。具体修改如下:
修改后的完整代码
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', home: MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key? key, this.title}) : super(key: key); final String? title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { final ScrollController c1 = ScrollController(); final ScrollController c2 = ScrollController(); bool _isSyncing = false; // 标记是否正在同步,避免循环触发 @override void initState() { super.initState(); // 监听c1滚动,同步到c2 c1.addListener(() { if (!_isSyncing) { _isSyncing = true; c2.jumpTo(c1.offset); // 当前帧渲染完成后重置标记,确保同步操作彻底结束 WidgetsBinding.instance.addPostFrameCallback((_) { _isSyncing = false; }); } }); // 监听c2滚动,同步到c1 c2.addListener(() { if (!_isSyncing) { _isSyncing = true; c1.jumpTo(c2.offset); WidgetsBinding.instance.addPostFrameCallback((_) { _isSyncing = false; }); } }); } @override void dispose() { // 销毁控制器,避免内存泄漏 c1.dispose(); c2.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('data test'), ), body: Container( padding: const EdgeInsets.all(30), color: Colors.blue[100], height: 200, child: Column( children: <Widget>[ const Text('SOME TEXT HERE'), SingleChildScrollView( scrollDirection: Axis.horizontal, controller: c1, child: Container( height: 50, color: Colors.green[100], child: Row( children: <Widget>[ Container(width: 100, child: const Text('+MOOOVE -')), Container(width: 100, child: const Text('-MOOOVE -')), Container(width: 100, child: const Text('-MOOOVE -')), Container(width: 100, child: const Text('*MOOOVE *')), Container(width: 100, child: const Text('-MOOOVE -')), Container(width: 100, child: const Text('-MOOOVE -')), Container(width: 100, child: const Text('-MOOOVE +')), ], ), ), ), const Text('ANOTHER TEXT HERE'), SingleChildScrollView( scrollDirection: Axis.horizontal, controller: c2, child: Container( height: 50, color: Colors.red[100], child: Row( children: <Widget>[ Container(width: 100, child: const Text('+ FOLLOW-')), Container(width: 100, child: const Text('- FOLLOW-')), Container(width: 100, child: const Text('- FOLLOW-')), Container(width: 100, child: const Text('* FOLLOW*')), Container(width: 100, child: const Text('- FOLLOW-')), Container(width: 100, child: const Text('- FOLLOW-')), Container(width: 100, child: const Text('- FOLLOW+')), ], ), ), ), ], ), ), ); } }
关键优化点
_isSyncing标记:用来区分是用户手动滚动还是同步操作触发的滚动,彻底避免无限循环- 双向监听逻辑:给两个控制器都添加监听器,实现滚动任意一个组件,另一个都会同步跟随
addPostFrameCallback:确保在当前渲染帧结束后再重置标记,保证同步操作完全完成后才允许下一次同步- 控制器销毁:在
dispose方法中销毁滚动控制器,防止内存泄漏
修改后不管滚动“MOVE”还是“FOLLOW”组件,两个组件都会同步滚动,且不会再出现栈溢出问题。
内容的提问来源于stack exchange,提问作者Chris
相关产品推荐
相关产品推荐

