How to persist BottomNavigationBar when using Flutter navigation and routes - dart

I'm making a project with 4 different pages. I use a "BottomNavigationBar" widget to navigate to each page. When an icon in the "BottomNavigationBar" is pressed, I display a different page in the Scaffold body. I don't use any routing so when a user presses back on Android, the app closes. Something I don't want happening.
All the guides I have found reload the whole "Scaffold" when navigating, but I want to only update the "body" property of the "Scaffold" widget. When a Navigation.pop() occurs I again only want the "Scaffold" body to change.
I have found a post around the same issue, but the answer didn't work for me.
Another workaround I can try is making a custom history list, that I then update when pages are changed. Catching OnWillPop event to update the pages when the back button is pressed. I haven't tried this because I feel like there has to be a better way.
The Scaffold widget that displays the page.
Widget createScaffold() {
return Scaffold(
backgroundColor: Colors.white,
appBar: EmptyAppBar(),
body: _displayedPage,
bottomNavigationBar: createBottomNavigationbar(),
);
}
The BottomNavigationBar widget.
Widget createBottomNavigationbar() {
return BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: _selectedIndex,
onTap: _onItemTapped,
items: [
BottomNavigationBarItem(
icon: new Icon(Icons.home,
color: _selectedIndex == 0 ? selectedColor : unselectedColor),
title: new Text('Home',
style: new TextStyle(
color:
_selectedIndex == 0 ? selectedColor : unselectedColor)),
),
BottomNavigationBarItem(
icon: new Icon(Icons.show_chart,
color: _selectedIndex == 1 ? selectedColor : unselectedColor),
title: new Text('Month',
style: new TextStyle(
color:
_selectedIndex == 1 ? selectedColor : unselectedColor)),
),
BottomNavigationBarItem(
icon: Icon(Icons.history,
color: _selectedIndex == 2 ? selectedColor : unselectedColor),
title: Text('History',
style: new TextStyle(
color: _selectedIndex == 2
? selectedColor
: unselectedColor))),
BottomNavigationBarItem(
icon: Icon(Icons.settings,
color: _selectedIndex == 3 ? selectedColor : unselectedColor),
title: Text('Settings',
style: new TextStyle(
color: _selectedIndex == 3
? selectedColor
: unselectedColor)))
],
);
}
Methods that update the state of the displayed page.
void _onItemTapped(int index) {
_changeDisplayedScreen(index);
setState(() {
_selectedIndex = index;
});
}
void _changeDisplayedScreen(int index) {
switch (index) {
case 0:
setState(() {
_displayedPage = new LatestReadingPage();
});
break;
case 1:
setState(() {
_displayedPage = new HomeScreen();
//Placeholder
});
break;
case 2:
setState(() {
_displayedPage = new HomeScreen();
//Placeholder
});
break;
case 3:
setState(() {
_displayedPage = new HomeScreen();
//Placeholder
});
break;
default:
setState(() {
_displayedPage = new LatestReadingPage();
});
break;
}
}
}
What I want is to be able to use the Flutter Navigation infrastructure, but only update the body property of the Scaffold widget when changing pages. Instead of the whole screen.
A lot like the Youtube app or Google news app.

I have added an answer to the post that you linked: https://stackoverflow.com/a/59133502/6064621
The answer below is similar to the one above, but I've also added unknown routes here:
What you want can be achieved by using a custom Navigator.
The Flutter team did a video on this, and the article they followed is here: https://medium.com/flutter/getting-to-the-bottom-of-navigation-in-flutter-b3e440b9386
Basically, you will need to wrap the body of your Scaffold in a custom Navigator:
class _MainScreenState extends State<MainScreen> {
final _navigatorKey = GlobalKey<NavigatorState>();
// ...
#override
Widget build(BuildContext context) {
return Scaffold(
body: Navigator(
key: _navigatorKey,
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
// Manage your route names here
switch (settings.name) {
case '/':
builder = (BuildContext context) => HomePage();
break;
case '/page1':
builder = (BuildContext context) => Page1();
break;
case '/page2':
builder = (BuildContext context) => Page2();
break;
default:
throw Exception('Invalid route: ${settings.name}');
}
// You can also return a PageRouteBuilder and
// define custom transitions between pages
return MaterialPageRoute(
builder: builder,
settings: settings,
);
},
),
bottomNavigationBar: _yourBottomNavigationBar,
);
}
}
Within your bottom navigation bar, to navigate to a new screen in the new custom Navigator, you just have to call this:
_navigatorKey.currentState.pushNamed('/yourRouteName');
If you don't used named routes, then here is what you should do for your custom Navigator, and for navigating to new screens:
// Replace the above onGenerateRoute function with this one
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute(
builder: (BuildContext context) => YourHomePage(),
settings: settings,
);
},
_navigatorKey.currentState.push(MaterialPageRoute(
builder: (BuildContext context) => YourNextPage(),
));
To let Navigator.pop take you to the previous view, you will need to wrap the custom Navigator with a WillPopScope:
#override
Widget build(BuildContext context) {
return Scaffold(
body: WillPopScope(
onWillPop: () async {
if (_navigatorKey.currentState.canPop()) {
_navigatorKey.currentState.pop();
return false;
}
return true;
},
child: Navigator(
// ...
),
),
bottomNavigationBar: _yourBottomNavigationBar,
);
}
And that should be it! No need to manually handle pop too much or manage a custom history list.

wrap your body inside the IndexedStack widget. Like this:
body: IndexedStack(
index: selectedIndex,
children: _children, //define a widget children list
),
Here's a link https://medium.com/flutter/getting-to-the-bottom-of-navigation-in-flutter-b3e440b9386

Have you tried this?
return Scaffold(
backgroundColor: Colors.white,
appBar: EmptyAppBar(),
body: _displayedPage[_selectedIndex],
bottomNavigationBar: _createBottomNavigationbar,
);
Then you make a widget list of all the Screens you want:
List<Widget> _displayedPage = [LatestReadingPage(), HomeScreen(), ExampleScreen()];

Related

How to detect re-select event and navigate to the first page in the cupetinotab in flutter

I created bottom navigation for iOS using CupetinoTabBar in flutter and I'd like to detect re-select tab event and go back to the its first page.
Is there any way to implement the function?
For iOS, it's a pretty common function but I don't know how to do it.
The code is below.
Thank you in advance.
class _MainTabScreenState extends State<MainTabScreen> {
final List<Widget> _pages = [FeedScreen(), Feed2Screen(), HomeScreen()];
#override
Widget build(BuildContext context) {
return CupertinoTabScaffold(
tabBar: CupertinoTabBar(
backgroundColor: Colors.black,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home)),
BottomNavigationBarItem(icon: Icon(Icons.movie)),
BottomNavigationBarItem(icon: Icon(Icons.person)),
],
),
tabBuilder: (BuildContext context, int index) {
return CupertinoTabView(
builder: (ctx) {
return _pages[index];
},
);
},
);
}
}

How to programmatically open a Drawer by tapping on a BottomNavigationBarItem?

I'm making a flutter app and I need to be able to open the Drawer by tapping on a BottomNavigationBarItem. Is there any way to do that?
The UX designer guy put the drawer menu icon at index 0 in the bottom navigation bar. I tried to find an answer in the Flutter documentation but I didn't find anything relevant. I actually found a way of opening it programmatically (as you can see below) but it does not work like that in my case.
class _HomeState extends State<Home> {
int _currentIndex = 1; // 0 = menu
final List<Widget> _children = [
PlaceholderWidget(Colors.deepPurple),
PlaceholderWidget(Colors.white),
DiagnosisWidget(),
FindUsWidget(),
];
_navItem(String text, IconData icon) {
return BottomNavigationBarItem(
/* Building Bottom nav item */
);
}
void onTabTapped(int index) {
setState(() {
if(index == 0) {
Scaffold.of(context).openDrawer(); // This is what I've tried
}
else {
_currentIndex = index;
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: Drawer(
child: MyDrawer(),
),
body: _children[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
onTap: onTabTapped,
currentIndex: _currentIndex,
type: BottomNavigationBarType.fixed, // 4+ items in the bar
items: [
_navItem('MENU', Icons.menu),
_navItem('HOME', Icons.home),
_navItem('DIAGNOSIS', Icons.person),
_navItem('FIND US', Icons.location_on),
],
),
);
}
}
Instead of having the Drawer showing up, I get the following error message :
Scaffold.of() called with a context that does not contain a Scaffold.
It's because in onTabTapped you use a context that doesn't contain the Scaffold you create.
You instantiate the Scaffold in build but in onTabTapped you're looking for a parent Scaffold in the current context (_HomeState context).
You can use Builder inside the Scaffold to get the correct context or use a GlobalKey on your Scaffold.
See this answer for more details.
EDIT:
In your case a GlobalKey is mush easier to implement.
You can do the following :
class _HomeState extends State<Home> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); // ADD THIS LINE
int _currentIndex = 1; // 0 = menu
final List<Widget> _children = [
PlaceholderWidget(Colors.deepPurple),
PlaceholderWidget(Colors.white),
DiagnosisWidget(),
FindUsWidget(),
];
_navItem(String text, IconData icon) {
return BottomNavigationBarItem(
/* Building Bottom nav item */
);
}
void onTabTapped(int index) {
setState(() {
if(index == 0) {
_scaffoldKey.currentState.openDrawer(); // CHANGE THIS LINE
}
else {
_currentIndex = index;
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey, // ADD THIS LINE
drawer: Drawer(
child: MyDrawer(),
),
body: _children[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
onTap: onTabTapped,
currentIndex: _currentIndex,
type: BottomNavigationBarType.fixed, // 4+ items in the bar
items: [
_navItem('MENU', Icons.menu),
_navItem('HOME', Icons.home),
_navItem('DIAGNOSIS', Icons.person),
_navItem('FIND US', Icons.location_on),
],
),
);
}
}

Remove App Bar Title from Navigation Drawer in Flutter?

I have created a Navigation Drawer in Flutter.
Source available here - https://github.com/deadcoder0904/flutter_navigation_drawer
It looks like this -
First Screen
When I click the button, it goes to
Second Screen
When I click the button, it goes to
Tabs Screen
When I click the hamburger icon on First Screen, it goes to
Drawer Screen
Now when I click on the 2nd List Item on Drawer Screen, I get the Second Screen like this
Relevant code is in navigation_drawer.dart which looks like -
class NavigationDrawer extends StatefulWidget {
_NavigationDrawerState createState() => _NavigationDrawerState();
}
class _NavigationDrawerState extends State<NavigationDrawer> {
int _selectionIndex = 0;
final drawerItems = [
DrawerItem("First Screen", Icons.looks_one),
DrawerItem("Second Screen", Icons.looks_two),
DrawerItem("Tabs", Icons.tab),
];
_getDrawerItemScreen(int pos) {
switch (pos) {
case 1:
return SecondScreen();
case 2:
return Tabs();
default:
return FirstScreen();
}
}
_onSelectItem(int index) {
setState(() {
_selectionIndex = index;
_getDrawerItemScreen(_selectionIndex);
});
Navigator.of(context).pop();
}
#override
Widget build(BuildContext context) {
List<Widget> drawerOptions = [];
for (var i = 0; i < drawerItems.length; i++) {
var d = drawerItems[i];
drawerOptions.add(ListTile(
leading: Icon(d.icon),
title: Text(
d.title,
style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w400),
),
selected: i == _selectionIndex,
onTap: () => _onSelectItem(i),
));
}
return Scaffold(
appBar: AppBar(
title: Text('First Screen'),
),
drawer: Drawer(
child: Column(
children: <Widget>[
UserAccountsDrawerHeader(
accountName: Text('Akshay Kadam (A2K)'),
accountEmail: Text('a2k#gmail.com'),
),
Column(
children: drawerOptions,
),
],
),
),
body: _getDrawerItemScreen(_selectionIndex),
);
}
}
How do I get the Second Screen without the Hamburger Icon & First Screen title?
First, change your code to set HomePage
body: _getDrawerItemScreen(_selectionIndex),
to
body: FirstScreen(),
Secondly,
_onSelectItem(int index) {
setState(() {
_selectionIndex = index;
_getDrawerItemScreen(_selectionIndex);
});
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => _getDrawerItemScreen(_selectionIndex),
),
);
}

Flutter search delegate doesn't work when navigate back from a route

I'm using Flutter SearchDelegate in my app and here's the code:
class NameSearch extends SearchDelegate<String> {
#override
List<Widget> buildActions(BuildContext context) {
return [
IconButton(
icon: Icon(Icons.clear),
onPressed: () {
query = "";
},
)
];
}
#override
Widget buildLeading(BuildContext context) {
return IconButton(
icon: AnimatedIcon(
icon: AnimatedIcons.menu_arrow, progress: transitionAnimation),
onPressed: () {
close(context, null);
},
);
}
#override
Widget buildResults(BuildContext context) {
return null;
}
#override
Widget buildSuggestions(BuildContext context) {
suggestionList = query.isEmpty ? [] : List.generate(nameList.length,
(i) => nameList[i]).where((p) => p.name.startsWith(query)).toList();
return ListView.builder(
itemBuilder: (context, index) => ListTile(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(
detail: suggestionList[index],
)));
},
leading: Icon(Icons.book),
title: RichText(
text: TextSpan(
text: suggestionList[index].name.substring(0, query.length),
style: TextStyle(
color: Colors.black, fontWeight: FontWeight.bold),
children: [
TextSpan(
text: suggestionList[index].name.substring(query.length),
style: TextStyle(color: Colors.grey))
]),
),
),
itemCount: suggestionList.length,
);
}
}
When I clicked on an item in a suggestion list, It gets me to the new detail screen and works properly. But when I want to back to the search screen, the text input become like that:
And I can't insert any text anymore, till restart the app or go to another page and after that go back to search page again!
And here is my DetailScreen code:
class DetailScreen extends StatelessWidget {
final BookDetail detail;
DetailScreen({Key key, #required this.detail}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(detail.name),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Text(detail.description),
),
);
}
}
In the line 83 -85 of the flutter search source code :
Once the user has selected a search result, [SearchDelegate.close] should be
called to remove the search page from the top of the navigation stack and
to notify the caller of [showSearch] about the selected search result.
So the the showSearch is structed for single-use only. And you have to call it again when you navigate back from your DetailScreen if you intend to use it for another search query.
I was facing same issue so what i did is i simply copy the search.dart from material library and replace
bool get maintainState => false;
to
bool get maintainState => true;
on line 294 it worked for me.

Flutter persistent navigation bar with named routes?

I've been searching around for a good navigation/router example for Flutter but I have not managed to find one.
What I want to achieve is very simple:
Persistent bottom navigation bar that highlights the current top level route
Named routes so I can navigate to any route from anywhere inside the app
Navigator.pop should always take me to the previous view I was in
The official Flutter demo for BottomNavigationBar achieves 1 but back button and routing dont't work. Same problem with PageView and TabView. There are many other tutorials that achieve 2 and 3 by implementing MaterialApp routes but none of them seem to have a persistent navigation bar.
Are there any examples of a navigation system that would satisfy all these requirements?
All of your 3 requirements can be achieved by using a custom Navigator.
The Flutter team did a video on this, and the article they followed is here: https://medium.com/flutter/getting-to-the-bottom-of-navigation-in-flutter-b3e440b9386
Basically, you will need to wrap the body of your Scaffold in a custom Navigator:
class _MainScreenState extends State<MainScreen> {
final _navigatorKey = GlobalKey<NavigatorState>();
// ...
#override
Widget build(BuildContext context) {
return Scaffold(
body: Navigator(
key: _navigatorKey,
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
// Manage your route names here
switch (settings.name) {
case '/':
builder = (BuildContext context) => HomePage();
break;
case '/page1':
builder = (BuildContext context) => Page1();
break;
case '/page2':
builder = (BuildContext context) => Page2();
break;
default:
throw Exception('Invalid route: ${settings.name}');
}
// You can also return a PageRouteBuilder and
// define custom transitions between pages
return MaterialPageRoute(
builder: builder,
settings: settings,
);
},
),
bottomNavigationBar: _yourBottomNavigationBar,
);
}
}
Within your bottom navigation bar, to navigate to a new screen in the new custom Navigator, you just have to call this:
_navigatorKey.currentState.pushNamed('/yourRouteName');
To achieve the 3rd requirement, which is Navigator.pop taking you to the previous view, you will need to wrap the custom Navigator with a WillPopScope:
#override
Widget build(BuildContext context) {
return Scaffold(
body: WillPopScope(
onWillPop: () async {
if (_navigatorKey.currentState.canPop()) {
_navigatorKey.currentState.pop();
return false;
}
return true;
},
child: Navigator(
// ...
),
),
bottomNavigationBar: _yourBottomNavigationBar,
);
}
And that should be it! No need to manually handle pop or manage a custom history list.
CupertinoTabBar behave exactly same as you described, but in iOS style. It can be used in MaterialApps however.
Sample Code
What you are asking for would violate the material design specification.
On Android, the Back button does not navigate between bottom
navigation bar views.
A navigation drawer would give you 2 and 3, but not 1. It depends on what's more important to you.
You could try using LocalHistoryRoute. This achieves the effect you want:
class MainPage extends StatefulWidget {
#override
State createState() {
return new MainPageState();
}
}
class MainPageState extends State<MainPage> {
int _currentIndex = 0;
List<int> _history = [0];
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Bottom Nav Back'),
),
body: new Center(
child: new Text('Page $_currentIndex'),
),
bottomNavigationBar: new BottomNavigationBar(
currentIndex: _currentIndex,
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(
icon: new Icon(Icons.touch_app),
title: new Text('keypad'),
),
new BottomNavigationBarItem(
icon: new Icon(Icons.assessment),
title: new Text('chart'),
),
new BottomNavigationBarItem(
icon: new Icon(Icons.cloud),
title: new Text('weather'),
),
],
onTap: (int index) {
_history.add(index);
setState(() => _currentIndex = index);
Navigator.push(context, new BottomNavigationRoute()).then((x) {
_history.removeLast();
setState(() => _currentIndex = _history.last);
});
},
),
);
}
}
class BottomNavigationRoute extends LocalHistoryRoute<void> {}

Resources