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

Flutter应用跨页面导航时,如何保持列表滚动位置持久化?

嘿,这个问题我之前做项目的时候正好碰到过,其实解决起来不难,核心就是要让页面的滚动状态在切换时不丢失,下面给你两种靠谱的方案:

方案一:用AutomaticKeepAliveClientMixin快速解决

这是Flutter官方提供的保持页面状态的方式,特别适合底部导航栏这种页面切换的场景。它的核心思路是让页面的State类混入这个Mixin,告诉Flutter“这个页面别随便销毁我的状态”。

步骤很简单:

  • 给每个ListView所在的页面State类添加with AutomaticKeepAliveClientMixin
  • 重写wantKeepAlive方法,返回true
  • 在build方法里一定要先调用super.build(context)(这个细节很容易忘,不调用的话状态还是会丢失)

代码示例

页面组件:

class FirstPage extends StatefulWidget {
  const FirstPage({super.key});

  @override
  State<FirstPage> createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true; // 告诉Flutter要保持页面状态

  @override
  Widget build(BuildContext context) {
    super.build(context); // 必须调用这行!
    return ListView.builder(
      itemCount: 100,
      itemBuilder: (context, index) {
        return ListTile(title: Text('第一页条目 $index'));
      },
    );
  }
}

// 第二页完全照搬第一页的写法即可
class SecondPage extends StatefulWidget {
  const SecondPage({super.key});

  @override
  State<SecondPage> createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ListView.builder(
      itemCount: 100,
      itemBuilder: (context, index) {
        return ListTile(title: Text('第二页条目 $index'));
      },
    );
  }
}

底部导航栏父组件(关键是用IndexedStack承载页面,避免切换时重建):

class BottomNavScreen extends StatefulWidget {
  const BottomNavScreen({super.key});

  @override
  State<BottomNavScreen> createState() => _BottomNavScreenState();
}

class _BottomNavScreenState extends State<BottomNavScreen> {
  int _currentIndex = 0;
  final List<Widget> _pages = const [
    FirstPage(),
    SecondPage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack( // 重点!用IndexedStack而不是直接返回_pages[_currentIndex]
        index: _currentIndex,
        children: _pages,
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
          BottomNavigationBarItem(icon: Icon(Icons.list), label: '列表'),
        ],
      ),
    );
  }
}
方案二:手动保存滚动位置(更灵活)

如果你的场景更复杂(比如页面可能被系统销毁、需要自定义滚动恢复逻辑,或者要把滚动位置持久化到本地),可以手动用ScrollController来保存和恢复滚动位置。

步骤:

  1. 在父组件中保存两个页面的滚动偏移量
  2. 给每个页面的ListView绑定ScrollController,监听滚动事件实时保存偏移量
  3. 切换回页面时,调用控制器的jumpTo或animateTo方法恢复位置

代码示例

父组件:

class BottomNavScreen extends StatefulWidget {
  const BottomNavScreen({super.key});

  @override
  State<BottomNavScreen> createState() => _BottomNavScreenState();
}

class _BottomNavScreenState extends State<BottomNavScreen> {
  int _currentIndex = 0;
  double _firstPageScrollOffset = 0.0;
  double _secondPageScrollOffset = 0.0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: [
          FirstPage(
            scrollOffset: _firstPageScrollOffset,
            onScrollChanged: (offset) {
              setState(() {
                _firstPageScrollOffset = offset;
              });
            },
          ),
          SecondPage(
            scrollOffset: _secondPageScrollOffset,
            onScrollChanged: (offset) {
              setState(() {
                _secondPageScrollOffset = offset;
              });
            },
          ),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
          BottomNavigationBarItem(icon: Icon(Icons.list), label: '列表'),
        ],
      ),
    );
  }
}

页面组件:

class FirstPage extends StatefulWidget {
  final double scrollOffset;
  final Function(double) onScrollChanged;

  const FirstPage({
    super.key,
    required this.scrollOffset,
    required this.onScrollChanged,
  });

  @override
  State<FirstPage> createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage> {
  late ScrollController _controller;

  @override
  void initState() {
    super.initState();
    // 初始化控制器,设置初始滚动位置
    _controller = ScrollController(initialScrollOffset: widget.scrollOffset);
    // 监听滚动事件,实时保存偏移量
    _controller.addListener(() {
      widget.onScrollChanged(_controller.offset);
    });
  }

  @override
  void didUpdateWidget(covariant FirstPage oldWidget) {
    super.didUpdateWidget(oldWidget);
    // 如果父组件传来的偏移量变化,更新控制器位置
    if (oldWidget.scrollOffset != widget.scrollOffset) {
      _controller.jumpTo(widget.scrollOffset);
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _controller,
      itemCount: 100,
      itemBuilder: (context, index) {
        return ListTile(title: Text('第一页条目 $index'));
      },
    );
  }
}

// 第二页写法和第一页完全一致
class SecondPage extends StatefulWidget {
  final double scrollOffset;
  final Function(double) onScrollChanged;

  const SecondPage({
    super.key,
    required this.scrollOffset,
    required this.onScrollChanged,
  });

  @override
  State<SecondPage> createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> {
  late ScrollController _controller;

  @override
  void initState() {
    super.initState();
    _controller = ScrollController(initialScrollOffset: widget.scrollOffset);
    _controller.addListener(() {
      widget.onScrollChanged(_controller.offset);
    });
  }

  @override
  void didUpdateWidget(covariant SecondPage oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.scrollOffset != widget.scrollOffset) {
      _controller.jumpTo(widget.scrollOffset);
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _controller,
      itemCount: 100,
      itemBuilder: (context, index) {
        return ListTile(title: Text('第二页条目 $index'));
      },
    );
  }
}

补充说明

  • 方案一适合绝大多数简单场景,代码量少,不用自己管理滚动位置
  • 方案二更灵活,比如可以把滚动位置存在SharedPreferences里,即使APP重启也能恢复之前的滚动位置
  • 不管用哪种方案,父组件一定要用IndexedStack承载页面,否则每次切换页面都会重建,状态必然丢失

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 08:51:11