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

BlocBuilder二次不重建:电影列表无限滚动分页UI不更新

问题分析与解决方案

你的核心问题在于Bloc状态更新时没有合并新旧分页数据,同时UI层的加载状态处理和列表计数存在疏漏,导致新数据无法实时刷新到UI。以下是具体修复步骤:

1. 修复SearchMovieBloc的状态更新逻辑

当前Bloc在请求成功时直接用新页数据替换了原有movies列表,且未在请求开始时设置加载状态。修改后的代码如下:

@injectable
class SearchMovieBloc extends Bloc<SearchMovieEvent, SearchMovieState> {
  final SearchMovieRepo searchmovierepo;
  SearchMovieBloc(this.searchmovierepo) : super(SearchMovieState.initial()) {
    on<SearchMovieEvent>((event, emit) async {
      // 请求开始时,先emit加载状态
      emit(state.copyWith(isLoading: true));
      
      final Either<MainFailures, List<MovieModel>> result =
          await searchmovierepo.searchmovie(
              moviequery: event.moviequery, page: event.page);
      log(result.toString());
      
      emit(result.fold(
          (failure) =>
              state.copyWith(isLoading: false, options: Some(Left(failure))),
          (newMovies) => state.copyWith(
              isLoading: false,
              // 合并原有数据和新分页数据,而非直接替换
              movies: [...state.movies, ...newMovies],
              options: Some(Right(newMovies)))));
    });
  }
}

关键改动:

  • 请求发起时立即设置isLoading: true,让UI同步显示加载状态
  • 成功时用展开运算符[...state.movies, ...newMovies]合并新旧列表,保证分页数据累加

2. 优化UI层的无限滚动逻辑

当前UI存在滚动监听重复触发请求、列表未预留加载更多item位置的问题。修改后的_SearchMovieListState代码如下:

class _SearchMovieListState extends State<SearchMovieList> {
  int page = 1;
  ScrollController controller = ScrollController();
  bool _isLoadingMore = false; // 新增标记,防止重复请求

  @override
  void initState() {
    controller.addListener(() {
      if (controller.position.maxScrollExtent == controller.offset && !_isLoadingMore) {
        _isLoadingMore = true; // 标记为正在加载,避免重复触发
        page++;
        BlocProvider.of<SearchMovieBloc>(context).add(
            SearchMovieEvent.searchmovie(
                moviequery: widget.text.text, page: page));
      }
    });
    super.initState();
  }

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

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<SearchMovieBloc, SearchMovieState>(
      builder: (context, state) {
        // 同步更新加载标记
        _isLoadingMore = state.isLoading;
        
        if (state.isLoading && state.movies.isEmpty) {
          return const Center(
            child: CircularProgressIndicator(
              color: orange,
            ),
          );
        } else if (state.movies.isEmpty && widget.text.text.isNotEmpty) {
          return const Center(
            child: Text(
              "No Results to Show",
              style: TextStyle(color: grey, fontSize: 25),
            ),
          );
        } else if (state.movies.isNotEmpty && widget.text.text.isNotEmpty) {
          return ListView.separated(
              controller: controller,
              keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
              physics: const BouncingScrollPhysics(),
              shrinkWrap: true,
              itemBuilder: ((context, itemIndex) {
                if (itemIndex < state.movies.length) {
                  return SearchTile(
                      ismovie: true,
                      overview: state.movies[itemIndex].overview!,
                      id: state.movies[itemIndex].movieid!,
                      heading: state.movies[itemIndex].title == null
                          ? state.movies[itemIndex].name!
                          : state.movies[itemIndex].title!,
                      rating:
                          state.movies[itemIndex].rating!.toStringAsFixed(1),
                      image: state.movies[itemIndex].posterPath != null
                          ? "$posterhead${state.movies[itemIndex].posterPath}"
                          : null,
                      year: state.movies[itemIndex].releasedate == null ||
                              state.movies[itemIndex].releasedate!.length < 5
                          ? "_"
                          : state.movies[itemIndex].releasedate!
                              .substring(0, 4));
                } else {
                  return const Center(
                      child: CircularProgressIndicator(color: orange));
                }
              }),
              separatorBuilder: (context, index) => const Divider(
                    height: 4.0,
                  ),
              // 列表计数加1,预留加载更多的item位置
              itemCount: state.movies.length + (state.isLoading ? 1 : 0));
        } else {
          return const SizedBox();
        }
      },
    );
  }
}

关键改动:

  • 新增_isLoadingMore标记,防止滚动到底部时重复发送请求
  • 修改itemCount为state.movies.length + (state.isLoading ? 1 : 0),让加载更多的item能够显示
  • 在BlocBuilder中同步更新_isLoadingMore状态,确保请求结束后能继续触发下一页加载

3. 补充说明

  • Freezed生成的状态类是不可变的,必须通过copyWith创建新状态,合并列表时要生成新的List实例(不能直接修改原列表)
  • 修改后滚动到底部时,Bloc会合并新数据并emit新状态,BlocBuilder会自动重建UI,无需热重载即可显示新数据

内容的提问来源于stack exchange,提问作者Sh'Jil

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.23 12:24:22