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),
],
),
);
}
}
Related
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()];
So in my app, I have a screen with a tabBar. When the screen loads, it sorts out the items and moves them into different tabs. But when I run this, The Items keep duplicating, and I am shown an error in the debug console that says I called setState() after dispose()
Here's the code for the screen:
import 'package:flutter/material.dart';
import './uiComponents/customWidgets.dart';
import './ticketsComponents/ticketsList.dart';
import './tabs.dart';
class Tickets extends StatefulWidget {
Tickets({ this.tickets, this.user });
final List tickets;
final Map user;
#override
_TicketsState createState() => new _TicketsState();
}
class _TicketsState extends State<Tickets> with SingleTickerProviderStateMixin {
TabController controller; // Tab controller for the screen
List _tickets;
// Variables to Store the sorted Tickets
List _availableTickets = [];
List _usedTickets = [];
List _expiredTickets = [];
#override
void initState(){
controller = new TabController(
vsync: this,
length: 4,
initialIndex: 1
);
WidgetsBinding.instance.addPersistentFrameCallback((_) async {
// Get the tickets and sort them
_tickets = widget.tickets;
if(_tickets != null){
_sortTickets();
}
});
super.initState();
}
#override
void dispose(){
controller.dispose();
super.dispose();
}
// DELETE A TICKET (FROM ID)
void deleteTicket(int id){
setState(() {
_tickets.removeWhere((item)=> item["id"] == id);
_availableTickets = [];
_usedTickets = [];
_expiredTickets = [];
_sortTickets();
});
}
// SORT THE TICKETS INTO AVAILABLE / UNUSED, USED AND EXPIRED
void _sortTickets(){
for (int i = 0; i < _tickets.length; i++){
Map ticket = _tickets[i];
if(ticket["isUsed"]){
setState(() {
_usedTickets.add(ticket);
});
}
else if(ticket["expired"]){
setState(() {
_expiredTickets.add(ticket);
});
}
else{
setState(() {
_availableTickets.add(ticket);
});
}
}
}
// NAVIGATE TO MAIN TAB AND CLEAR PREVIOUS ROUTES
void _navProfile(){
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (context) => Tabs(
user: widget.user
)
),
(route)=> false
);
}
// TabBar for Filtering Tickets
TabBar _buildTabBar(){
return new TabBar(
controller: controller,
indicatorWeight:2.2,
labelStyle: TextStyle(
fontSize:14.0,
fontWeight: FontWeight.bold
),
unselectedLabelStyle: TextStyle(
fontWeight:FontWeight.normal
),
labelColor: blue,
indicatorColor: blue,
unselectedLabelColor: Colors.black,
tabs: [
Tab(text: "All"),
Tab(text: "Available"),
Tab(text: "Used"),
Tab(text: "Expired")
],
);
}
// THE AppBar with the sub menu under it
AppBar _buildAppBar(){
Function onBackButtonPressed = _navProfile;
return new AppBar(
title: customTextBold("My Tickets"),
iconTheme: new IconThemeData(
color: blue
),
leading: GestureDetector(
child: Icon(Icons.arrow_back, color: blue),
onTap: onBackButtonPressed,
),
elevation: 1.0,
backgroundColor: Colors.white,
bottom: _buildTabBar()
);
}
// BUILD MAIN SCREEN
Container _buildTicketsPage(){
return new Container(
padding: EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: backgroundBlue
),
child: new TabBarView(
controller: controller,
children: [
TicketsList(
tickets: _tickets,
deleteTicket: deleteTicket,
),
TicketsList(
tickets: _availableTickets,
deleteTicket: deleteTicket,
),
TicketsList(
tickets: _usedTickets,
deleteTicket: deleteTicket,
),
TicketsList(
tickets: _expiredTickets,
deleteTicket: deleteTicket,
)
],
)
);
}
// UI
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: _buildAppBar(),
body: (_tickets == null)
? buildLoadingScreen("Fetching Tickets")
: _buildTicketsPage()
);
}
}
Running this will render the correct screen, but the ticket items will start duplicating, and this error is displayed on the debug console:
.
E/flutter (31673): [ERROR:flutter/lib/ui/ui_dart_state.cc(148)] Unhandled Exception: setState() called after dispose(): _TicketsState#2cafe(lifecycle state: defunct, not mounted, ticker inactive)
E/flutter (31673): This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback. The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the t
Please how do I fix this ?
add before each setState a condition
if(!mounted) return;
for example:
if(!mounted) return;
setState(() {
_expiredTickets.add(ticket);
});
class _HomeState extends State<Home> {
int _currentIndex = 0;
final List<Widget> _children = [Profile(), ServiceRequestList()];
void _onTap(int index) {
setState(() {
_currentIndex = index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: _children[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
onTap: _onTap,
currentIndex: _currentIndex,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.account_circle),
title: Text('Profile'),
),
BottomNavigationBarItem(
icon: Icon(Icons.assignment),
title: Text('Service'),
),
],
));
}
}
I am using BottomNavigationBar to navigate to diff SliverList. I observe that It does not retain to the last row that I scrolled. How can fix this?
PageStorageKey is what you are looking for!
Within your Profile page and ServiceRequestList page's SliverList widget, set a unique page storage key like the following:
SliverList(
key: PageStorageKey("somethingUnique"),
)
PageStorageKey works with any widget that has a scrolling region.
I have a PageView used with a BottomNavigationBar so that I can have swipeable and tappable tabs with a bottom bar rather than the normal navigation bar. Then I have two tabs/pages you can swipe between. One has a form with 4 UI elements and the other has no UI elements yet. Everything works fine but the performance of the PageView is very bad.
When I swipe between pages it is extremely slow and jumpy at first, definitely not the 60 frames per second promised by Flutter. Probably not even 30. After swiping several times though the performance gets better and better until its almost like a normal native app.
Below is my page class that includes the PageView, BottomNavigationBar, and logic connecting them. does anyone know how I can improve the performance of the PageView?
class _TabbarPageState extends State<TabbarPage> {
int _index = 0;
final controller = PageController(
initialPage: 0,
keepPage: true,
);
final _tabPages = <Widget>[
StartPage(),
OtherPage(),
];
final _tabs = <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.play_arrow),
title: Text('Start'),
),
BottomNavigationBarItem(
icon: Icon(Icons.accessibility_new),
title: Text('Other'),
)
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: PageView(
controller: controller,
children: _tabPages,
onPageChanged: _onPageChanged,
),
bottomNavigationBar: BottomNavigationBar(
items: _tabs,
onTap: _onTabTapped,
currentIndex: _index,
),
floatingActionButton: _index != 1
? null
: FloatingActionButton(
onPressed: () {},
tooltip: 'Test',
child: Icon(Icons.add),
),
);
}
void _onTabTapped(int index) {
controller.animateToPage(
index,
duration: Duration(milliseconds: 300),
curve: Curves.ease,
);
setState(() {
_index = index;
});
}
void _onPageChanged(int index) {
setState(() {
_index = index;
});
}
}
Ensure you performance test with profile or release builds only. Evaluating performance with debug builds is completely meaningless.
https://flutter.io/docs/testing/ui-performance
https://flutter.io/docs/cookbook/testing/integration/profiling
Sorry but Günter's answer didn't helped me! You have to set physics: AlwaysScrollableScrollPhysics() And your performance increases.
Worked for me 👍
I am new to flutter, please tell me if this is wrong.
Have the same problem, here is my effort. Wors for me.
class HomePage extends StatefulWidget {
#override
State createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
var _currentIndex = 1;
var _pageController = PageController(initialPage: 1);
var _todoPage, _inProgressPage, _donePage, _userPage;
#override
void initState() {
this._currentIndex = 1;
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageView.builder(
controller: this._pageController,
onPageChanged: (index) {
setState(() {
this._currentIndex = index.clamp(0, 3);
});
},
itemCount: 4,
itemBuilder: (context, index) {
if (index == 0) return this.todoPage();
if (index == 1) return this.inProgressPage();
if (index == 2) return this.donePage();
if (index == 3) return this.userPage();
return null;
},
),
bottomNavigationBar: buildBottomNavigationBar(),
);
}
Widget buildBottomNavigationBar() {
return BottomNavigationBar(
showUnselectedLabels: false,
items: [
BottomNavigationBarItem(title: Text("待办"), icon: Icon(Icons.assignment)),
BottomNavigationBarItem(title: Text("进行"), icon: Icon(Icons.blur_on)),
BottomNavigationBarItem(title: Text("完成"), icon: Icon(Icons.date_range)),
BottomNavigationBarItem(title: Text("我的"), icon: Icon(Icons.account_circle)),
],
currentIndex: this._currentIndex,
onTap: (index) {
setState(() {
this._currentIndex = index.clamp(0, 3);
});
_pageController.jumpToPage(this._currentIndex);
},
);
}
Widget todoPage() {
if (this._todoPage == null) this._todoPage = TodoPage();
return this._todoPage;
}
Widget inProgressPage() {
if (this._inProgressPage == null) this._inProgressPage = InProgressPage();
return this._inProgressPage;
}
Widget donePage() {
if (this._donePage == null) this._donePage = DonePage();
return this._donePage;
}
Widget userPage() {
if (this._userPage == null) this._userPage = UserPage();
return this._userPage;
}
}
I just cache the pages that pageview hold. this REALLY smooth my pageview a lot, like native. but would prevent hotreload (ref: How to deal with unwanted widget build?),.
Try this hack,
Apply viewportFraction to your controller with value 0.99(it can be 0.999 or 0.9999 hit and try until you get desired result)
final controller = PageController(
viewportFraction: 0.99 );
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> {}