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

Flutter ListView滚动加载API分页数据失败求助

解决Flutter分页加载时List无法更新的问题

核心问题排查方向

你遇到的问题本质是状态更新未正确触发UI重建,或数据处理逻辑存在漏洞,结合你描述的现象,从以下几个关键点入手:


1. 先确保基础逻辑无错误

滚动监听的正确性

检查ScrollController的监听是否在initState中注册,且dispose时销毁,同时滚动到底部的判断阈值要合理:

@override
void initState() {
  super.initState();
  _scrollController.addListener(_onScroll);
}

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

void _onScroll() {
  // 距离底部200px时触发加载,避免触发过于频繁
  if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) {
    _loadNextPage();
  }
}

List的可变性

确保_list是可变列表,初始化时不要用不可变构造(比如List.unmodifiable):

// 正确初始化方式
List<String> _list = [];

防止重复加载

必须加_isLoading标记,避免滚动到底部时多次触发API请求:

bool _isLoading = false;

Future<void> _loadNextPage() async {
  if (_isLoading) return; // 正在加载时直接返回
  setState(() {
    _page++;
    _isLoading = true;
  });
  // ...后续API请求逻辑
}

2. 修复AsyncValue.whenData无效的问题

如果使用Riverpod等状态管理库,不要在UI层的whenData回调里直接修改List,应该把数据拼接逻辑放到状态管理内部:

状态管理层处理数据拼接

final apiDataProvider = StateNotifierProvider<ApiDataNotifier, AsyncValue<List<String>>>((ref) {
  return ApiDataNotifier();
});

class ApiDataNotifier extends StateNotifier<AsyncValue<List<String>>> {
  ApiDataNotifier() : super(const AsyncLoading()) {
    _loadPage(1);
  }

  int _currentPage = 1;
  bool _isLoading = false;

  Future<void> _loadPage(int page) async {
    if (_isLoading) return;
    _isLoading = true;
    try {
      final apiData = await fetchApiData(page: page);
      // 第一页直接替换数据,后续页追加数据
      if (page == 1) {
        state = AsyncData(apiData.list_string);
      } else {
        state = AsyncData([...state.value!, ...apiData.list_string]);
      }
      _currentPage = page;
    } catch (e) {
      state = AsyncError(e, StackTrace.current);
    } finally {
      _isLoading = false;
    }
  }

  void loadNextPage() {
    _loadPage(_currentPage + 1);
  }
}

UI层只负责监听状态

class MyPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final scrollController = ScrollController();
    final apiData = ref.watch(apiDataProvider);

    // 滚动监听触发加载下一页
    scrollController.addListener(() {
      if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 200) {
        ref.read(apiDataProvider.notifier).loadNextPage();
      }
    });

    return apiData.when(
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (e, st) => Center(child: Text("加载失败: $e")),
      data: (list) {
        final isLoading = ref.read(apiDataProvider.notifier)._isLoading;
        return ListView.builder(
          controller: scrollController,
          itemCount: list.length + (isLoading ? 1 : 0),
          itemBuilder: (context, index) {
            if (index == list.length) {
              return const Center(child: CircularProgressIndicator());
            }
            return ListTile(title: Text(list[index]));
          },
        );
      },
    );
  }
}

3. 传统StatefulWidget的正确写法

如果不用状态管理,直接在State类中处理,确保所有数据更新都包裹在setState中:

class MyPage extends StatefulWidget {
  @override
  State<MyPage> createState() => _MyPageState();
}

class _MyPageState extends State<MyPage> {
  final ScrollController _scrollController = ScrollController();
  List<String> _list = [];
  int _page = 1;
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();
    _loadFirstPage();
    _scrollController.addListener(_onScroll);
  }

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

  Future<void> _loadFirstPage() async {
    setState(() => _isLoading = true);
    try {
      final apiData = await fetchApiData(page: _page);
      setState(() {
        _list.addAll(apiData.list_string);
        _isLoading = false;
      });
    } catch (e) {
      setState(() => _isLoading = false);
      // 处理错误逻辑
    }
  }

  Future<void> _loadNextPage() async {
    if (_isLoading) return;
    setState(() {
      _page++;
      _isLoading = true;
    });
    try {
      final apiData = await fetchApiData(page: _page);
      setState(() {
        _list.addAll(apiData.list_string); // 分页数据追加到列表末尾
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _page--; // 加载失败回退页码
        _isLoading = false;
      });
      // 处理错误逻辑
    }
  }

  void _onScroll() {
    if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) {
      _loadNextPage();
    }
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _scrollController,
      itemCount: _list.length + (_isLoading ? 1 : 0),
      itemBuilder: (context, index) {
        if (index == _list.length) {
          return const Center(child: CircularProgressIndicator());
        }
        return ListTile(title: Text(_list[index]));
      },
    );
  }
}

// 模拟API请求
Future<APIData> fetchApiData({required int page}) async {
  await Future.delayed(const Duration(seconds: 1));
  return APIData(list_string: List.generate(10, (i) => "第${page}页: 条目${i+1}"));
}

class APIData {
  final List<String> list_string;
  APIData({required this.list_string});
}

关键注意事项

  • 分页数据用addAll追加到列表末尾,不要用insert(除非业务需要插入特定位置)
  • 不要在AsyncValue.whenData的回调中直接调用setState,状态更新逻辑尽量放在状态管理内部
  • 加载失败时记得回退页码,避免后续请求页码错误

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 00:06:17