Flutter 保存恢复ListView最后滚动位置功能失效如何解决
问题根因
你当前的实现存在3个核心问题导致滚动位置无法跨重启恢复:
- 偏移量仅存储在内存的
DoubleHolder对象中,应用重启后内存数据会被完全清空,你提到使用了SharedPreferences但现有代码没有对应的持久化写入、启动读取逻辑 initState阶段初始化ScrollController时,列表数据还未加载完成、ListView未完成渲染,此时设置的initialScrollOffset会因为列表可滚动高度不足被系统忽略,默认回到顶部- 滚动监听仅更新了内存中的偏移值,没有同步写入持久化存储,退出应用后数据就丢失了
修复方案
你可以按照以下步骤调整代码:
- 封装SharedPreferences读写滚动偏移的工具方法,处理异步操作
- 等待列表数据加载完成、页面第一帧渲染结束后,再主动调用滚动方法恢复位置,不要依赖
initialScrollOffset - 滚动偏移更新时同步写入SharedPreferences持久化存储
- 页面销毁时释放
ScrollController避免内存泄漏
完整修改后代码
import 'package:shared_preferences/shared_preferences.dart'; class ListViewScreen3 extends StatefulWidget { String itemHolder; ListViewScreen3({Key key, @required this.itemHolder}) : super(key: key); @override ListViewScreenState3 createState() => ListViewScreenState3(); } class ListViewScreenState3 extends State<ListViewScreen3> { ScrollController _controller; // 存储滚动偏移量的自定义key static const String _scrollOffsetKey = "list_scroll_offset"; List surahcountlist = []; // 替换为你自己的列表数据源定义 @override void initState() { super.initState(); _controller = ScrollController(); // 先加载列表数据 getAllSavedData().then((_) { // 数据加载完成后,等页面第一帧渲染结束再恢复滚动位置 WidgetsBinding.instance.addPostFrameCallback((_) async { final prefs = await SharedPreferences.getInstance(); final savedOffset = prefs.getDouble(_scrollOffsetKey) ?? 0.0; // 校验偏移量合法性,避免超出列表可滚动范围 if (_controller.hasClients && savedOffset <= _controller.position.maxScrollExtent) { _controller.jumpTo(savedOffset); } }); }); } // 你原有加载列表数据的方法,确保是异步返回 Future<void> getAllSavedData() async { // 原有加载数据的逻辑,给surahcountlist赋值 } // 持久化存储滚动偏移 Future<void> _saveScrollOffset(double offset) async { final prefs = await SharedPreferences.getInstance(); await prefs.setDouble(_scrollOffsetKey, offset); } @override void dispose() { _controller.dispose(); // 释放控制器避免内存泄漏 super.dispose(); } @override Widget build(BuildContext context) { final makeBody = Container( decoration: BoxDecoration( color: light_mode ? const Color(0xFFFFFFFF) : const Color(0xFF6D6D6D) ), child: returnListView() ); // 补充你原有外层页面结构,比如Scaffold return Scaffold(body: makeBody); } Widget returnListView() { return Column( textDirection: TextDirection.rtl, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( child: SizedBox( height: 200.0, child: NotificationListener<ScrollNotification>( child: ListView.builder( controller: _controller, itemCount: (surahcountlist.length/2).toInt(), itemBuilder: (context, index) { return GestureDetector( child: Card( color: light_mode ? Colors.white : const Color(0xFF6D6D6D), child: Column( textDirection: TextDirection.rtl, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ showBSMLSV(index), Wrap( direction: Axis.horizontal, alignment: WrapAlignment.start, runAlignment: WrapAlignment.center, textDirection: TextDirection.rtl, spacing: 2.0, runSpacing: 5.0, children: makeSurahListview( surahcountlist[index].surah_no, surahcountlist[index].ayah_no, surahcountlist[index].count, index ) ), ] ) ) ); } ), onNotification: (notification) { // 仅在滚动位置更新时存储,减少不必要的IO操作 if (notification is ScrollUpdateNotification) { _saveScrollOffset(notification.metrics.pixels); } return true; } ) ) ) ], ); } }
额外优化建议
如果你的列表item高度不固定,或者数据量极大,建议改为存储可见第一个item的下标,而非滚动像素偏移,避免item高度变化导致恢复位置不准;如果需要滚动恢复动画,可以把_controller.jumpTo替换为_controller.animateTo自行配置动画参数。
内容的提问来源于stack exchange,提问作者Noor
相关产品推荐
相关产品推荐

