I have been searching around for ages online for an example of a flutter app that supports the following:
- Persistent bottom navigation bar
- Navigator support for each bottom navigation bar item that persists state
- Navigators update the URL in flutter web on navigation
- Navigators support hyperlinking to a specific page in flutter web. For example if a user types www.myappdomain.com/#/profile they get navigated to the profile page. Or they type www.myappdomain.com/#/profile?id=123 they get navigated to the profile page for user 123.
I've been able to achieve points 1 and 2 with the following code:
import 'package:flutter/material.dart';
void main() {
runApp(BaseApp());
}
class BaseApp extends StatefulWidget {
@override
_BaseAppState createState() => _BaseAppState();
}
class _BaseAppState extends State<BaseApp> {
// define all navigation tabs for user
static List<NavigationTabModel> _navigationTabs = [
NavigationTabModel(
title: 'Home',
icon: Icons.home,
url: '/home',
navigatorKey: GlobalKey<NavigatorState>(),
),
NavigationTabModel(
title: 'Search',
icon: Icons.search,
url: '/search',
navigatorKey: GlobalKey<NavigatorState>(),
),
NavigationTabModel(
title: 'Profile',
icon: Icons.person,
url: '/profile',
navigatorKey: GlobalKey<NavigatorState>(),
),
];
// route generator used to build all material page routes
RouteGenerator _routeGenerator = RouteGenerator();
// set the current tab to the home page
int _currentIndex = 0;
void _select(int index) {
if (index == _currentIndex) {
// case 1 - if user presses on currently selected tab
// pop to first route - i.e. ensure no routes are over laid on top of the current route
_navigationTabs[_currentIndex]
.navigatorKey
.currentState
.popUntil((route) {
return route.isFirst;
});
} else {
// case 2 - user selects any other tab
// rebuild application state with the newly selected navigation tab
setState(() {
_currentIndex = index;
});
}
}
/// generate a list of navigators that will have their state persisted in an
/// indexed stack.
List<Widget> _getPersistantStack() {
return _navigationTabs.map((tab) {
return WillPopScope(
onWillPop: () async {
return !await tab.navigatorKey.currentState.maybePop();
},
child: Navigator(
key: tab.navigatorKey,
initialRoute: tab.url,
onGenerateRoute: _routeGenerator.generateRoute,
),
);
}).toList();
}
@override
Widget build(BuildContext context) {
/// ********************* HOLD POINT *********************
/// MaterialApp contains our top-level Navigator. Top level navigator is
/// required to enable navigation via urls in flutter web. Likely that this
/// section requires refractoring in some way to enable url updates from the
/// nested navigators and hyperlinking from web browsers to specific pages
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: SafeArea(
child: IndexedStack(
children: _getPersistantStack(),
index: _currentIndex,
),
),
bottomNavigationBar: BottomNavigationBar(
items: _navigationTabs.map((tab) {
return BottomNavigationBarItem(
label: tab.title,
icon: Icon(tab.icon),
);
}).toList(),
onTap: (int index) {
_select(index);
},
currentIndex: _currentIndex,
type: BottomNavigationBarType.fixed,
// hide titles on navigation bar
showSelectedLabels: false,
showUnselectedLabels: false,
),
),
);
}
}
class NavigationTabModel {
final String title;
final IconData icon;
final String url;
final GlobalKey<NavigatorState> navigatorKey;
NavigationTabModel({
this.title,
this.icon,
this.url,
this.navigatorKey,
});
}
class RouteGenerator {
Route<dynamic> generateRoute(RouteSettings settings) {
// Widget builder (function that returns a widget) to construct the route page
WidgetBuilder builder;
// build different route (page) based on the route passed to the navigator
switch (settings.name) {
case '/home':
builder = (BuildContext context) {
return SamplePage(name: 'home');
};
break;
case '/search':
builder = (BuildContext context) {
return SamplePage(name: 'search');
};
break;
case '/profile':
builder = (BuildContext context) {
return SamplePage(name: 'profile');
};
break;
case '/':
builder = null;
break;
default:
// If there is no such named route in the switch statement
builder = (BuildContext context) {
return SamplePage();
};
}
// prevent page being added to default '/' route
if (builder == null) {
return null;
}
return MaterialPageRoute(
builder: builder,
settings: settings,
);
}
}
class SamplePage extends StatelessWidget {
final String name;
SamplePage({
this.name,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('$name'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
RaisedButton(
child: Text('push new route'),
onPressed: () {
Navigator.of(context).pushNamed('/$name');
},
),
SizedBox(
height: 16,
),
Expanded(
child: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return Card(
child: Text(
index.toString(),
),
);
},
),
),
],
),
),
);
}
}
However I cannot figure out how to update this app to achieve points 3 and 4. Does anyone know how this can be achieved?