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

Flutter更新列表时如何保持滚动位置?

解决Flutter滚动加载更多后页面闪烁并回到顶部的问题

问题根源

  1. 使用FutureBuilder管理列表数据,每次更新notificationsListFuture会触发整个FutureBuilder重建,导致列表Widget被重新创建,滚动控制器的关联状态被重置。
  2. 直接保存当前滚动的绝对像素值,在新数据追加后列表总长度增加,原像素值对应的位置已不再是用户之前的浏览位置,导致滚动跳转失效。

修复方案

1. 改用State直接持有列表数据

移除notificationsListFuture,在State中直接维护NotificationsList实例,更新列表时仅触发局部刷新,避免整个列表重建。

修改State变量:

NotificationsList? _notificationsList;

在initState中初始化数据并监听滚动:

@override
void initState() {
  super.initState();
  _loadInitialData();
  _scrollController.addListener(() {
    // 距离底部100px时触发加载更多
    if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 100) {
      _loadMoreItems();
    }
  });
}

Future<void> _loadInitialData() async {
  String udid = await FlutterUdid.udid;
  _notificationsList = await getNotifications(0, max, udid);
  setState(() {});
}

2. 调整加载更多逻辑,维护相对滚动位置

加载新数据前计算当前位置到列表底部的偏移量,加载完成后基于新列表长度计算目标位置,保持用户浏览的相对位置。

修改_loadMoreItems方法:

Future<void> _loadMoreItems() async {
  if (!_isLoading || _notificationsList == null) return;

  setState(() {
    _isLoading = true;
  });

  // 计算当前滚动位置距离底部的偏移
  final double bottomOffset = _scrollController.position.maxScrollExtent - _scrollController.position.pixels;

  String udid = await FlutterUdid.udid;
  final int newOffset = offset + max;
  final NotificationsList newNotifsList = await getNotifications(newOffset, max, udid);

  if (newNotifsList.notifications.isNotEmpty) {
    _notificationsList!.notifications.addAll(newNotifsList.notifications);
    offset = newOffset;
  } else {
    // 无更多数据,恢复offset
    offset = offset - max;
    if (offset < 0) offset = 0;
  }

  setState(() {
    _isLoading = false;
  });

  // UI重建后调整滚动位置
  WidgetsBinding.instance.addPostFrameCallback((_) {
    if (_scrollController.hasClients) {
      final double targetPosition = _scrollController.position.maxScrollExtent - bottomOffset;
      _scrollController.jumpTo(targetPosition);
    }
  });
}

3. 重构Build方法,移除FutureBuilder

直接使用State中的_notificationsList构建UI,避免FutureBuilder的重建问题:

Widget build(BuildContext context) {
  return Scaffold(
    backgroundColor: Colors.white,
    appBar: widget.showAppbar ? TopAppBar() : null,
    drawer: widget.showAppbar ? TopDrawer() : null,
    body: RefreshIndicator(
      onRefresh: _refreshItems,
      child: _notificationsList == null
          ? _buildLoadingIndicator()
          : Container(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Padding(
                    padding: const EdgeInsets.only(top: 16, left: 16, bottom: 8),
                    child: Text(
                      'Notifications',
                      style: TextStyle(fontWeight: FontWeight.bold, fontSize: ScreenSizeService.h1FontSize),
                    ),
                  ),
                  Expanded(
                    child: _notificationsList!.notifications.isEmpty
                        ? Padding(
                            padding: const EdgeInsets.only(top: 8, left: 8, bottom: 0),
                            child: Text('No notifications'),
                          )
                        : ListView.separated(
                            controller: _scrollController,
                            itemCount: _notificationsList!.notifications.length + 1,
                            separatorBuilder: (_, __) => Divider(height: 1),
                            itemBuilder: (context, index) {
                              if (index < _notificationsList!.notifications.length) {
                                final NotificationBody notif = _notificationsList!.notifications[index];
                                return Padding(
                                  padding: EdgeInsets.fromLTRB(0, 8, 0, 8),
                                  child: ListTile(
                                    title: Padding(
                                      padding: EdgeInsets.only(bottom: 8.0),
                                      child: Text(
                                        notif.title,
                                        style: TextStyle(
                                          fontWeight: FontWeight.bold,
                                          color: Colors.black,
                                        ),
                                      ),
                                    ),
                                    subtitle: Padding(
                                      padding: EdgeInsets.fromLTRB(0, 0, 0, 8),
                                      child: Column(
                                        crossAxisAlignment: CrossAxisAlignment.start,
                                        children: [
                                          Padding(
                                            padding: EdgeInsets.only(bottom: 8.0),
                                            child: Text(
                                              notif.msg,
                                              style: TextStyle(
                                                fontWeight: FontWeight.normal,
                                                color: Colors.black,
                                              ),
                                            ),
                                          ),
                                          Text(
                                            notif.date,
                                            style: TextStyle(
                                              color: Colors.grey,
                                            ),
                                          ),
                                        ],
                                      ),
                                    ),
                                    trailing: Icon(Icons.arrow_forward, color: Colors.red),
                                    onTap: () {
                                      print("notification list item tapped");
                                    },
                                  ),
                                );
                              } else if (_isLoading) {
                                return _buildLoadingIndicator();
                              } else {
                                return Container();
                              }
                            },
                          ),
                  )
                ],
              ),
            ),
    ),
  );
}

4. 实现下拉刷新方法

重置列表数据和offset,重新加载初始数据:

Future<void> _refreshItems() async {
  setState(() {
    _isLoading = true;
  });

  String udid = await FlutterUdid.udid;
  _notificationsList = await getNotifications(0, max, udid);
  offset = 0;

  setState(() {
    _isLoading = false;
  });

  if (_scrollController.hasClients) {
    _scrollController.jumpTo(0);
  }
}

关键说明

  • 用State直接持有数据,避免FutureBuilder重建导致的Widget树重置,解决页面闪烁问题。
  • 基于相对底部的偏移量计算滚动位置,而非绝对像素值,确保新数据追加后用户浏览位置不变。
  • 在addPostFrameCallback中执行滚动操作,保证UI完成重建后滚动控制器能正确获取新的列表高度。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 04:35:05