Как работать с NavigationBar в go_router? | Флаттер
В настоящее время я изо всех сил пытаюсь реорганизовать свой код маршрутизации с помощью go_router.
У меня уже есть несколько простых маршрутов, таких как
/signin
&
/signup
, но проблема возникает, когда я пытаюсь настроить маршрутизацию с помощью BottomNavigationBar с несколькими экранами. Я хотел бы иметь отдельный маршрут для каждого из них, например
/home
,
/events
&
/profile
.
Я понял, что мне каким-то образом нужно вернуть тот же виджет с другим параметром, чтобы предотвратить изменение всего экрана при каждом нажатии BottomNavigationBarItem, а вместо этого обновить только часть над BottomNavigationBar, которая будет самим экраном.
Я придумал довольно хитрое решение:
GoRoute(
path: '/:path',
builder: (BuildContext context, GoRouterState state) {
final String path = state.params['path']!;
if (path == 'signin') {
return const SignInScreen();
}
if (path == 'signup') {
return const SignUpScreen();
}
if (path == 'forgot-password') {
return const ForgotPasswordScreen();
}
// Otherwise it has to be the ScreenWithBottomBar
final int index = getIndexFromPath(path);
if (index != -1) {
return MainScreen(selectedIndex: index);
}
return const ErrorScreen();
}
)
Это выглядит не очень хорошо и делает невозможным добавление подмаршрутов типа
/profile/settings/appearance
или
/events/:id
.
Я хотел бы иметь что-то легкое для понимания, например:
GoRoute(
path: '/signin',
builder: (BuildContext context, GoRouterState state) {
return const SignInScreen();
}
),
GoRoute(
path: '/signup',
builder: (BuildContext context, GoRouterState state) {
return const SignUpScreen();
}
),
GoRoute(
path: '/home',
builder: (BuildContext context, GoRouterState state) {
return const ScreenWithNavBar(selectedScreen: 1);
}
),
GoRoute(
path: '/events',
builder: (BuildContext context, GoRouterState state) {
return const ScreenWithNavBar(selectedScreen: 2);
},
routes: <GoRoute>[
GoRoute(
path: ':id',
builder: (BuildContext context, GoRouterState state) {
return const EventScreen();
}
)
]
)
Есть ли способ добиться поведения?
4 ответа
Теперь вы можете использовать
ShellRouter
с
GoRouter
создавать
Navigation Bar
Объяснение:
О чем следует помнить при использовании отShellRoute
к
- Указать
parentNavigatorKey
опора в каждомGoRoute
- Использовать
context.go()
заменить страницу ,context.push()
поместить страницу в стек
Структура кода:
final _parentKey = GlobalKey<NavigatorState>();
final _shellKey = GlobalKey<NavigatorState>();
|_ GoRoute
|_ parentNavigatorKey = _parentKey Specify key here
|_ ShellRoute
|_ GoRoute // Needs Bottom Navigation
|_ parentNavigatorKey = _shellKey
|_ GoRoute // Needs Bottom Navigation
|_ parentNavigatorKey = _shellKey
|_ GoRoute // Full Screen which doesn't need Bottom Navigation
|_parentNavigatorKey = _parentKey
Код:
Маршрутизатор
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();
final router = GoRouter(
initialLocation: '/',
navigatorKey: _rootNavigatorKey,
routes: [
ShellRoute(
navigatorKey: _shellNavigatorKey,
pageBuilder: (context, state, child) {
print(state.location);
return NoTransitionPage(
child: ScaffoldWithNavBar(
location: state.location,
child: child,
));
},
routes: [
GoRoute(
path: '/',
parentNavigatorKey: _shellNavigatorKey,
pageBuilder: (context, state) {
return const NoTransitionPage(
child: Scaffold(
body: Center(child: Text("Home")),
),
);
},
),
GoRoute(
path: '/discover',
parentNavigatorKey: _shellNavigatorKey,
pageBuilder: (context, state) {
return const NoTransitionPage(
child: Scaffold(
body: Center(child: Text("Discover")),
),
);
},
),
GoRoute(
parentNavigatorKey: _shellNavigatorKey,
path: '/shop',
pageBuilder: (context, state) {
return const NoTransitionPage(
child: Scaffold(
body: Center(child: Text("Shop")),
),
);
}),
],
),
GoRoute(
parentNavigatorKey: _rootNavigatorKey,
path: '/login',
pageBuilder: (context, state) {
return NoTransitionPage(
key: UniqueKey(),
child: Scaffold(
appBar: AppBar(),
body: const Center(
child: Text("Login"),
),
),
);
},
),
],
);
НижняяНавигацияБар
class ScaffoldWithNavBar extends StatefulWidget {
String location;
ScaffoldWithNavBar({super.key, required this.child, required this.location});
final Widget child;
@override
State<ScaffoldWithNavBar> createState() => _ScaffoldWithNavBarState();
}
class _ScaffoldWithNavBarState extends State<ScaffoldWithNavBar> {
int _currentIndex = 0;
static const List<MyCustomBottomNavBarItem> tabs = [
MyCustomBottomNavBarItem(
icon: Icon(Icons.home),
activeIcon: Icon(Icons.home),
label: 'HOME',
initialLocation: '/',
),
MyCustomBottomNavBarItem(
icon: Icon(Icons.explore_outlined),
activeIcon: Icon(Icons.explore),
label: 'DISCOVER',
initialLocation: '/discover',
),
MyCustomBottomNavBarItem(
icon: Icon(Icons.storefront_outlined),
activeIcon: Icon(Icons.storefront),
label: 'SHOP',
initialLocation: '/shop',
),
MyCustomBottomNavBarItem(
icon: Icon(Icons.account_circle_outlined),
activeIcon: Icon(Icons.account_circle),
label: 'MY',
initialLocation: '/login',
),
];
@override
Widget build(BuildContext context) {
const labelStyle = TextStyle(fontFamily: 'Roboto');
return Scaffold(
body: SafeArea(child: widget.child),
bottomNavigationBar: BottomNavigationBar(
selectedLabelStyle: labelStyle,
unselectedLabelStyle: labelStyle,
selectedItemColor: const Color(0xFF434343),
selectedFontSize: 12,
unselectedItemColor: const Color(0xFF838383),
showUnselectedLabels: true,
type: BottomNavigationBarType.fixed,
onTap: (int index) {
_goOtherTab(context, index);
},
currentIndex: widget.location == '/'
? 0
: widget.location == '/discover'
? 1
: widget.location == '/shop'
? 2
: 3,
items: tabs,
),
);
}
void _goOtherTab(BuildContext context, int index) {
if (index == _currentIndex) return;
GoRouter router = GoRouter.of(context);
String location = tabs[index].initialLocation;
setState(() {
_currentIndex = index;
});
if (index == 3) {
context.push('/login');
} else {
router.go(location);
}
}
}
class MyCustomBottomNavBarItem extends BottomNavigationBarItem {
final String initialLocation;
const MyCustomBottomNavBarItem(
{required this.initialLocation,
required Widget icon,
String? label,
Widget? activeIcon})
: super(icon: icon, label: label, activeIcon: activeIcon ?? icon);
}
Выход:
Это выдающийся запрос функции для go_router, который я надеюсь решить в ближайшие недели. Следите за обновлениями.
Вот полный рабочий пример из последних изменений go_router с ShellRoute:
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
Future<void> main() async {
runApp(
StatefulShellRouteExampleApp(),
);
}
class ScaffoldBottomNavigationBar extends StatelessWidget {
const ScaffoldBottomNavigationBar({
required this.navigationShell,
Key? key,
}) : super(key: key ?? const ValueKey<String>('ScaffoldBottomNavigationBar'));
final StatefulNavigationShell navigationShell;
@override
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell,
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section_A'),
BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section_B'),
],
currentIndex: navigationShell.currentIndex,
onTap: (int tappedIndex) {
navigationShell.goBranch(tappedIndex);
},
),
);
}
}
class StatefulShellRouteExampleApp extends StatelessWidget {
StatefulShellRouteExampleApp({super.key});
final GoRouter _router = GoRouter(
initialLocation: '/login',
routes: <RouteBase>[
GoRoute(
path: '/login',
builder: (BuildContext context, GoRouterState state) {
return const LoginScreen();
},
routes: <RouteBase>[
GoRoute(
path: 'detailLogin',
builder: (BuildContext context, GoRouterState state) {
return const DetailLoginScreen();
},
),
],
),
StatefulShellRoute.indexedStack(
builder: (BuildContext context, GoRouterState state,
StatefulNavigationShell navigationShell) {
return ScaffoldBottomNavigationBar(
navigationShell: navigationShell,
);
},
branches: <StatefulShellBranch>[
StatefulShellBranch(
routes: <RouteBase>[
GoRoute(
path: '/sectionA',
builder: (BuildContext context, GoRouterState state) {
return const RootScreen(
label: 'Section A',
detailsPath: '/sectionA/details',
);
},
routes: <RouteBase>[
GoRoute(
path: 'details',
builder: (BuildContext context, GoRouterState state) {
return const DetailsScreen(label: 'A');
},
),
],
),
],
),
StatefulShellBranch(
routes: <RouteBase>[
GoRoute(
path: '/sectionB',
builder: (BuildContext context, GoRouterState state) {
return const RootScreen(
label: 'Section B',
detailsPath: '/sectionB/details',
);
},
routes: <RouteBase>[
GoRoute(
path: 'details',
builder: (BuildContext context, GoRouterState state) {
return const DetailsScreen(label: 'B');
},
),
],
),
],
),
],
),
],
);
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Go_router Complex Demo',
theme: ThemeData(
primarySwatch: Colors.orange,
),
routerConfig: _router,
);
}
}
class RootScreen extends StatelessWidget {
const RootScreen({
required this.label,
required this.detailsPath,
super.key,
});
final String label;
final String detailsPath;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Root of section $label'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'Screen $label',
style: Theme.of(context).textTheme.titleLarge,
),
const Padding(padding: EdgeInsets.all(4)),
TextButton(
onPressed: () {
GoRouter.of(context).go(detailsPath);
},
child: const Text('View details'),
),
const Padding(padding: EdgeInsets.all(4)),
TextButton(
onPressed: () {
GoRouter.of(context).go('/login');
},
child: const Text('Logout'),
),
],
),
),
);
}
}
class DetailsScreen extends StatefulWidget {
const DetailsScreen({
required this.label,
super.key,
});
final String label;
@override
State<StatefulWidget> createState() => DetailsScreenState();
}
class DetailsScreenState extends State<DetailsScreen> {
int _counter = 0;
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Details Screen - ${widget.label}'),
),
body: _build(context),
);
}
Widget _build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'Details for ${widget.label} - Counter: $_counter',
style: Theme.of(context).textTheme.titleLarge,
),
const Padding(padding: EdgeInsets.all(4)),
TextButton(
onPressed: () {
setState(() {
_counter++;
});
},
child: const Text('Increment counter'),
),
const Padding(padding: EdgeInsets.all(8)),
const Padding(padding: EdgeInsets.all(4)),
TextButton(
onPressed: () {
GoRouter.of(context).go('/login');
},
child: const Text('Logout'),
),
],
),
);
}
}
class LoginScreen extends StatelessWidget {
const LoginScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login Screen')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {
context.go('/login/detailLogin');
},
child: const Text('Go to the Details Login screen'),
),
],
),
),
);
}
}
class DetailLoginScreen extends StatelessWidget {
const DetailLoginScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Details Login Screen')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <ElevatedButton>[
ElevatedButton(
onPressed: () {
// context.go('/sectionA');
context.go('/sectionB');
},
child: const Text('Go to BottomNavBar'),
),
],
),
),
);
}
}
Решение, которое сработало для меня в конце, было следующим:
Мне удалось создать маршрут, указывающий, какие значения разрешены:
GoRoute(
path: '/:screen(home|discover|notifications|profile)',
builder: (BuildContext context, GoRouterState state) {
final String screen = state.params['screen']!;
return TabScreen(screen: screen);
}
)
После этого я передаю любое значение, которое содержит маршрут (например, «/home» или «/discover»), на TabScreen, который затем отображает тот же виджет, но без перезагрузки TabBar снова и снова:
class TabScreen extends StatelessWidget {
const TabScreen({
super.key,
required this.screen
});
final String screen;
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(child: screen == 'home' ? const HomeScreen() : screen == 'discover' ? const DiscoverScreen() : screen == 'notifications' ? const NotificationsScreen() : ProfileScreen(),
CustomBottomNavigationBar()
]
);
}
}