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

如何在Flutter中用BLoC Cubit实现分页?旧数据消失问题解决

Flutter分页加载后旧数据消失的问题排查与解决

我在Flutter应用中实现分页功能,定义了movies列表存储API获取的指定页码数据,用ListView展示。通过ScrollController监听滚动,滚动到底部时更改页码重新调用接口,打算把新数据追加到旧列表中,但加载新数据后旧数据消失了,请问问题出在哪?该怎么解决?


相关代码

UI组件代码

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:movie_app/res/constants/colors.dart';
import 'package:movie_app/ui/pages/home_page/home_cubit/home_cubit/top_rated_bloc/top_rated_cubit.dart';

import '../../../models/movies.dart';
import '../../bloc_cubit/movies_status.dart';

class TopRatedMovies extends StatefulWidget {
  const TopRatedMovies({Key? key}) : super(key: key);

  @override
  State<TopRatedMovies> createState() => _TopRatedMoviesState();
}

class _TopRatedMoviesState extends State<TopRatedMovies> {
  ScrollController scrollController = ScrollController();
  List<Movies> movies = [];

  @override
  void initState() {
    BlocProvider.of<TopRatedCubit>(context).getTopRatedMovies();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: MyColor.primaryColor,
      appBar: AppBar(
        backgroundColor: MyColor.primaryColor,
        title: const Text('TOP RATED MOVIES'),
      ),
      body: BlocConsumer<TopRatedCubit, TopRatedState>(
        listener: (context, state){},
        builder: (context, state){
          var cubit = BlocProvider.of<TopRatedCubit>(context);

          movies = state.topRated;
          
          switch(state.status){
            case MoviesStatus.loading:
              return const Center(child: CircularProgressIndicator());
            case MoviesStatus.initial:
            case MoviesStatus.success:
            case MoviesStatus.failure:
              return ListView.separated(
                controller: scrollController..addListener(() {
                  if(scrollController.position.pixels == scrollController.position.maxScrollExtent){
                    cubit.currentPage++;
                    BlocProvider.of<TopRatedCubit>(context).getTopRatedMovies();
                    movies = [...movies, ...state.topRated];
                  }
                }),
                itemCount: movies.length,
                itemBuilder: (context, index){
                  return Text(
                    '${movies[index].title}',style: const TextStyle(color: Colors.white),
                  );
                },
                separatorBuilder: (context, index){
                  return const SizedBox(height: 40,);
                }
              );
          }
        },
      ),
    );
  }
}

Cubit代码

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/cupertino.dart';
import 'package:meta/meta.dart';

import '../../../../../../models/movies.dart';
import '../../../../../../repositories/movies_repo.dart';
import '../../../../../bloc_cubit/movies_status.dart';

part 'top_rated_state.dart';

class TopRatedCubit extends Cubit<TopRatedState> {
  TopRatedCubit({required this.moviesRepo}) : super(const TopRatedState());

  final MoviesRepo moviesRepo;
  int currentPage = 1;

  Future<List<Movies>> getTopRatedMovies() async{
    emit(
      state.copyWith(
        status: MoviesStatus.loading,
      )
    );

    moviesRepo.getTopRatedMovies(page: currentPage).then((topRatedMovies) {
      emit(
        state.copyWith(
          status: MoviesStatus.success,
          topRated: topRatedMovies
        ),
      );
    }).catchError((error){
      emit(
        state.copyWith(
          status: MoviesStatus.failure,
          exception: error
        )
      );
    });
    return [];
  }
}

问题原因与解决方法

1. Cubit中未累加历史数据(核心问题)

每次调用getTopRatedMovies获取新页数据时,直接用新数据覆盖了state.topRated,没有把新数据追加到旧数据列表里,导致UI层拿到的永远是当前页的新数据,旧数据丢失。

修复Cubit代码:

moviesRepo.getTopRatedMovies(page: currentPage).then((topRatedMovies) {
  emit(
    state.copyWith(
      status: MoviesStatus.success,
      // 合并旧数据与新数据
      topRated: [...state.topRated, ...topRatedMovies]
    ),
  );
})

2. UI层的movies赋值逻辑错误

在BlocConsumer的builder里直接movies = state.topRated会覆盖本地列表,且滚动监听里的movies = [...movies, ...state.topRated]无效——Bloc状态更新后会重新触发builder,该赋值会被覆盖。

修复UI层逻辑:
不需要维护本地movies列表,直接使用state.topRated:

builder: (context, state){
  var cubit = BlocProvider.of<TopRatedCubit>(context);
  
  switch(state.status){
    case MoviesStatus.loading:
      return const Center(child: CircularProgressIndicator());
    case MoviesStatus.initial:
    case MoviesStatus.success:
    case MoviesStatus.failure:
      return ListView.separated(
        controller: scrollController,
        // 直接使用state中的数据
        itemCount: state.topRated.length,
        itemBuilder: (context, index){
          return Text(
            '${state.topRated[index].title}',style: const TextStyle(color: Colors.white),
          );
        },
        separatorBuilder: (context, index){
          return const SizedBox(height: 40,);
        }
      );
  }
}

3. 滚动监听重复绑定问题

在ListView.separated的controller里每次builder都添加监听,会导致重复绑定多个监听,触发多次加载。应把监听添加到initState中:

修复滚动监听:

@override
void initState() {
  super.initState();
  BlocProvider.of<TopRatedCubit>(context).getTopRatedMovies();
  // 仅在初始化时添加一次监听
  scrollController.addListener(() {
    if(scrollController.position.pixels == scrollController.position.maxScrollExtent){
      var cubit = BlocProvider.of<TopRatedCubit>(context);
      cubit.currentPage++;
      cubit.getTopRatedMovies();
    }
  });
}

// 销毁时释放controller资源
@override
void dispose() {
  scrollController.dispose();
  super.dispose();
}

4. 可选优化:避免重复触发加载

在Cubit中添加isLoading状态,防止滚动到底部时多次触发API请求:

class TopRatedCubit extends Cubit<TopRatedState> {
  TopRatedCubit({required this.moviesRepo}) : super(const TopRatedState());

  final MoviesRepo moviesRepo;
  int currentPage = 1;
  bool isLoading = false; // 新增加载状态标记

  Future<List<Movies>> getTopRatedMovies() async{
    if(isLoading) return []; // 正在加载时直接返回,避免重复请求
    isLoading = true;
    emit(
      state.copyWith(
        status: MoviesStatus.loading,
      )
    );

    moviesRepo.getTopRatedMovies(page: currentPage).then((topRatedMovies) {
      emit(
        state.copyWith(
          status: MoviesStatus.success,
          topRated: [...state.topRated, ...topRatedMovies]
        ),
      );
      isLoading = false;
    }).catchError((error){
      emit(
        state.copyWith(
          status: MoviesStatus.failure,
          exception: error
        )
      );
      isLoading = false;
    });
    return [];
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 05:35:42