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

Flutter使用ScrollController的jumpTo方法触发栈溢出错误的解决

解决Flutter滚动组件同步滚动时的栈溢出问题

我来帮你分析这个问题并给出可行的解决方案。

错误原因

你的代码里用了一个NotificationListener包裹两个滚动组件,它会监听所有子组件的所有滚动事件——包括通过jumpTo强制触发的滚动。

当你触摸标注“FOLLOW”的组件(由c2控制)滚动时,会触发以下无限循环:

  1. 滚动操作触发ScrollNotification,进入回调函数
  2. 执行c2.jumpTo(c1.offset),强制c2滚动到c1的位置
  3. 这个强制滚动又会再次触发ScrollNotification,回调函数再次执行
  4. 往复循环最终导致栈溢出(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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 04:38:40