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

Flutter:Riverpod结合infinite_scroll_pagination分页的优化建议

Riverpod + infinite_scroll_pagination 4.0.0 新闻分页错误纠正与优化方案

常见错误纠正

  • FutureProvider 不适配分页场景:FutureProvider仅处理单次异步请求,无法维护分页所需的加载状态、当前页码、列表数据等可变状态,会导致重复请求、状态丢失等问题。
    纠正:改用AsyncNotifierProvider(Riverpod 2.x+)或StateNotifierProvider管理分页状态,这类Provider支持状态更新,更适配分页逻辑。
  • 搜索词变更未重置分页:搜索关键词改变时,未重置页码和清空旧数据,导致新老搜索结果混杂。
    纠正:监听搜索词状态变化,一旦更新,立即重置页码为初始值、清空现有列表,并触发第一页请求。
  • 重复触发加载请求:未联动Provider的加载状态,导致请求未完成时重复触发加载更多。
    纠正:在Provider中维护加载状态,请求进行时拒绝新请求;UI层通过分页控制器的状态控制加载触发逻辑。
  • API参数与分页判断错误:未正确传递页码、每页数量,或未根据API返回判断是否还有更多数据,导致无效请求。
    纠正:确保页码随加载递增,根据API返回的hasMore(或总页数)判断是否终止加载,避免无意义请求。

优化方案

1. 状态管理优化(AsyncNotifierProvider实现)

// 定义分页数据模型
class PagedNewsList {
  final List<News> data;
  final bool hasMore;

  PagedNewsList({required this.data, required this.hasMore});
}

// 分页状态管理Notifier
final newsPaginationProvider = AsyncNotifierProvider.family<NewsPaginationNotifier, PagedNewsList, String>(NewsPaginationNotifier.new);

class NewsPaginationNotifier extends AsyncNotifier<PagedNewsList> {
  static const _pageSize = 10;
  int _currentPage = 1;
  bool _hasMore = true;

  @override
  Future<PagedNewsList> build(String searchQuery) async {
    // 首次加载或搜索词变更时重置状态
    _currentPage = 1;
    _hasMore = true;
    final response = await _fetchNews(searchQuery, _currentPage);
    _hasMore = response.hasMore;
    return PagedNewsList(data: response.news, hasMore: _hasMore);
  }

  Future<void> loadMore() async {
    if (!_hasMore || state.isLoading) return;

    _currentPage++;
    state = const AsyncValue.loading();
    try {
      final searchQuery = ref.read(searchQueryProvider);
      final response = await _fetchNews(searchQuery, _currentPage);
      _hasMore = response.hasMore;

      final currentData = state.value?.data ?? [];
      state = AsyncValue.data(
        PagedNewsList(
          data: [...currentData, ...response.news],
          hasMore: _hasMore,
        ),
      );
    } catch (e, stackTrace) {
      state = AsyncValue.error(e, stackTrace);
      // 加载失败回退页码,避免跳过页面
      _currentPage--;
    }
  }

  // 封装API请求
  Future<NewsApiResponse> _fetchNews(String query, int page) {
    return NewsApi.fetch(query, page: page, pageSize: _pageSize);
  }
}

// 搜索词状态管理
final searchQueryProvider = StateProvider<String>((ref) => '');

2. UI层与Provider联动优化

class NewsSearchPage extends ConsumerStatefulWidget {
  const NewsSearchPage({super.key});

  @override
  ConsumerState<NewsSearchPage> createState() => _NewsSearchPageState();
}

class _NewsSearchPageState extends ConsumerState<NewsSearchPage> {
  late final PagingController<int, News> _pagingController;
  Timer? _debounceTimer;

  @override
  void initState() {
    super.initState();
    _pagingController = PagingController(firstPageKey: 1);
    _bindPagingController();
  }

  void _bindPagingController() {
    // 监听Provider状态更新分页控制器
    ref.listen<AsyncValue<PagedNewsList>>(
      newsPaginationProvider(ref.watch(searchQueryProvider)),
      (previous, next) {
        next.when(
          data: (pagedList) {
            if (pagedList.data.isEmpty) {
              _pagingController.itemList = [];
              _pagingController.appendLastPage([]);
            } else if (previous?.value?.data.length != pagedList.data.length) {
              if (pagedList.hasMore) {
                _pagingController.appendPage(pagedList.data, _pagingController.nextPageKey);
              } else {
                _pagingController.appendLastPage(pagedList.data);
              }
            }
          },
          loading: () => _pagingController.status = PagingStatus.loadingNextPage,
          error: (e, _) {
            _pagingController.error = e;
          },
        );
      },
    );

    // 绑定加载更多逻辑
    _pagingController.addPageRequestListener((_) {
      ref.read(newsPaginationProvider(ref.watch(searchQueryProvider)).notifier).loadMore();
    });
  }

  @override
  void dispose() {
    _pagingController.dispose();
    _debounceTimer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final paginationState = ref.watch(newsPaginationProvider(ref.watch(searchQueryProvider)));

    return Scaffold(
      appBar: AppBar(
        title: TextField(
          onChanged: (value) {
            _debounceTimer?.cancel();
            _debounceTimer = Timer(const Duration(milliseconds: 500), () {
              ref.read(searchQueryProvider.notifier).state = value;
            });
          },
          decoration: const InputDecoration(hintText: '搜索新闻', border: InputBorder.none),
        ),
      ),
      body: PagedListView<int, News>(
        pagingController: _pagingController,
        builderDelegate: PagedChildBuilderDelegate<News>(
          itemBuilder: (context, item, index) => ListTile(
            title: Text(item.title),
            subtitle: Text(item.publishTime),
          ),
          firstPageErrorIndicatorBuilder: (_) => Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Text('加载失败:${paginationState.error.toString()}'),
                TextButton(
                  onPressed: () => ref.invalidate(newsPaginationProvider(ref.watch(searchQueryProvider))),
                  child: const Text('重试'),
                ),
              ],
            ),
          ),
          noItemsFoundIndicatorBuilder: (_) => const Center(child: Text('暂无新闻')),
          loadingIndicatorBuilder: (_) => const Center(child: CircularProgressIndicator()),
        ),
      ),
    );
  }
}

3. 额外优化建议

  • 本地缓存:对已请求的搜索词分页数据进行本地缓存,可结合Hive或SharedPreferences,减少重复API请求。
  • 状态持久化:如果需要保留用户上次的搜索分页状态,可将页码、搜索词、列表数据持久化到本地,重启APP后恢复。
  • 空状态与错误状态优化:给空状态、错误状态添加更友好的提示文案和交互,提升用户体验。

内容的提问来源于stack exchange,提问作者Tanvir Ibn Mizan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 15:45:21