EdsonBueno / infinite_scroll_pagination

Flutter package to help you lazily load and display pages of items as the user scrolls down your screen.
https://pub.dev/packages/infinite_scroll_pagination
MIT License
605 stars 201 forks source link

_pagingController dispose issue when used inside a BLoC #315

Closed vgavrilovikj closed 4 months ago

vgavrilovikj commented 4 months ago

What happens in case where the pagingController is inside a BLoC, how to dispose it then.. This is the error I am getting:

Exception: A PagingController was used after being disposed.
Once you have called dispose() on a PagingController, it can no longer be used.
If you’re using a Future, it probably completed after the disposal of the owning widget.
Make sure dispose() has not been called yet before using the PagingController.

This is my reservations.dart where the controller is used:

import 'package:common_business/core/constants/app.dart';
import 'package:common_business/core/dto/reservations_find_all_dto.dart';
import 'package:common_business/core/utils/secure_storage_utils.dart';
import 'package:common_business/core/utils/utils.dart';
import 'package:common_business/domain/usecases/reservations/find_all.dart';
import 'package:common_business/presentation/blocs/reservations/find_all/reservations_find_all_bloc.dart';
import 'package:common_business/presentation/widgets/filter_buttons_list/filter_buttons_list.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import '../../domain/entities/reservation_list_entity.dart';
import '../../injection_container.dart';
import '../blocs/auth/auth_bloc.dart';
import '../widgets/reservations_list/reservation_list_item.dart';
import '../widgets/reservations_not_found.dart';

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

  @override
  State<ReservationsScreen> createState() => _ReservationsScreenState();
}

class _ReservationsScreenState extends State<ReservationsScreen> with WidgetsBindingObserver {
  final PagingController<int, ReservationListEntity> _pagingController =
  sl<PagingController<int, ReservationListEntity>>();
  late final ReservationsFindAllBloc _reservationsFindAllBloc;
  FilterReservationsEnum selectedFilterReservationsEnum =
      FilterReservationsEnum.ALL;
  bool isFilterClicked = false;

  void showAlert(String message) {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text("Error"),
          content: Text(message),
          actions: <Widget>[
            TextButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              child: const Text("OK"),
            ),
          ],
        );
      },
    );
  }

  @override
  void initState() {
    super.initState();
    _reservationsFindAllBloc = ReservationsFindAllBloc(
        sl<FindAllUseCase>(), sl<SecureStorage>(), _pagingController);

    _reservationsFindAllBloc.resetPageController();
    _pagingController.addPageRequestListener(scrollListener);
  }

  void updateFilter(FilterReservationsEnum filterReservationsEnum) {
    setState(() {
      selectedFilterReservationsEnum = filterReservationsEnum;
      isFilterClicked = true;

      _reservationsFindAllBloc.resetPageController();
      _pagingController.removePageRequestListener(scrollListener);
      _pagingController.addPageRequestListener(scrollListener);
    });
  }

  void scrollListener(pageKey) {
    if (isFilterClicked) {
      _reservationsFindAllBloc.resetPageController();
    }

    _reservationsFindAllBloc.add(GetReservationsFindAll(
        reservationsFindAllDto: ReservationsFindAllDto(
          page: pageKey,
          filterReservationsEnum: selectedFilterReservationsEnum,
        ),
        isFilterClicked: isFilterClicked));

    if (isFilterClicked) {
      isFilterClicked = false;
    }
  }

  @override
  void dispose() {
    _pagingController.removePageRequestListener(scrollListener);
    _pagingController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
      statusBarColor: Colors.transparent,
      systemNavigationBarColor: Colors.white,
    ));

    return MultiBlocProvider(
      providers: [
        BlocProvider<ReservationsFindAllBloc>.value(
            value: _reservationsFindAllBloc)
      ],
      child: Scaffold(
        backgroundColor: colorGrayLightest,
        body: Padding(
          padding: const EdgeInsets.only(top: 10, left: 10, right: 10),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              SizedBox(
                height: MediaQuery.of(context).viewPadding.top,
              ),
              Text("Резервации", style: font28Medium),
              Container(
                height: 0.065.sh,
                padding: const EdgeInsets.only(top: 8),
                child: FilterButtonsList(
                  updateFilter: updateFilter,
                ),
              ),
              Expanded(
                child: BlocBuilder<ReservationsFindAllBloc,
                    ReservationsFindAllState>(
                  builder: (_, state) {
                    if (state is ReservationsError) {
                      if (state.exception != null) {
                        if (state.exception!.requestOptions.extra
                            .containsKey('isLogout')) {
                          BlocProvider.of<AuthBloc>(context)
                              .add(const LogoutEvent());
                        } else if (state.exception?.response != null) {
                          Future.microtask(() {
                            showAlert(Utils.getErrorResponseMessage(
                                state.exception!.response!));
                          });
                        }
                      } else {
                        Future.microtask(() {
                          showAlert(Utils.getErrorResponseMessage(null));
                        });
                      }
                    }

                    return PagedListView<int, ReservationListEntity>(
                      pagingController: _pagingController,
                      shrinkWrap: true,
                      padding: EdgeInsets.zero,
                      builderDelegate:
                      PagedChildBuilderDelegate<ReservationListEntity>(
                        itemBuilder: (context, item, index) {
                          return ReservationListItem(item);
                        }, noItemsFoundIndicatorBuilder: (context) {
                        return const NotFoundReservationsWidget();
                      },
                        firstPageProgressIndicatorBuilder: (context) => Container(
                          height: 100,
                          alignment: Alignment.center,
                          child: const CircularProgressIndicator(
                            valueColor: AlwaysStoppedAnimation<Color>(colorGrayDark), // Change color here
                          ),
                        ),
                        newPageProgressIndicatorBuilder: (context) => Container(
                          height: 100,
                          alignment: Alignment.center,
                          child: const CircularProgressIndicator(
                            valueColor: AlwaysStoppedAnimation<Color>(colorGrayDark), // Change color here
                          ),
                        ),),
                    );
                  },
                ),
              ),
            ],
          ),
        ),
        floatingActionButton: Padding(
          padding: const EdgeInsets.only(bottom: 70.0),
          child: FloatingActionButton(
            onPressed: () {
              print("FAB Pressed");
            },
            foregroundColor: colorWhite,
            backgroundColor: colorRedLight,
            child: const Icon(Icons.add),
          ),
        ),
      ),
    );
  }
}

and this is the bloc:

import 'package:bloc/bloc.dart';
import 'package:common_business/core/dto/reservations_find_all_dto.dart';
import 'package:common_business/core/resources/data_state.dart';
import 'package:common_business/core/utils/secure_storage_utils.dart';
import 'package:common_business/domain/entities/reservation_list_entity.dart';
import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:meta/meta.dart';
import '../../../../domain/usecases/reservations/find_all.dart';

part 'reservations_find_all_event.dart';

part 'reservations_find_all_state.dart';

class ReservationsFindAllBloc
    extends Bloc<ReservationsFindAllEvent, ReservationsFindAllState> {
  final FindAllUseCase _findAllUseCase;
  final SecureStorage _secureStorage;
  final PagingController<int, ReservationListEntity> _pagingController;
  bool _isDisposed = false;

  ReservationsFindAllBloc(
      this._findAllUseCase, this._secureStorage, this._pagingController)
      : super(const ReservationsLoading()) {
    on<GetReservationsFindAll>(onFindAll);
  }

  void onFindAll(GetReservationsFindAll getReservationsFindAll,
      Emitter<ReservationsFindAllState> emit) async {
    emit(const ReservationsLoading());
    String? nextStartDate;
    ReservationsFindAllDto? reservationsFindAllDto =
        getReservationsFindAll.reservationsFindAllDto;

    if (getReservationsFindAll.isFilterClicked != null &&
        getReservationsFindAll.isFilterClicked! == true) {
      resetPageController();
    } else {
      nextStartDate = await _secureStorage.getReservationsNextStartDate();
    }

    ReservationsFindAllDto? newReservationsFindAllDto = ReservationsFindAllDto(
        filterReservationsEnum: reservationsFindAllDto?.filterReservationsEnum,
        limit: reservationsFindAllDto?.limit,
        page: reservationsFindAllDto?.page ?? 1,
        nextStartDate: nextStartDate);

    final dataState =
        await _findAllUseCase.call(params: newReservationsFindAllDto);

    if (dataState is DataSuccess) {
      final newData = dataState.data!;
      final isLastPage = newData.reservations!.isEmpty;
      final int page =
          getReservationsFindAll.reservationsFindAllDto?.page != null
              ? getReservationsFindAll.reservationsFindAllDto!.page
              : 1;

      if (isLastPage) {
        _pagingController.appendLastPage(newData.reservations!);
      } else {
        _pagingController.appendPage(newData.reservations!, page + 1);
      }

      emit(ReservationsLoaded(newData.reservations!));
    }

    if (dataState is DataFailed) {
      emit(ReservationsError(dataState.exception!));
    }
  }

  void resetPageController() async {
    await _secureStorage.deleteNextStartDate();
    _pagingController.refresh();
    _pagingController.value = const PagingState(
      nextPageKey: 1,
      error: null,
      itemList: null,
    );
  }
}

_Originally posted by @vgavrilovikj in https://github.com/EdsonBueno/infinite_scroll_pagination/issues/64#issuecomment-1966186068_

clragon commented 4 months ago

Hi, it seems you are experiencing issues with combining the package with the rest of your codebase. The github issues are majorly for problems or feature requests with the package. If you need support with your code, please check out the community resources instead.

coder-gogo commented 1 month ago

im facing same issue as above when i come in to screen where pagination is used and go back before the data loads from api at that time pagecontroller is disposed but data come from api assigned to page controller

im using bloc have you found fix @vgavrilovikj