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