I'm trying to get a list of movies from api but when implementing infinite scroll pagination using ListView ScrollController infinite scroll is not working. I've checked the output from state also it is getting correctly when scroll reaches end of list but not updating in UI. only after i hot reload it shows in the UI.

Here is the Blocfile.

@injectable
class SearchMovieBloc extends Bloc<SearchMovieEvent, SearchMovieState> {
  final SearchMovieRepo searchmovierepo;
  SearchMovieBloc(this.searchmovierepo) : super(SearchMovieState.initial()) {
    on<SearchMovieEvent>((event, emit) async {
      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))),
          (success) => state.copyWith(
              isLoading: false,
              movies: success,
              options: Some(Right(success)))));
    });
  }
}

SearchMovieState

@freezed
class SearchMovieState with _$SearchMovieState {
  const factory SearchMovieState(
          {required bool isLoading,
          required List<MovieModel> movies,
          required Option<Either<MainFailures, List<MovieModel>>> options}) =
      _SearchMovieState;

  factory SearchMovieState.initial() =>
      const SearchMovieState(isLoading: false, options: None(), movies: []);
}

SearchMovieEvent

@freezed
class SearchMovieEvent with _$SearchMovieEvent {
  const factory SearchMovieEvent.searchmovie(
      {required String moviequery, required int page}) = _SearchMovie;
}

and the UI

class SearchMovieList extends StatefulWidget {
  TextEditingController text;
  SearchMovieList({
    Key? key,
    required this.text,
  }) : super(key: key);

  @override
  State<SearchMovieList> createState() => _SearchMovieListState();

It's been two days i've working on this issue hope someone helps me. }

class _SearchMovieListState extends State<SearchMovieList> {
  int page = 1;
  ScrollController controller = ScrollController();

  @override
  void initState() {
    controller.addListener(() {
      if (controller.position.maxScrollExtent == controller.offset) {
        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) {
        if (state.isLoading) {
          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) {
          /*  log(state.movies.toString()); */
          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,
                  ),
              itemCount: state.movies.length);
        } else {
          return const SizedBox();
        }
      },
    );
  }
}


Solution 1: DiyorbekDev

use the equatable package or write the == operator manually. However, I have not tried using the freezed package together with equatable

class SearchMovieState extends Equatable {
  const SearchMovieState(
      {required this.isLoading, required this.movies, required this.options});
  final bool isLoading;
  final List<MovieModel> movies;
  final Option<Either<MainFailures, List<MovieModel>>> options;
  @override
  List<Object?> get props => [isLoading, movies, options];
}


Solution 2: Yeasin Sheikh

I am using equatable. For my case, props wasn't enough to emit the same state. I had to include identityHashCode on props.

abstract class TestState extends Equatable {
  const TestState();
  @override
  List<Object?> get props => [];
}

class ErrorState extends TestState{
  final Map? message;
  const ErrorState({this.message});
  @override
  List<Object?> get props => [message, identityHashCode(this)];
}


Solution 3: Leandro González Cuello

I recommend using Equatable https://pub.dev/packages/equatable to override the == operator. So instead of checking for the same address, Equatable checks if the object and its values are equal.

And you have to be sure that you are requesting more data and not the same data

I recommend you debug this line of code

final Either<MainFailures, List<MovieModel>> result = await searchmovierepo.searchmovie(moviequery: event.moviequery, page: event.page);

Just to be sure that the data is different


Solution 4: Leskeboy

Use a fancy bool operator to trick the block to emit the state. use the below code.

In your state add a new field change state and initialize with false.

@freezed
class SearchMovieState with _$SearchMovieState {
  const factory SearchMovieState(
          {required bool isLoading,
          required bool changeState, // add this field here
          required List<MovieModel> movies,
          required Option<Either<MainFailures, List<MovieModel>>> options}) =
      _SearchMovieState;

  factory SearchMovieState.initial() =>
      const SearchMovieState(isLoading: false, changeState: false, options: None(), movies: []); // initialize the changefield with false
}

on the BlockFile invert the changeState on success

emit(result.fold(
    (failure) =>
        state.copyWith(isLoading: false, options: Some(Left(failure))),
    (success) => state.copyWith(
        isLoading: false,
        changeState: !state.changeState, // toggle the changeStatefield
        movies: success,
        options: Some(Right(success)))));

This Worked for me.