I am trying to implement a NavigationBar using the new Material You API.
https://api.flutter.dev/flutter/material/NavigationBar-class.html
I was just curious to know if we can implement the same using the Go_Router package .
Updated Answer (v6.0.0)
My original answer was created using GoRouter v3 and it was not possible at the time to keep the NavigationBar in sub screens.
Currently, in version 6, GoRouter allows that using the ShellRoute, where you can use the builder attribute to build a Scaffold with the navigation bar.
You can see the oficial live example here.
I am rewriting the outdated answer below using the GoRouter v6.0.0, and I am leaving the original answer in case someone finds it useful.
Updated Code
We need to create some basic models to store data:
/// Just a generic model that will be used to present some data on the screen.
class Person {
final String id;
final String name;
Person({required this.id, required this.name});
}
/// Family will be the model that represents our tabs. We use the properties `icon` and `name` in the `NavigationBar`.
class Family {
final String id;
final String name;
final List<Person> people;
final Icon icon;
Family({
required this.id,
required this.name,
required this.people,
required this.icon,
});
}
/// Families will be used to store the tabs to be easily accessed anywhere. In a real application you would use something fancier.
class Families {
static const List<Icon> icons = [
Icon(Icons.looks_one),
Icon(Icons.looks_two),
Icon(Icons.looks_3)
];
static final List<Family> data = List.generate(
3,
(fid) => Family(
id: '$fid',
name: 'Family $fid',
people: List.generate(
10,
(id) => Person(id: '$id', name: 'Family $fid Person $id'),
),
icon: icons[fid],
),
);
}
Now we'll create the basic views that will render the model's data:
/// Used to present Person's data.
class PersonView extends StatelessWidget {
const PersonView({required this.person, Key? key}) : super(key: key);
final Person person;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Text(person.name),
),
);
}
}
/// This is the view that will be used by each application's tab.
class FamilyView extends StatelessWidget {
const FamilyView({super.key, required this.family});
final Family family;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(family.name),
),
body: ListView(
children: [
for (final p in family.people)
ListTile(
title: Text(p.name),
onTap: () => context.go('/family/${family.id}/person/${p.id}'),
),
],
),
);
}
}
Now, let's finally create the widget that will show the NavigationBar:
/// Widget responsible to render the actual page and the navigation bar.
class ShellScreen extends StatelessWidget {
final Widget child;
final int index;
const ShellScreen({super.key, required this.child, required this.index});
#override
Widget build(BuildContext context) {
if (index < 0 || index >= Families.data.length) {
// Just in case someone tries to pass an invalid index in the url.
GoRouter.of(context).go('/');
return const SizedBox.shrink();
}
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
destinations: [
for (final f in Families.data)
NavigationDestination(
icon: f.icon,
label: f.name,
)
],
onDestinationSelected: (index) => context.go(
'/family/${Families.data[index].id}',
),
selectedIndex: index,
),
);
}
}
Finally, this will only work if we define the app's routes using the StackRouter and set the GoRouter as the app's navigator:
void main() {
usePathUrlStrategy();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Flutter Demo',
routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
theme: ThemeData(
primarySwatch: Colors.blue,
),
);
}
}
final router = GoRouter(
routes: [
GoRoute(
path: '/',
redirect: (_, __) => '/family/${Families.data[0].id}',
),
ShellRoute(
// The ShellRoute is what make it possible to wrap the subroutes in a common widget using the `builder`
builder: (BuildContext context, GoRouterState state, Widget child) {
int index = int.tryParse(state.params['fid'] ?? '0') ?? 0;
return ShellScreen(index: index, child: child);
},
routes: [
GoRoute(
path: '/family/:fid',
builder: (context, state) {
final fid = state.params['fid']!;
final family = Families.data.firstWhere((f) => f.id == fid,
orElse: () => throw Exception('family not found: $fid'));
return FamilyView(
key: state.pageKey,
family: family,
);
},
routes: [
GoRoute(
path: 'person/:id',
builder: (context, state) {
final fid = state.params['fid']!;
final id = state.params['id'];
final person = Families.data
.firstWhere((f) => f.id == fid,
orElse: () => throw Exception('family not found: $fid'))
.people
.firstWhere(
((p) => p.id == id),
orElse: () => throw Exception('person not found: $id'),
);
return PersonView(key: state.pageKey, person: person);
},
),
],
),
],
),
],
);
The important part that solves our need is the ShellRouter. It is a route used to display any matching sub-routes, instead of placing them on the root Navigator.
The widget built by the matching sub-route becomes the child parameter of the builder. So, the ShellScreen can render the sub-route widget presenting the navigation bar.
With all these steps you will have this:
Outdated Answer (v3.0.0)
Yes, it is possible [as a matter of fact, it was not possible, but I didn't understand the question at the time].
Let's use the example in GoRouter documentation as a starting point.
We need to create some basic models to store data:
/// Just a generic model that will be used to present some data on the screen.
class Person {
final String id;
final String name;
Person({required this.id, required this.name});
}
/// Family will be the model that represents our tabs. We use the properties `icon` and `name` in the `NavigationBar`.
class Family {
final String id;
final String name;
final List<Person> people;
final Icon icon;
Family({
required this.id,
required this.name,
required this.people,
required this.icon,
});
}
/// Families will be used to store the tabs to be easily accessed anywhere. In a real application you would use something fancier.
class Families {
static const List<Icon> icons = [
Icon(Icons.looks_one),
Icon(Icons.looks_two),
Icon(Icons.looks_3)
];
static final List<Family> data = List.generate(
3,
(fid) => Family(
id: '$fid',
name: 'Family $fid',
people: List.generate(
10,
(id) => Person(id: '$id', name: 'Family $fid Person $id'),
),
icon: icons[fid],
),
);
}
Now we'll create the basic views that will render the model's data:
/// Used to present Person's data.
class PersonView extends StatelessWidget {
const PersonView({required this.person, Key? key}) : super(key: key);
final Person person;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Text(person.name),
),
);
}
}
/// This is the view that will be used by each application's tab.
class FamilyView extends StatefulWidget {
const FamilyView({required this.family, Key? key}) : super(key: key);
final Family family;
#override
State<FamilyView> createState() => _FamilyViewState();
}
class _FamilyViewState extends State<FamilyView>
with AutomaticKeepAliveClientMixin {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return ListView(
children: [
for (final p in widget.family.people)
ListTile(
title: Text(p.name),
onTap: () =>
context.go('/family/${widget.family.id}/person/${p.id}'),
),
],
);
}
}
Until now we did nothing different compared to the GoRouter documentation, so let's finally create the widget that will show the NavigationBar:
class FamilyTabsScreen extends StatefulWidget {
final int index;
FamilyTabsScreen({required Family currentFamily, Key? key})
: index = Families.data.indexWhere((f) => f.id == currentFamily.id),
super(key: key) {
assert(index != -1);
}
#override
_FamilyTabsScreenState createState() => _FamilyTabsScreenState();
}
class _FamilyTabsScreenState extends State<FamilyTabsScreen>
with TickerProviderStateMixin {
#override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text(_title(context)),
),
body: FamilyView(family: Families.data[widget.index]),
bottomNavigationBar: NavigationBar(
destinations: [
for (final f in Families.data)
NavigationDestination(
icon: f.icon,
label: f.name,
)
],
onDestinationSelected: (index) => _tap(context, index),
selectedIndex: widget.index,
),
);
void _tap(BuildContext context, int index) =>
context.go('/family/${Families.data[index].id}');
String _title(BuildContext context) =>
(context as Element).findAncestorWidgetOfExactType<MaterialApp>()!.title;
}
This is the important part of the code above:
/// [...] suppressed code
bottomNavigationBar: NavigationBar(
destinations: [
for (final f in Families.data)
NavigationDestination(
icon: f.icon,
label: f.name,
)
],
onDestinationSelected: (index) => _tap(context, index),
selectedIndex: widget.index,
),
/// [...] suppressed code
So, basically we are using the NavigationBar almost exactly as we would use the TabBarView.
Finally, this will only work if we define the app's routes and set the GoRouter as the app's navigator:
void main() {
GoRouter.setUrlPathStrategy(UrlPathStrategy.path);
runApp(const MyApp());
}
final _router = GoRouter(
routes: [
GoRoute(
path: '/',
redirect: (_) => '/family/${Families.data[0].id}',
),
GoRoute(
path: '/family/:fid',
builder: (context, state) {
final fid = state.params['fid']!;
final family = Families.data.firstWhere((f) => f.id == fid,
orElse: () => throw Exception('family not found: $fid'));
return FamilyTabsScreen(key: state.pageKey, currentFamily: family);
},
routes: [
GoRoute(
path: 'person/:id',
builder: (context, state) {
final fid = state.params['fid']!;
final id = state.params['id'];
final person = Families.data
.firstWhere((f) => f.id == fid,
orElse: () => throw Exception('family not found: $fid'))
.people
.firstWhere(
((p) => p.id == id),
orElse: () => throw Exception('person not found: $id'),
);
return PersonView(key: state.pageKey, person: person);
},
),
]),
],
);
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Flutter Demo',
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
theme: ThemeData(
primarySwatch: Colors.blue,
),
);
}
}
With all these steps you will have this:
For anyone searching on a persistent BottomNavBar across all pages, this is actively being discussed on Github,
https://github.com/flutter/packages/pull/2453
Now you can use ShellRouter with GoRouter to create Navigation Bar
Explaination:
Things to keep in mind while using context.go() from ShellRoute to GoRoute
Specify parentNavigatorKey prop in each GoRoute
Use context.go() to replace page , context.push() to push page to stack
Code Structure to follow:
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
Code has following features:
Active icon navbar
Persists navBar item's focus when transisted to new page
back button in the transisted page
Code:
Router
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"),
),
),
);
},
),
],
);
BottomNavigationBar
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);
}
Output:
Related
I use GetX package since a while, but sometimes I suffer from some mistakes..
Now I have a bottomNavigationBar which has 5 pages to navigate between (Offers - Categories - Cart - Favorite - Account).
My problem is:
When I go from index 0 to index 2 (for an example), it goes normally, but when I want to get back to index 0, here the app crashes and give me this error:
Null check operator used on a null value
The same way I was using with another project, but there I was using TabBar, I used it normally without this mistake, but here in the bottom navigation bar it happens.
Actually I don't believe that the error because of the widget kind, but really want to solve it.
Note :
I created a HomePageController which I defined all of the bottomNavigationBar operations, like changing the index, and the list of pages, ..etc
And for each page it has its controller, even when I get back to the page which uses HomePageController it crashes!!!
This is a simple of my code:
class HomePageController extends GetxController {
static HomePageController instance = HomePageController();
late TextEditingController categoriesSearchController;
#override
void onInit() {
super.onInit();
categoriesSearchController = TextEditingController();
}
int bottomNavIndex = 0;
changeBottomIndex(int index) {
bottomNavIndex = index;
update();
}
List<Widget> bottomScreens = const [
Offers(),
Categories(),
Cart(),
Favorite(),
Account(),
];
List<ItemModel> meatsList = [
ItemModel(
title: 'Thigh',
image: 'assets/images/home_page/pin_thigh.png',
description: '1 Kg',
price: 1.72,
),
ItemModel(
title: 'Breast',
image: 'assets/images/home_page/breasts2.jpg',
description: '1 Kg',
price: 1.65,
),
ItemModel(
title: 'lamb',
image: 'assets/images/home_page/lamb.jpeg',
description: '1 Kg',
price: 6.55,
),
];
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return GetBuilder<HomePageController>(
builder: (controller) => controller != null
? SafeArea(
child: Scaffold(
backgroundColor: AppColors.whiteColor,
bottomNavigationBar: BottomNavigationBar(
items: controller.changingBottom(),
currentIndex: controller.bottomNavIndex,
type: BottomNavigationBarType.fixed,
selectedItemColor: AppColors.onBoardingButton,
onTap: (index) {
controller.changeBottomIndex(index);
},
),
body: controller.bottomScreens[controller.bottomNavIndex],
),
)
: const Center(
child: CircularProgressIndicator(),
),
);
}
}
Update:
I forgot to mention that I use GetX Binding class to initialize all controllers when it's needed, like this way:
class Binding extends Bindings {
#override
void dependencies() {
Get.put(() => DatabaseController());
Get.lazyPut(() => AuthController());
Get.lazyPut(() => HomePageController());
Get.lazyPut(() => ProductsController());
Get.lazyPut(() => CartController());
}
}
So there is no need to initialize each controller in each page.
In your Homepage, add init method and autoRemove in GetBuilder like this:
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return GetBuilder<HomePageController>(
init:HomePageController(),
autoRemove:false,
builder: (controller) => controller != null
? SafeArea(
child: Scaffold(
backgroundColor: AppColors.whiteColor,
bottomNavigationBar: BottomNavigationBar(
items: controller.changingBottom(),
currentIndex: controller.bottomNavIndex,
type: BottomNavigationBarType.fixed,
selectedItemColor: AppColors.onBoardingButton,
onTap: (index) {
controller.changeBottomIndex(index);
},
),
body: controller.bottomScreens[controller.bottomNavIndex],
),
)
: const Center(
child: CircularProgressIndicator(),
),
);
}
}
By using init, it will rebuild your controller if it is disposed. And, by using autoRemove to false, it won't dispose the controller every single time.
Finally I got solution..
I had to use Get.find<xController>() in each UI page I use any controller class in it.
Widget build(BuildContext context) {
return GetBuilder<CartController>(
init: Get.find<CartController>(),
builder: (controller) => Scaffold(),
And after testing, it works good.
I tried to make a social media app and when setup my layout contains bottom Nav bar and screens also my cubit is ok and the code in the main is ok I tried to change bottom Nav to Text and the problem solved I had a problem with getting Bloc from child widget and I have no idea what I'm doing wrong.
how can I got this error any one can help ?
class SocialLayout extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocConsumer<SocialCubit , SocialStates>
(listener: (context,state){},
builder: (context,state)
{
var cubit= SocialCubit.get(context);
return Scaffold(
appBar: AppBar(
title: Text('News Feed',),
),
body: cubit.screens[cubit.currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: cubit.currentIndex,
onTap: (index){
cubit.changeBottomNav(index);
},
items: [
BottomNavigationBarItem(icon: Icon(IconBroken.Home,),label: "Home"),
BottomNavigationBarItem(icon: Icon(IconBroken.Chat,),label: "Chats"),
BottomNavigationBarItem(icon: Icon(IconBroken.Setting,),label: "Settings"),
BottomNavigationBarItem(icon: Icon(IconBroken.User,),label: "Users"),
],
),
);
}
);
}
}
//cubit
class SocialCubit extends Cubit<SocialStates> {
SocialCubit() : super(SocialInitialState());
static SocialCubit get(context) => BlocProvider.of(context);
SocialUserModel ? socialUserModel;
void getUserData() {
uId = CacheHelper.getData(key: 'uId');
emit(SocialGetUserLoadingState());
FirebaseFirestore.instance.collection('users').doc(uId).get().then((value) {
socialUserModel = SocialUserModel.fromJson(value.data());
print(socialUserModel.toString());
emit(SocialGetUserSuccessState());
})
.catchError((error) {
print(error.toString());
emit(SocialGetUserErrorState(error.toString()));
});
}
int currentIndex =0;
List<Widget>screens =[
Feeds(),
Chats(),
Users(),
Setting(),
];
void changeBottomNav(int index)
{
currentIndex = index;
emit(SocialChangeBottomNav());
}
}
//main
void main() async
SimpleBlocObserver();
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
DioHelper.init();
await CacheHelper.init();
bool ? isDarkMode = CacheHelper.getData(key: 'isDarkMode');
Widget widget;
uId = CacheHelper.getData(key: 'uId');
if(uId != null)
{
widget = SocialLayout();
}
else{
widget=SocialLoginScreen();
}
runApp(MyApp(isDarkMode, widget ,token ));
}
// ignore: must_be_immutable
class MyApp extends StatelessWidget {
bool ? isDarkMode;
late final Widget startWidget;
String? token;
MyApp(this.isDarkMode,this.startWidget, String? token, {Key? key}) : super(key: key) ;
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider( create: (BuildContext context) => AppCubit()..changeAppMode(fromShared: isDarkMode,),
),
BlocProvider( create: (BuildContext context) => SocialCubit()..getUserData(),
),
],
child: BlocConsumer<AppCubit, AppStates>(
listener: (context, state){},
builder: (context ,state){
return MaterialApp(
debugShowCheckedModeBanner: false,
themeMode: AppCubit.get(context).isDarkMode ? ThemeMode.light: ThemeMode.dark,
home: AnimatedSplashScreen(
splash: SplashScreen(),
nextScreen: startWidget,
splashIconSize: 700,
animationDuration: const Duration(milliseconds: 2000),
splashTransition: SplashTransition.fadeTransition,
),
theme: MyTheme.lightTheme,
darkTheme: MyTheme.darkTheme,
builder: BotToastInit(),
navigatorObservers: [BotToastNavigatorObserver()],
);
},
),
);
}
}
*Invalid argument(s): Never is not provided to BlocBuilder<SocialCubit, SocialStates>. Context used for Bloc retrieval must be a descendant of BlocProvider.
Solved by remove unused bloc packages from pubsbec.yaml file
I have a very simply scenario. A button where when pushed would transition to the next screen. In Navigator 1.0, it is very simple to do by:
ElevatedButton(
onPressed: () {
// Navigate to next screen.
Navigator.push(
context,
MaterialPageRoute(builder: (context) => NextScreen()),
);
},
child: Text('Next Screen!'),
)
It seems that with Navigator 2.0 I would need to have a state to keep track of the current screen.
...
Navigator(
pages: [
MainScreenPageRoute()
if (state.isNextScreen) {
NextScreenPageRoute()
}
],
onPopPage: (route, result) {
// would have to keep track of this value
state.isNextScreen = false;
return route.didPop(result);
},
)
...
As for before I don't have to keep track of a state just to navigate, In Navigator 2.0 it seems that it is required. Is it really the case? If so do you guys have any suggestion of how to handle this properly?
P.S.
It also feels like now I have to keep track of the state which adds to more work compared to before.
yes in navigator 2 you change the page when you change the state.
There are some packages that help you to avoid this like qlevar_router
see this example
import 'package:flutter/material.dart';
import 'package:qlevar_router/qlevar_router.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final books = [
Book('Stranger in a Strange Land', 'Robert A. Heinlein'),
Book('Foundation', 'Isaac Asimov'),
Book('Fahrenheit 451', 'Ray Bradbury'),
];
#override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationParser: QRouteInformationParser(),
routerDelegate: QRouterDelegate([
QRoute(path: '/', builder: () => BooksListScreen(books)),
QRoute(
path:
'/books/:id([0-${books.length - 1}])', // The only available pages are the pages in the list
builder: () => BookDetailsScreen(books[QR.params['id']!.asInt!])),
]));
}
class Book {
final String title;
final String author;
Book(this.title, this.author);
}
class BooksListScreen extends StatelessWidget {
final List<Book> books;
BooksListScreen(this.books);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView(
children: [
for (var book in books)
ListTile(
title: Text(book.title),
subtitle: Text(book.author),
onTap: () => QR.to('/books/${books.indexOf(book)}'))
],
),
);
}
}
class BookDetailsScreen extends StatelessWidget {
final Book book;
BookDetailsScreen(this.book);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(book.title, style: Theme.of(context).textTheme.headline6),
Text(book.author, style: Theme.of(context).textTheme.subtitle1),
],
),
),
);
}
}
don't forget to add qlevar_router: 1.4.0 in pubspec.yaml
What's the correct way of setting up navigation architecture named routes while using a bottomNavigationBar?
Here's my current setup but I feel there's a better way of doing it:
main.dart:
onGenerateRoute: (settings) {
return MaterialPageRoute(
settings: settings,
builder: (context) {
switch (settings.name) {
case NamedRoutes.splashScreen:
return SplashScreen();
case NamedRoutes.login:
return LoginPage();
case NamedRoutes.mainApp:
return NavigatorSetup();
default:
throw Exception('Invalid route: ${settings.name}');
}
});
navigatorSetup.dart:
IndexedStack(
index: Provider.of<RoutesProvider>(context).selectedViewIndex,
children: [FirstMain(), SecondMain(), ThirdMain(), FourthMain()],
), bottomNavigationBar...
in each main files there is the following setup
class FirstMain extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Navigator(
key: Provider.of<RoutesProvider>(context).homeKey,
onGenerateRoute: (settings) {
return MaterialPageRoute(
settings: settings,
builder: (context) {
switch (settings.name) {
case '/':
case NamedRoutes.mainPage:
return MainPage();
case NamedRoutes.singleMainPage:
return SingleMainPage();
default:
throw Exception('Invalid route: ${settings.name}');
}
},
);
},
);
}
}
Then my routes provider looks like this:
class RoutesProvider extends ChangeNotifier {
int _selectedViewIndex = 0;
get selectedViewIndex => _selectedViewIndex;
set selectedViewIndex(int newIndex) {
_selectedViewIndex = newIndex;
notifyListeners();
}
GlobalKey _mainKey = GlobalKey<NavigatorState>();
GlobalKey _homeKey = GlobalKey();
GlobalKey _secondKey = GlobalKey();
GlobalKey _thirdKey = GlobalKey();
GlobalKey _fourthKey = GlobalKey();
get mainKey => _mainKey;
get homeKey => _homeKey;
get secondKey => _secondKey;
get thirdKey => _thirdKey;
get fourthKey => _fourthKey;
}
The way I'm currently changing routes when on another page of the indexedStack
final RoutesProvider routesProvider = Provider.of<RoutesProvider>(context, listen: false);
final GlobalKey thirdKey = routesProvider.thirdKey;
routesProvider.selectedViewIndex = 2;
Navigator.pushReplacementNamed(thirdKey.currentContext, NamedRoutes.third);
The better way to navigate
Creating a route_generator
import 'package:flutter/material.dart';
import 'package:routing_prep/main.dart';
class RouteGenerator {
static Route<dynamic> generateRoute(RouteSettings settings) {
// Getting arguments passed in while calling Navigator.pushNamed
final args = settings.arguments;
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (_) => FirstPage());
case SecondPage.routeName:
// Validation of correct data type
if (args is String) {
return MaterialPageRoute(
builder: (_) => SecondPage(
data: args,
),
);
}
// If args is not of the correct type, return an error page.
// You can also throw an exception while in development.
return _errorRoute();
default:
// If there is no such named route in the switch statement, e.g. /third
return _errorRoute();
}
}
static Route<dynamic> _errorRoute() {
return MaterialPageRoute(builder: (_) {
return Scaffold(
appBar: AppBar(
title: Text('Error'),
),
body: Center(
child: Text('ERROR'),
),
);
});
}
}
As you can see, you've moved from having bits of routing logic everywhere around your codebase, to a single place for this logic - in the RouteGenerator. Now, the only navigation code which will remain in your widgets will be the one pushing named routes with a navigator.
Before you can run and test the app, there's still a bit of a setup to do for this RouteGenerator to function.
main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
...
// Initially display FirstPage
initialRoute: '/',
onGenerateRoute: RouteGenerator.generateRoute,
);
}
}
class FirstPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
...
RaisedButton(
child: Text('Go to second'),
onPressed: () {
// Pushing a named route
Navigator.of(context).pushNamed(
SecondPage.routeName,
arguments: 'Hello there from the first page!',
);
},
)
...
}
}
class SecondPage extends StatelessWidget {
static const routeName = "/second";
// This is a String for the sake of an example.
// You can use any type you want.
final String data;
SecondPage({
Key key,
#required this.data,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Routing App'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'Second Page',
style: TextStyle(fontSize: 50),
),
Text(
data,
style: TextStyle(fontSize: 20),
),
],
),
),
);
}
}
I´m trying to send data with pushName. Then i try to get this data to show in a Toast message.
PushName
Navigator.pushNamed(
context,
'/navigator',
arguments: <String, String>{
'instalation': widget.instalation,
'message': DemoLocalizations.of(context)
.text('cancel-message') +
" " +
widget.datameterValue.toString(),
},
);
Trying to retrieve data
class Navigation extends StatefulWidget {
final ConnectionPage args;
Navigation({Key key, this.message, this.instalation, this.args}) : super(key: key);
}
class _NavigationState extends State<Navigation> {
void initState() {
super.initState();
print(widget.args); //NULL
final snackBar = SnackBar(
duration: Duration(seconds: 5),
content: Text(widget.args.messsage+ '.', textAlign: TextAlign.center),
backgroundColor: Colors.red[700],
);
key.currentState.showSnackBar(snackBar);
}
}
The problem: Return null.
So: What is the right way to get data using pushName? In the documentation show how can we get data inside Scaffold but i need to get data in the initState.
UPDATE
Routes
routes: {
'/login': (context) => LoginPage(),
'/navigator': (context) => Navigation(),
'/home': (context) => HomePageScreen(),
'/connect': (context) => ConnectionPage(),
},
UPDATE 2
I try something like this
Navigator.pushNamed(
context,
'/navigator',
arguments: Navigation(
instalation: widget.instalation,
message: DemoLocalizations.of(context)
.text('cancel-message') +
" " +
widget.datameterValue.toString(),
),
);
To do this in initState You need WidgetsBinding.instance.addPostFrameCallback and ModalRoute.of(context).settings.arguments
Demo pass arguments: {'instalation': "123", "message": "456"}
You can see full code and working demo picture below
code snippet use push
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ExtractArgumentsScreen(),
// Pass the arguments as part of the RouteSettings. The
// ExtractArgumentScreen reads the arguments from these
// settings.
settings: RouteSettings(
arguments: {'instalation': "123", "message": "456"},
),
),
);
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final routeArgs1 =
ModalRoute.of(context).settings.arguments as Map<String, String>;
final instalation = routeArgs1['instalation'];
final message = routeArgs1['message'];
print('instalation ${instalation}');
print('message ${message}');
key.currentState
.showSnackBar(SnackBar(content: Text(message)));
});
}
code snippet use Navigator.pushNamed
return MaterialApp(
// Provide a function to handle named routes. Use this function to
// identify the named route being pushed, and create the correct
// Screen.
routes: {
'/extractArguments': (context) => ExtractArgumentsScreen(),
},
...
Navigator.pushNamed(
context,
ExtractArgumentsScreen.routeName,
arguments: {'instalation': "123", "message": "456"},
);
working demo
full code
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
// Provide a function to handle named routes. Use this function to
// identify the named route being pushed, and create the correct
// Screen.
routes: {
'/extractArguments': (context) => ExtractArgumentsScreen(),
},
onGenerateRoute: (settings) {
// If you push the PassArguments route
if (settings.name == PassArgumentsScreen.routeName) {
// Cast the arguments to the correct type: ScreenArguments.
final ScreenArguments args = settings.arguments;
// Then, extract the required data from the arguments and
// pass the data to the correct screen.
return MaterialPageRoute(
builder: (context) {
return PassArgumentsScreen(
title: args.title,
message: args.message,
);
},
);
}
},
title: 'Navigation with Arguments',
home: HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home Screen'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// A button that navigates to a named route that. The named route
// extracts the arguments by itself.
RaisedButton(
child: Text("Navigate to screen that extracts arguments"),
onPressed: () {
// When the user taps the button, navigate to the specific route
// and provide the arguments as part of the RouteSettings.
Navigator.pushNamed(
context,
ExtractArgumentsScreen.routeName,
arguments: {'instalation': "123", "message": "456"},
);
/*Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ExtractArgumentsScreen(),
// Pass the arguments as part of the RouteSettings. The
// ExtractArgumentScreen reads the arguments from these
// settings.
settings: RouteSettings(
arguments: {'instalation': "123", "message": "456"},
),
),
);*/
},
),
// A button that navigates to a named route. For this route, extract
// the arguments in the onGenerateRoute function and pass them
// to the screen.
RaisedButton(
child: Text("Navigate to a named that accepts arguments"),
onPressed: () {
// When the user taps the button, navigate to a named route
// and provide the arguments as an optional parameter.
Navigator.pushNamed(
context,
PassArgumentsScreen.routeName,
arguments: ScreenArguments(
'Accept Arguments Screen',
'This message is extracted in the onGenerateRoute function.',
),
);
},
),
],
),
),
);
}
}
// A Widget that extracts the necessary arguments from the ModalRoute.
class ExtractArgumentsScreen extends StatefulWidget {
static const routeName = '/extractArguments';
#override
_ExtractArgumentsScreenState createState() => _ExtractArgumentsScreenState();
}
class _ExtractArgumentsScreenState extends State<ExtractArgumentsScreen> {
final GlobalKey<ScaffoldState> key = new GlobalKey<ScaffoldState>();
final snackBar = SnackBar(
duration: Duration(seconds: 5),
content: Text("message" + '.', textAlign: TextAlign.center),
backgroundColor: Colors.red[700],
);
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final routeArgs1 =
ModalRoute.of(context).settings.arguments as Map<String, String>;
final instalation = routeArgs1['instalation'];
final message = routeArgs1['message'];
print('instalation ${instalation}');
print('message ${message}');
key.currentState
.showSnackBar(SnackBar(content: Text(message)));
});
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
// Extract the arguments from the current ModalRoute settings and cast
// them as ScreenArguments.
final routeArgs =
ModalRoute.of(context).settings.arguments as Map<String, String>;
final instalation = routeArgs['instalation'];
final message = routeArgs['message'];
return Scaffold(
key: key,
appBar: AppBar(
title: Text(' ${routeArgs['code']} '),
),
body: Column(
children: <Widget>[
Center(
child: Text('instalation ${instalation}'),
),
RaisedButton(
onPressed: () {
key.currentState.showSnackBar(snackBar);
},
),
],
),
);
}
}
// A Widget that accepts the necessary arguments via the constructor.
class PassArgumentsScreen extends StatelessWidget {
static const routeName = '/passArguments';
final String title;
final String message;
// This Widget accepts the arguments as constructor parameters. It does not
// extract the arguments from the ModalRoute.
//
// The arguments are extracted by the onGenerateRoute function provided to the
// MaterialApp widget.
const PassArgumentsScreen({
Key key,
#required this.title,
#required this.message,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Text(message),
),
);
}
}
// You can pass any object to the arguments parameter. In this example,
// create a class that contains both a customizable title and message.
class ScreenArguments {
final String title;
final String message;
ScreenArguments(this.title, this.message);
}
So, I see you're using the simple routes approach.
In order to extract route arguments you need to supply an onGenerateRoute function to your MaterialApp (or Cupertino, I guess).
You can find an exhaustive example on how to do it here, so I won't crowd this answer more than that.
Hope this solves your problem, happy coding!