secondaryBody SlotLayout "Cannot emit new states after calling close"

27 Views Asked by At

I am creating an app that has small and mediumAndUp displays I want a List-Detail structure that behaves like this in small :

  • Display list of items
  • when tap on an item push a new screen to show the detail

in mediumAndUp:

  • Display in half the screen the list of items, and in the other half of the screen the details
  • when tap on an item, update the other half of the screen with the details uf such item

I am using flutter_adaptive_scaffold library

so far This is my code:

home_page.dart

class HomePage extends StatefulWidget {
  HomePage({
    super.key,
    required String tab,
  }): index = tabs.indexWhere((element) => element.name == tab);

  static const pageConfig = PageConfig(
    icon: Icons.home,
    name: homeRoute,
  );

  final int index;

  static const tabs = [
    RacesPage.pageConfig,
    ReportsPage.pageConfig,
  ];

  @override
  State<HomePage> createState() => HomePageState();
}

class HomePageState extends State<HomePage> {

  final destinations = HomePage.tabs.map(
      (page) => NavigationDestination(
          icon: Icon(page.icon),
          label: page.name,
      )
  ).toList();

  @override
  Widget build(BuildContext context) {
  ...
    return Scaffold(
      body: SafeArea(
        child: AdaptiveLayout(
          primaryNavigation: SlotLayout(... ),
          bottomNavigation: SlotLayout(...),
          body: SlotLayout(
            config: <Breakpoint, SlotLayoutConfig>{
              Breakpoints.small: SlotLayout.from(
                  key: const Key('primary-body-small'),
                  builder: (_) => Scaffold(
                    appBar: AppBar(
                      title: const Text('Centinelas'),
                      automaticallyImplyLeading: false,
                      elevation: 8.0,
                      actions: [
                        Padding(
                            padding: const EdgeInsets.only(right: 20.0),
                            child: GestureDetector(
                              onTap: (){
                                context.goNamed(ProfilePage.pageConfig.name,);
                              },
                              child: const Icon(
                                Icons.person_2_rounded,
                                size: 26.0,
                              ),
                            )
                        ),
                      ],
                    ),
                    body: HomePage.tabs[widget.index].child,
                  )
              ),
              Breakpoints.mediumAndUp: SlotLayout.from(
                  key: const Key('primary-body-medium-up'),
                  builder: (_) => HomePage.tabs[widget.index].child
              ),
            },
          ),
          secondaryBody: SlotLayout(
            config: <Breakpoint, SlotLayoutConfig>{
              Breakpoints.mediumAndUp: SlotLayout.from(
                  key: const Key('secondary-body-medium'),
                  builder: widget.index != 0 ? null : (_) =>
                      BlocBuilder<NavigationCubit, NavigationCubitState>(
                        builder: (context, state) {
                          final selectedRaceId = state.selectedRaceId;
                          final isSecondBodyDisplayed = Breakpoints.mediumAndUp.isActive(context);

                          context.read<NavigationCubit>().secondBodyHasChanged(
                            isSecondBodyDisplayed: isSecondBodyDisplayed,
                          );

                          if(selectedRaceId == null){
                            return const Center();
                          }
                          return RaceDetailPageProvider(
                            key: Key(selectedRaceId.value),
                            raceEntryIdString: selectedRaceId.value,
                          );
                        },
                      ),
              )
            },
          ),
        ),
      ),
    );
  }
void tapOnNavigationDestination(BuildContext context, int index) =>
      context.go('/$homeRoute/${HomePage.tabs[index].name}');
}

navigation_cubit.dart

class NavigationCubit extends Cubit<NavigationCubitState>{
  NavigationCubit(): super(const NavigationCubitState());

  void selectedRaceChanged(RaceEntryId raceEntryId){
    emit(NavigationCubitState(selectedRaceId: raceEntryId));
  }

  void secondBodyHasChanged({required bool isSecondBodyDisplayed}) {
    if (state.isSecondBodyDisplayed != isSecondBodyDisplayed) {
      emit(NavigationCubitState(
        isSecondBodyDisplayed: isSecondBodyDisplayed,
      ));
    }
  }
}

routes.dart

final routes = GoRouter(
  navigatorKey: rootNavigatorKey,
  initialLocation: '/${SessionPage.pageConfig.name}',
  observers: [GoRouterObserver()],
  routes: [
    GoRoute(
      name: SessionPage.pageConfig.name,
      path: '/${SessionPage.pageConfig.name}',
      builder: (context, state) => BlocProvider<auth.AuthCubit>(
        create: (context) => serviceLocator<auth.AuthCubit>(),
        child: const SessionPage(),
      ),
    ),
    GoRoute(
      name: LoginPage.pageConfig.name,
      path: '/${LoginPage.pageConfig.name}',
      builder: (context, state) => SignInScreen(...),
    GoRoute(
      name: ProfilePage.pageConfig.name,
      path: '/${ProfilePage.pageConfig.name}',
      builder: (context, state) => Scaffold(...),
    ),
    ShellRoute(
      navigatorKey: shellNavigatorKey,
      builder: (context, state, child) => child,
      routes: [
        GoRoute(
          name: HomePage.pageConfig.name,
          path: '/$homeRoute/:tab',
          builder: (context, state) => HomePage(
            key: state.pageKey,
            tab: state.pathParameters['tab']!,
          ),
        ),
      ],
    ),
    GoRoute(
      name: RaceDetailPage.pageConfig.name,
      path: '/$homeRoute/$racesRoute/:raceEntryId',
      builder: (context, state) {
        return BlocListener<NavigationCubit, NavigationCubitState>(
          listenWhen: (previous, current) => previous.isSecondBodyDisplayed != current.isSecondBodyDisplayed,
          listener: (context, state){
            if(context.canPop() && (state.isSecondBodyDisplayed ?? false )){
              context.pop();
            }
          },
          child: Scaffold(
            appBar: AppBar(
              title: const Text('Detalles de carrera'),
              leading: BackButton(
                onPressed: (){
                  if(context.canPop()){
                    context.pop();
                  } else {
                    context.goNamed(
                      HomePage.pageConfig.name,
                      pathParameters: {'tab' : RacesPage.pageConfig.name},
                    );
                  }
                },
              ),
            ),
            body: RaceDetailPageProvider(
              raceEntryIdString: state.pathParameters['raceEntryId'] ?? '',
            ),
          ),
        );
      }
    ),
  ],
);

race_entry_item_view_loaded.dart

class RaceEntryItemViewLoaded extends StatelessWidget {
  const RaceEntryItemViewLoaded({
    super.key,
    required this.raceEntry,
  });

  final RaceEntry raceEntry;

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<NavigationCubit, NavigationCubitState>(
      buildWhen: (previous, current) =>
      previous.selectedRaceId != current.selectedRaceId,
      builder: (context, state){
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Material(
            elevation: 8.0,
            shape: RoundedRectangleBorder(borderRadius:BorderRadius.circular(8.0)),
            child: Container(
              width: double.infinity,
              decoration: BoxDecoration(...),
              child: ListTile(... ),
                onTap: (){
                  debugPrint('onTap race:${raceEntry.id.value}');
                  context.read<NavigationCubit>().selectedRaceChanged(raceEntry.id);
                  if(Breakpoints.small.isActive(context)){
                    debugPrint('small: ${raceEntry.id.value}');
                    context.pushNamed(
                      RaceDetailPage.pageConfig.name,
                      pathParameters: {
                        'raceEntryId': raceEntry.id.value.toString(),
                      },
                    );
                  }
                },
              ),
            ),
          ),
        );
      },
    );
  }
}

race_detail_bloc.dart

class RaceDetailBloc extends Bloc<RaceDetailEvent, RaceDetailState> {
  RaceDetailBloc({
    required this.loadRaceFullUseCase,
  }) : super(const RaceDetailLoadingState());

  final LoadRaceFullUseCase loadRaceFullUseCase;

  Future<void> readRaceFull(RaceEntryId raceEntryId) async{
    emit(const RaceDetailLoadingState());
    try{
      final raceFull = await loadRaceFullUseCase.call(raceEntryId);
      if(raceFull.isRight){
        emit(const RaceDetailErrorState());
      } else {
        emit(RaceDetailLoadedState(raceFull: raceFull.left));
      }
    }catch(exception) {
      debugPrint(exception.toString());
      emit(const RaceDetailErrorState());
    }
  }

  @override
  Stream<int> mapEventToState(RaceDetailEvent event) async* {
    debugPrint('mapEventToState ${event.toString()}');
  }

  @override
  void onEvent(RaceDetailEvent event) {
    debugPrint('onEvent ${event.toString()}');
  }
}

The app works as expected in small displays correctly, but in mediumAndUp I get the next error:

I/flutter (17477): Bad state: Cannot emit new states after calling close
E/flutter (17477): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Bad state: Cannot emit new states after calling close
E/flutter (17477): #0      BlocBase.emit (package:bloc/src/bloc_base.dart:97:9)
E/flutter (17477): #1      Bloc.emit (package:bloc/src/bloc.dart:154:35)
E/flutter (17477): #2      RaceDetailBloc.readRaceFull (package:centinelas_app/application/pages/race_detail/bloc/race_detail_bloc.dart:31:7)

The odd thing is If I run the webapp And select any item from the small display, the detail is pushed correctly, then if resize the window to be mediumAndUp the detail is displayed correctly in the secondaryuBody But it will throw the exception described above if I tap a list item from mediumAndUp

0

There are 0 best solutions below