Reputation: 1392
According to Flutter's documentation a Scaffold should not be nested.
The Scaffold seems to be an important class to get a basic visual structure that defines a lot of things, like the AppBar, BottomNavigationBar, and Drawers.
Not to nest makes sense. E.g., if you have a nested Scaffold with a Drawer, then the Drawer will not overlay the BottomNavigationBar from the outer Scaffold. See an example in this issue.
So, what I try to achieve is the following:
In terms of hierarchy, the app currently looks like this (stripped down to the essential parts):
- MaterialApp
- ScaffoldBloc (BlocProvider & BlocBuilder)
- Scaffold
- IndexedStack
- ShellPage
- WidgetA
- ShellPage
- WidgetB
- ShellPage
- WidgetC
What I'm doing right now is the following:
Each ShellPage (my own Stateful widget) knows about four things: the actual Widget, as well as AppBar, Drawer and end Drawer.
When its state gets created, I send a message to the ScaffoldBloc which contains the AppBar and the Drawers.
Due to that, a BlocBuilder for the ScaffoldBloc triggers a build. That build will than use the current state's AppBar and Drawers for the Scaffold.
Basically, this approach works. However, it has the drawback, that you can see the content of WidgetA being rendered and due to the async nature of bloc "a frame later" you see the AppBar pop-in. Looks kind of ugly. See an example in this video (Dropbox, because I need MP4, in a GIF the pop-in was not visible)
Also the overall navigation handling does not seem to work well when you have nested routes that also have their own AppBar and Drawers.
I've also tried changing the structure in this way:
- MaterialApp
- IndexedStack
- WidgetA
- Scaffold
- ActualWidgetAView
- WidgetB
- Scaffold
- ActualWidgetBView
- WidgetC
- Scaffold
- ActualWidgetCView
I like this approach more because it's easier to implement. However, each Scaffold now has its own BottomNavigationBar. When you switch from WidgetA to WidgetB you see the animation starting on WidgetA's BottomNavigationBar, then WidgetB gets rendered and you don't see the animation finishing because now WidgetB "overlays" the whole screen.
Normally it should look like this:
Using a GlobalKey for the BottomNavigationBar does not work, because it will be rendered in multiple places then, for each Widget inside the IndexedStack.
I stripped it down as much as possible, removed all import
s and part
s for brevity:
// Main routes.dart file
final rootNavigatorKey = GlobalKey<NavigatorState>();
final unauthenticatedShellNavigatorKey = GlobalKey<NavigatorState>();
final authenticatedShellNavigatorKey = GlobalKey<NavigatorState>();
const _statefulShellRoute = TypedStatefulShellRoute<AppShellRouteData>(
branches: [
$leaveBranch,
$meBranch,
],
);
@TypedShellRoute<AppUnauthenticatedShellRouteData>(
routes: [
// Here are other routes, which are outside the stateful shell route, for example for signing in
// ...
_statefulShellRoute,
],
)
@immutable
class AppUnauthenticatedShellRouteData extends ShellRouteData {
const AppUnauthenticatedShellRouteData();
static final $navigatorKey = unauthenticatedShellNavigatorKey;
@override
Widget builder(final BuildContext context, final GoRouterState state, final Widget navigator) =>
navigator;
}
@immutable
class AppShellRouteData extends StatefulShellRouteData {
const AppShellRouteData();
@override
Widget builder(final BuildContext context,
final GoRouterState state,
final StatefulNavigationShell navigationShell) => navigationShell;
static Widget $navigatorContainerBuilder(final BuildContext context,
final StatefulNavigationShell navigationShell,
final List<Widget> children,) =>
ShellNavigator(
navigationShell: navigationShell,
navigationItems: [
ShellNavigationItem(
label: 'Leave',
icon: FontAwesomeIcons.lightClock,
),
ShellNavigationItem(
label: 'Me',
icon: FontAwesomeIcons.lightUser,
),
],
children: children,
);
}
final mainRouter = GoRouter(
initialLocation: LeaveDashboardRoute().location,
routes: $appRoutes,
navigatorKey: rootNavigatorKey,
);
// Branch configuration for "leave"
const _leaveUrlPart = '/leave';
const $leaveBranch = TypedStatefulShellBranch(
routes: [
TypedGoRoute<LeaveDashboardRoute>(path: '$_leaveUrlPart/dashboard', routes: [
TypedGoRoute<NestedTestRoute>(
path: 'nested-test',
),
]),
],
);
@immutable
class LeaveDashboardRoute extends GoRouteData {
@override
Widget build(final BuildContext context, final GoRouterState state) => const Dashboard();
}
@immutable
class NestedTestRoute extends GoRouteData {
@override
Widget build(final BuildContext context, final GoRouterState state) => const Placeholder();
}
// Branch configuration for "me"
const _meUrlPart = '/me';
const $meBranch = TypedStatefulShellBranch(
routes: [
TypedGoRoute<MeDashboardRoute>(
path: '$_meUrlPart/dashboard',
),
],
);
@immutable
class MeDashboardRoute extends GoRouteData {
@override
Widget build(final BuildContext context, final GoRouterState state) => const Me();
}
// BLoC
class ShellBloc extends Bloc<ShellEvent, ShellState> {
final StatefulNavigationShell navigationShell;
ShellBloc({
required this.navigationShell,
}) : super(
ShellState(),
) {
on<ShellUpdate>(_updateAppBar);
on<ShellNavigateToBranch>(_navigateToBranch);
}
FutureOr<void> _updateAppBar(final ShellUpdate event, final Emitter<ShellState> emit) {
emit(
state.copyWithComponents(
navigationIndex: event.navigationIndex,
endDrawer: event.endDrawer,
drawer: event.drawer,
appBar: event.appBar,
),
);
}
FutureOr<void> _navigateToBranch(
final ShellNavigateToBranch event,
final Emitter<ShellState> emit,
) {
emit(state.copyWith(navigationIndex: event.index));
navigationShell.goBranch(
event.index,
initialLocation: event.index == navigationShell.currentIndex,
);
}
}
// State
class ShellComponents {
final Drawer? drawer;
final Drawer? endDrawer;
final AppBar? appBar;
const ShellComponents({
this.drawer,
this.endDrawer,
this.appBar,
});
}
@immutable
class ShellState extends Equatable {
final Map<int, ShellComponents> shellComponents;
final int navigationIndex;
ShellState()
: shellComponents = {},
navigationIndex = 0;
const ShellState._({
required this.navigationIndex,
required this.shellComponents,
});
AppBar? get appBar => shellComponents[navigationIndex]?.appBar;
Drawer? get drawer => shellComponents[navigationIndex]?.drawer;
Drawer? get endDrawer => shellComponents[navigationIndex]?.endDrawer;
@override
List<Object?> get props => [navigationIndex, shellComponents];
ShellState copyWithComponents({
required final int navigationIndex,
final AppBar? appBar,
final Drawer? drawer,
final Drawer? endDrawer,
}) {
final shellComponents = Map<int, ShellComponents>.from(this.shellComponents);
final components = ShellComponents(
appBar: appBar,
drawer: drawer,
endDrawer: endDrawer,
);
shellComponents.update(
navigationIndex,
(final value) => components,
ifAbsent: () => components,
);
final state = ShellState._(
navigationIndex: navigationIndex,
shellComponents: Map.from(shellComponents),
);
return state;
}
ShellState copyWith({
required final int navigationIndex,
}) {
final shellComponents = Map<int, ShellComponents>.from(this.shellComponents);
final state = ShellState._(
navigationIndex: navigationIndex,
shellComponents: Map.from(shellComponents),
);
return state;
}
}
// Events
@immutable
abstract class ShellEvent {
const ShellEvent();
}
class ShellUpdate extends ShellEvent {
final Drawer? drawer;
final Drawer? endDrawer;
final AppBar? appBar;
final int navigationIndex;
const ShellUpdate({
required this.navigationIndex,
this.appBar,
this.drawer,
this.endDrawer,
});
}
class ShellNavigateToBranch extends ShellEvent {
final int index;
const ShellNavigateToBranch({required this.index});
}
class ShellNavigator extends StatelessWidget {
final StatefulNavigationShell navigationShell;
final List<Widget> children;
final List<ShellNavigationItem> navigationItems;
const ShellNavigator({
super.key,
required this.navigationShell,
required this.children,
required this.navigationItems,
});
@override
Widget build(final BuildContext context) => BlocProvider(
create: (final context) => ShellBloc(
navigationShell: navigationShell,
),
child: _ShellNavigator(
navigationItems: navigationItems,
navigationShell: navigationShell,
children: children,
),
);
}
class _ShellNavigator extends StatelessWidget {
final StatefulNavigationShell navigationShell;
final List<Widget> children;
final List<ShellNavigationItem> navigationItems;
const _ShellNavigator({
required this.navigationShell,
required this.children,
required this.navigationItems,
});
@override
Widget build(final BuildContext context) => BlocBuilder<ShellBloc, ShellState>(
builder: (final context, final state) => Scaffold(
appBar: state.appBar,
bottomNavigationBar: ShellBottomNavigationBar(
navigationShell: navigationShell,
items: navigationItems,
),
drawer: state.drawer,
endDrawer: state.endDrawer,
body: IndexedStack(
index: navigationShell.currentIndex,
children: children
.mapIndexed(
(final index, final element) => RepositoryProvider(
create: (final context) => NavigationIndexCubit(index: index),
child: element,
),
)
.toList(growable: false),
),
// body: child,
),
);
}
Personally, I think Idea 2 is better, because it requires much less code. The only issue is the animation for the BottomNavigatioBar
. If that somehow is solvable, that would be perfect. As written above, using a GlobalKey
for BottomNavigationBar
does not work, because it is inserted in multiple children of the IndexedStack
. If it somehow would be possible to move the BottomNavigationBar
from one Widget to another when the user presses the BottomNavigationBar
, then the animation issue would be solved.
For me, this UseCase seems pretty standard for a mobile app and I'm not sure, if my approach is completely wrong or if I'm doing something else wrong.
I also found an issue in Flutter's GitHub repository, asking basically the same thing.
Upvotes: 2
Views: 1030
Reputation: 37
The accepted approach of @Cabdirashiid have some weak sides.
It is only working with [ShellRoute] but not [StatefulShellRoute] especially .indexedStack. This causes to lose your state whenever screen changes. While we can not use it with .indexedStack there is so much side effects of this approach.
Every screen must be [StatefulWidget]. To remove the appbar which you added last, you should clean it on the dispose method of widget. Using [StatefulWidget] brings you setState function but this function should not be accessible if it not needed. So you need an extra package like [flutter_hook].
It separates state of body and Scaffold properties in the root level. So we need to think about the Scaffold state constantly, did you added them ? forgot the remove them ? This brings complexity to our codebase, and open for problems.
I used it so short of time and saw these side effects maybe there is more which i did not see.
So there for i build an Example App that does not contain any of these side effects. You can keep states even changing screen, all screens can be stateless and you only will think about navigation the rest is handle by the cubit.
The App almost 500 lines of codes so i will put a single dart file codes here, you can copy and paste them to your main and try.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
///
/// Start Of The [goRouter]
final goRouter = GoRouter(
initialLocation: '/screen1',
routes: [
StatefulShellRoute.indexedStack(
pageBuilder: (context, state, navigationShell) => NoTransitionPage(
child: NavigatorBar(
state,
navigationShell,
[
Screen1Route.instance,
Screen2Route.instance,
SubScreen2Route.instance,
],
),
),
branches: [
StatefulShellBranch(routes: [Screen1Route.instance]),
StatefulShellBranch(routes: [Screen2Route.instance]),
],
),
Screen3Route.instance,
],
);
///
/// Start Of The [main]
void main() => runApp(const App());
///
/// Start Of The [App]
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
debugShowCheckedModeBanner: false,
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.dark,
routerConfig: goRouter,
);
}
}
///
/// Start Of The [NavigatorBar]
class NavigatorBar extends StatelessWidget {
const NavigatorBar(
this.goRouterState,
this.navigationShell,
this.subGoRouteList, {
super.key,
});
final List<NavigatorBarSubGoRoute> subGoRouteList;
final GoRouterState goRouterState;
final StatefulNavigationShell navigationShell;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => NavigatorBarCubit(
goRouterState: goRouterState,
subRoutes: subGoRouteList,
),
child: Builder(builder: (context) {
context.read<NavigatorBarCubit>().goRouterStateUpdated(goRouterState);
return BlocBuilder<NavigatorBarCubit, NavigatorBarCubitState>(
builder: (context, state) {
final selectedSubRoute = state.selectedSubRoute;
return Scaffold(
appBar: selectedSubRoute?.appBar,
drawer: selectedSubRoute?.drawer,
endDrawer: selectedSubRoute?.endDrawer,
body: navigationShell,
bottomNavigationBar: BottomNavigationBar(
currentIndex: navigationShell.currentIndex,
onTap: (i) {
navigationShell.goBranch(
i,
// If user pressing the bottom button when its already open
// it probably means user want to go all the way back.
// This code doing it.
initialLocation: i == navigationShell.currentIndex,
);
},
selectedItemColor: Colors.green,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Screen 1',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Screen 2',
),
],
),
);
});
}),
);
}
}
class NavigatorBarCubit extends Cubit<NavigatorBarCubitState> {
NavigatorBarCubit({
required GoRouterState goRouterState,
required List<NavigatorBarSubGoRoute> subRoutes,
}) : super(
NavigatorBarCubitState(
goRouterState: goRouterState,
subRoutes: subRoutes,
),
);
void goRouterStateUpdated(GoRouterState goRouterState) {
final subRoute = state.subRoutes
.where((element) => element.name == goRouterState.topRoute!.name)
.firstOrNull;
emit(
state.copyWith(goRouterState: goRouterState, selectedSubRoute: subRoute),
);
}
}
class NavigatorBarCubitState {
const NavigatorBarCubitState({
required this.goRouterState,
required this.subRoutes,
this.selectedSubRoute,
});
final GoRouterState goRouterState;
final List<NavigatorBarSubGoRoute> subRoutes;
final NavigatorBarSubGoRoute? selectedSubRoute;
NavigatorBarCubitState copyWith({
GoRouterState? goRouterState,
List<NavigatorBarSubGoRoute>? subRoutes,
NavigatorBarSubGoRoute? selectedSubRoute,
}) {
return NavigatorBarCubitState(
goRouterState: goRouterState ?? this.goRouterState,
subRoutes: subRoutes ?? this.subRoutes,
selectedSubRoute: selectedSubRoute ?? this.selectedSubRoute,
);
}
}
abstract class NavigatorBarSubGoRoute extends GoRoute {
NavigatorBarSubGoRoute({
required super.path,
required super.name,
required super.pageBuilder,
this.appBar,
this.drawer,
this.endDrawer,
});
@override
List<NavigatorBarSubGoRoute> get routes => [];
final PreferredSizeWidget? appBar;
final Widget? drawer;
final Widget? endDrawer;
}
///
/// Start Of The [Screen1]
class Screen1 extends StatelessWidget {
const Screen1({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('SubScreen of Screen 2 :'),
TextButton(
onPressed: () {
SubScreen2Route.instance.push(context);
},
child: const Text('Push'),
),
const Text('different then'),
TextButton(
onPressed: () {
SubScreen2Route.instance.go(context);
},
child: const Text('Go'),
),
],
),
],
);
}
}
class Screen1Route extends NavigatorBarSubGoRoute {
factory Screen1Route() {
return instance;
}
Screen1Route._()
: super(
path: _path,
name: _name,
pageBuilder: _pageBuilder,
appBar: const Screen1AppBar(),
drawer: const Screen1Drawer(),
);
static final Screen1Route instance = Screen1Route._();
static const String _path = '/screen1';
static const String _name = 'screen1';
static GoRouterPageBuilder get _pageBuilder =>
(context, state) => const NoTransitionPage(
child: Screen1(),
);
void go(BuildContext context) {
GoRouter.of(context).goNamed(_name);
}
void push(BuildContext context) {
GoRouter.of(context).pushNamed(_name);
}
}
class Screen1Drawer extends StatelessWidget {
const Screen1Drawer({super.key});
@override
Widget build(BuildContext context) {
return Drawer(
child: Center(
child: TextButton(
onPressed: () async {
Screen3Route.instance.push(context);
await Future.delayed(Durations.extralong2);
if (!context.mounted) {
return;
}
Scaffold.of(context).closeEndDrawer();
Scaffold.of(context).closeDrawer();
},
child: const Text('Navigate to Screen 3')),
),
);
}
}
class Screen1AppBar extends StatelessWidget implements PreferredSizeWidget {
const Screen1AppBar({super.key});
@override
Widget build(BuildContext context) {
return AppBar(title: const Text('Screen 1 AppBar'));
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
///
/// Start Of The [Screen2]
class Screen2 extends StatelessWidget {
const Screen2({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('SubScreen of This:'),
TextButton(
onPressed: () {
SubScreen2Route.instance.go(context);
},
child: const Text('Go'),
),
const Text(' same '),
TextButton(
onPressed: () {
SubScreen2Route.instance.push(context);
},
child: const Text('Push'),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Screen 3 :'),
TextButton(
onPressed: () {
Screen3Route.instance.push(context);
},
child: const Text('Go'),
),
],
),
],
);
}
}
class Screen2Route extends NavigatorBarSubGoRoute {
factory Screen2Route() {
return instance;
}
Screen2Route._()
: super(
path: _path,
name: _name,
pageBuilder: (context, state) => const NoTransitionPage(
child: Screen2(),
),
appBar: const Screen2AppBar(),
);
static final Screen2Route instance = Screen2Route._();
static const String _path = '/screen2';
static const String _name = 'screen2';
@override
List<NavigatorBarSubGoRoute> get routes => [
SubScreen2Route.instance,
];
void go(BuildContext context) {
GoRouter.of(context).goNamed(_name);
}
void push(BuildContext context) {
GoRouter.of(context).pushNamed(_name);
}
}
class Screen2AppBar extends StatelessWidget implements PreferredSizeWidget {
const Screen2AppBar({super.key});
@override
Widget build(BuildContext context) {
return AppBar(
title: const Text('Screen 2 AppBar'),
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
///
/// Start Of The [SubScreen2]
class SubScreen2 extends StatelessWidget {
const SubScreen2({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: TextButton(
onPressed: () {
if (context.canPop()) {
context.pop();
}
},
child: const Text('Navigate Back'),
),
);
}
}
class SubScreen2Route extends NavigatorBarSubGoRoute {
factory SubScreen2Route() {
return instance;
}
SubScreen2Route._()
: super(
path: _path,
name: _name,
pageBuilder: (context, state) => const NoTransitionPage(
child: SubScreen2(),
),
appBar: const SubScreen2AppBar(),
);
static final SubScreen2Route instance = SubScreen2Route._();
static const String _path = 'sub_screen2';
static const String _name = 'sub_screen2';
void go(BuildContext context) {
GoRouter.of(context).goNamed(_name);
}
void push(BuildContext context) {
GoRouter.of(context).pushNamed(_name);
}
}
class SubScreen2AppBar extends StatelessWidget implements PreferredSizeWidget {
const SubScreen2AppBar({super.key});
@override
Widget build(BuildContext context) {
return AppBar(
leading: context.canPop()
? BackButton(
onPressed: () {
context.pop();
},
)
: null,
title: const Text('SubScreen 2 AppBar'),
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
///
/// Start Of The [Screen3]
class Screen3 extends StatelessWidget {
const Screen3({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const Screen3AppBar(),
body: Center(
child: TextButton(
onPressed: () {
if (!context.canPop()) {
return;
}
context.pop();
},
child: const Text('Navigate Back'),
),
),
);
}
}
class Screen3Route extends GoRoute {
factory Screen3Route() {
return instance;
}
Screen3Route._()
: super(
path: _path,
name: _name,
pageBuilder: (context, state) => const NoTransitionPage(
child: Screen3(),
),
);
static final Screen3Route instance = Screen3Route._();
static const String _path = '/screen3';
static const String _name = 'screen3';
void go(BuildContext context) {
GoRouter.of(context).goNamed(_name);
}
void push(BuildContext context) {
GoRouter.of(context).pushNamed(_name);
}
}
class Screen3AppBar extends StatelessWidget implements PreferredSizeWidget {
const Screen3AppBar({super.key});
@override
Widget build(BuildContext context) {
return AppBar(title: const Text('Screen 3 AppBar'));
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
The code also covers some of edge cases. If you have any question about part of the code we can discuss it on the comments. I hope this answer can help.
Upvotes: 0
Reputation: 1597
Seems like a common use case. But, There's no easy way to do it that I know of. The way the navigator in Flutter works is, when pages are pushed they're stacked on top of each other. So, If you want persistent Appbars and BottomNavs, It would make sense to have a parent scaffold and switch out the body. But then comes the issue of how to update the Appbar for each body?
The way I do it with go_router & flutter_bloc, I have a shell page with my top, bottoms and a bloc logic to handle adding and removing widgets.
Example:
I've put the full code in a single file so it's easy to copy and try. LMK if it's what you're looking for. So, I can explain further and beautify the answer.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
// goRouter start
final GlobalKey<NavigatorState> _rootNavKey =
GlobalKey<NavigatorState>(debugLabel: 'root');
final GlobalKey<NavigatorState> shellNavKey =
GlobalKey<NavigatorState>(debugLabel: 'home shell');
final GoRouter goRouter = GoRouter(
navigatorKey: _rootNavKey,
initialLocation: '/page1',
routes: [
ShellRoute(
navigatorKey: shellNavKey,
pageBuilder: (context, state, child) {
return MaterialPage(
key: state.pageKey,
child: BlocProvider(
create: (context) => ScaffCubit(),
child: HomeShellPage(
body: child,
),
),
);
},
routes: [
GoRoute(
name: 'page1',
path: '/page1',
parentNavigatorKey: shellNavKey,
pageBuilder: (context, state) {
return const NoTransitionPage(
child: Page1(),
);
},
),
GoRoute(
name: 'page2',
path: '/page2',
parentNavigatorKey: shellNavKey,
pageBuilder: (context, state) {
return const NoTransitionPage(
child: Page2(),
);
},
),
GoRoute(
name: 'page3',
path: '/page3',
parentNavigatorKey: shellNavKey,
pageBuilder: (context, state) {
return const NoTransitionPage(
child: Page3(),
);
},
),
],
),
],
);
/// goRouter end
// bloc start
@immutable
sealed class ScaffState {}
final class ScaffLoaded extends ScaffState {
final List<PreferredSizeWidget> appBar;
final List<Widget> drawer;
final List<Widget> bottomNavBar;
ScaffLoaded({
required this.appBar,
required this.drawer,
required this.bottomNavBar,
});
}
class ScaffCubit extends Cubit<ScaffState> {
ScaffCubit()
: super(ScaffLoaded(
// Initiate the lists as empty avoiding null checks later
appBar: List.empty(growable: true),
drawer: List.empty(growable: true),
bottomNavBar: List.empty(growable: true),
));
add({
PreferredSizeWidget? appBar,
Widget? drawer,
Widget? bottomNavBar,
}) {
ScaffLoaded s = state as ScaffLoaded;
if (appBar != null) s.appBar.add(appBar);
if (drawer != null) s.drawer.add(drawer);
if (bottomNavBar != null) s.bottomNavBar.add(bottomNavBar);
emit(ScaffLoaded(
appBar: s.appBar,
drawer: s.drawer,
bottomNavBar: s.bottomNavBar,
));
}
removeLast({
bool appBar = false,
bool drawer = false,
bool bottomNavBar = false,
}) {
ScaffLoaded s = state as ScaffLoaded;
if (appBar && s.appBar.isNotEmpty) s.appBar.removeLast();
if (drawer && s.drawer.isNotEmpty) s.drawer.removeLast();
if (bottomNavBar && s.bottomNavBar.isNotEmpty) s.bottomNavBar.removeLast();
emit(ScaffLoaded(
appBar: s.appBar,
drawer: s.drawer,
bottomNavBar: s.bottomNavBar,
));
}
clear() {
ScaffLoaded s = state as ScaffLoaded;
emit(ScaffLoaded(
appBar: s.appBar,
drawer: s.drawer,
bottomNavBar: s.bottomNavBar,
));
}
}
/// bloc end
// main start
void main() {
runApp(const MyApp());
}
/// main end
// app start
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: goRouter,
);
}
}
// app end
// pages start
class HomeShellPage extends StatelessWidget {
final Widget body;
const HomeShellPage({super.key, required this.body});
@override
Widget build(BuildContext context) {
return BlocBuilder<ScaffCubit, ScaffState>(
builder: (context, state) {
state as ScaffLoaded;
return Scaffold(
appBar: state.appBar.isEmpty
? AppBar(
title: const Text('Home Shell Page'),
)
: state.appBar.last,
body: body,
drawer: state.drawer.isEmpty
? Drawer(
child: ListView(
children: List.generate(4, (index) {
return ListTile(
title: Text('Button $index'),
subtitle: const Text('Home shell drawer'),
);
}),
),
)
: null,
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.square_outlined),
label: 'Page 1',
),
BottomNavigationBarItem(
icon: Icon(Icons.circle_outlined),
label: 'Page 2',
),
],
onTap: (value) {
value == 0
? context.goNamed(
'page1',
)
: context.goNamed('page2');
},
),
);
},
);
}
}
class Page1 extends StatefulWidget {
const Page1({super.key});
@override
State<Page1> createState() => _Page1State();
}
class _Page1State extends State<Page1> {
@override
Widget build(BuildContext context) {
return const Center(
child: Text('Page 1'),
);
}
}
class Page2 extends StatefulWidget {
const Page2({super.key});
@override
State<Page2> createState() => _Page2State();
}
class _Page2State extends State<Page2> {
late AppBar appBar;
@override
void initState() {
appBar = AppBar(title: const Text('Page 2'));
context.read<ScaffCubit>().add(appBar: appBar, drawer: null);
super.initState();
}
@override
void dispose() {
shellNavKey.currentContext!.read<ScaffCubit>().removeLast(appBar: true);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Page 2'),
ElevatedButton(
onPressed: () => context.pushNamed('page3'),
child: const Text('Go to Page3'),
)
],
);
}
}
class Page3 extends StatefulWidget {
const Page3({super.key});
@override
State<Page3> createState() => _Page3State();
}
class _Page3State extends State<Page3> {
late AppBar appBar;
@override
void initState() {
appBar = AppBar(
leading: BackButton(onPressed: () => context.pop()),
title: const Text('Page 3'),
);
context.read<ScaffCubit>().add(appBar: appBar);
super.initState();
}
@override
void dispose() {
shellNavKey.currentContext!.read<ScaffCubit>().removeLast(appBar: true);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Page 3'),
ElevatedButton(
onPressed: () => shellNavKey.currentContext!.pushNamed('page4'),
child: const Text('Go to page 4'),
)
],
);
}
}
/// pages end
Upvotes: 1