I'm learning flutter and starting with a simple 2 tab, bottom navigation bar app (specifically Cupertino based). This video does a decent job of explaining the fundamentals but I am missing something.
The example provided in the video suggests that when you tap on a BottomNavigationBarItem, you don't actually navigate to a new screen, you simply re-render the widgets on the existing screen. The video only invokes Navigator after he constructs a button widget in the page container.
Is a BottomNavigationBarItem in a CupertinoTabBar supposed to invoke Navigator? Or am I misunderstanding the use case of CupertinoTabBar entirely?
Here is my example code:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class HomeScreen extends StatelessWidget
{
#override
Widget build(BuildContext context) {
return CupertinoTabScaffold(
tabBar: CupertinoTabBar(
items: <BottomNavigationBarItem> [
BottomNavigationBarItem(
icon: Icon(Icons.menu),
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.person_solid),
),
],
),
tabBuilder: (BuildContext context, int index) {
switch (index) {
case 0:
return new Container(
color: Colors.red
); break;
case 1:
return new Container(
color: Colors.white
); break;
default: {
return new Container(
color: Colors.white
);
}
}
},
);
}
}
I'm wondering how I would alter the switch to invoke Navigator or not.
So the pattern that seems to be correct is:
The CupertinoTabBar indices / screens which are navigated to via BottomNavigationBarItem are really just CupertinoPageScaffold widgets. Then within those CupertinoPageScaffold screens, you can use the Navigator methods to navigate to other routes / screens.
The main reason why this makes sense is each index / screen maintains its own navigation stack so you can be on screen (index) 1, navigate a few pages, go to index 0 and back and your navigation stack state is preserved.
It took me a few times reading the documentation on CupertinoTabScaffold to drill it in to my brain but it makes sense.
Let me know if I'm wrong on this.
Related
I'm new to flutter and I'm trying to implement a persistent bottom navigation bar with the "persistent_bottom_nav_bar 4.0.2" plugin.
The issue I'm running into is, that I have to declare my AppBar in the page that implements the bottom bar, since that is where my scaffold is and the screens are just widgets loaded by the plugin into this scaffold. Now, if I'm navigating to a different page from one of the tabs, I want the AppBar to show a back button. For that I need to add an AppBar in the page I'm navigating to, but then I have two AppBars stacked. Thus, I need to hide the AppBar from the bottom bar page when I push pages into the navigator stack of the active tab.
Currently I'm trying to get the name of the route with ModalRoute.of(context)?.settings.name and compare it with the name of the route of the page that holds the bottom bar, and if they're not equal I can pass an argument back and hide the app bar. (I'm open for better solutions, that's the first I came up with). The problem is, that the named route does not work on the navigators of the different tabs. I'm getting something like this as the name: /9f580fc5-c252-45d0-af25-9429992db112.
My HomePage looks like this:
#override
Widget build(BuildContext context) {
print(ModalRoute.of(context)?.settings.name); // this gives me the correct route name
return Scaffold(
appBar: _showAppBar ? MyAppBar("") : null,
body: PersistentTabView(
context,
controller: _controller,
screens: _buildScreens(),
items: _navBarsItems(),
confineInSafeArea: true,
popAllScreensOnTapOfSelectedTab: true,
popActionScreens: PopActionScreensType.all,
navBarStyle: NavBarStyle.style6,
));
}
List<PersistentBottomNavBarItem> _navBarsItems() {
return [
PersistentBottomNavBarItem(
routeAndNavigatorSettings: const RouteAndNavigatorSettings(onGenerateRoute:
router.generateRoute),
icon: const Icon(Icons.add_circle_outline),
title: ("Add"))
];
}
List<Widget> _buildScreens() {
return [
const AddPage(),
];
}
On the AddPage (which is one of the tab views and only a widget without scaffold) I want a button, that navigates to a different screen. If that happens I want to hide the AppBar from the home_page and show the AppBar of this page to have the back navigation.
I do the navigation like this:
child: IconButton(
icon: const Icon(Icons.add),
color: Colors.white,
onPressed: () {
Navigator.pushNamed(context, AddCollectionRoute, arguments: 'test');
RouteSettings? routeSettings = ModalRoute.of(context)?.settings;
},
),
But here I now have "/9f580fc5-c252-45d0-af25-9429992db112" as name and null as argument.
Routes are generated like this:
Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case AddCollectionRoute:
return MaterialPageRoute(builder: (context) => const AddCollectionPage());
case HomeRoute:
return MaterialPageRoute(builder: (context) => const HomePage());
}
}
So my questions are:
Why are the routes in the tabs anonymous and why are the arguments gone? Up until the HomePage everything works as expected.
Is there a better approach to hide the AppBar on navigation?
I am new to Flutter and I am trying to save the value of "counter" on first_screen when I navigate to second_screen and after that I want to save the value of "secondCounter" on second_screen when I navigate to first_screen. The "counter" and "secondCounter" value resets to 0 when I navigate between the two screens but I want to save the values of them. My code is as follows :
main.dart :-
import 'package:flutter/material.dart';
import 'package:provider_practice/screens/first_screen.dart';
import 'package:provider_practice/screens/second_screen.dart';
void main() {
runApp(MaterialApp(
home: FirstScreen(),
routes: {
"/first" : (context) => FirstScreen(),
"/second" : (context) => SecondScreen(),
},
));
}
first_screen :-
import 'package:flutter/material.dart';
class FirstScreen extends StatefulWidget {
#override
_FirstScreenState createState() => _FirstScreenState();
}
class _FirstScreenState extends State<FirstScreen> {
int counter = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("First Screen"),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("You pressed the button $counter times."),
SizedBox(height: 20),
RaisedButton(
onPressed: () {
setState(() {
counter++;
});
},
child: Text("Click Me"),
),
SizedBox(height: 20),
RaisedButton(
onPressed: () {
Navigator.pushNamed(context, "/second");
},
child: Text("Go to Second"),
),
],
),
),
),
);
}
}
second_screen.dart :-
import 'package:flutter/material.dart';
class SecondScreen extends StatefulWidget {
#override
_SecondScreenState createState() => _SecondScreenState();
}
class _SecondScreenState extends State<SecondScreen> {
int secondCounter = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Screen"),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("You pressed the button $secondCounter times."),
SizedBox(height: 20),
RaisedButton(
onPressed: () {
setState(() {
secondCounter++;
});
},
child: Text("Click Me"),
),
SizedBox(height: 20),
RaisedButton(
onPressed: () {
Navigator.pushNamed(context, "/first");
//Navigator.pop(context);
},
child: Text("Go to First"),
),
],
),
),
),
);
}
}
I am not sure if you specifically you need it to reset when the app relaunches or not but if it is fine if the value is preserved when you relaunch the app then here are a few options. Either way, you can reset the value when the app launches manually by setting it back to 0.
The first and simplest way is to use the answer in this comment. If you have both widgets in an IndexedStack (read more here) and then have the button change the stack index that would work but you would lose the benefit of page transition animations and this is a less performant option as your app grows because flutter has to run both widgets at the same time even if one isn't being used.
A second more performant way you can do this is through the Shared Preferences package. This would save it to the disk so you would need to reset it every time you launch the app if you want it to be 0 every time you open the app.
A third way is to use an external database such as Firebase. Firebase offers both their "Realtime Database" and their newer "Cloud Firestore" as well as their authentication services all for free so it might be an option you want to look into for building apps in the future. I would recommend Firestore over the real time database because it is newer and I prefer it personally. This option would also need you to reset the counter when launching the app but that shouldn't be too big of a problem.
Another way you can do this (this won't preserve state when relaunching the app) is to use the Provider Package. This package was endorsed by Flutter and is the recommended way to manage state. If you add a provider at the root of your app then it will be preserved and it can store both the first and second counter for you. Provider has a bit of a learning curve so I would recommend you look into it a bit.
Here are two videos which helped me get started with Provider:
https://youtu.be/O71rYKcxUgA
https://youtu.be/MkFjtCov62g
I'd recommend you watch them both as they are by the same person and one is an introduction to what Provider is and the other shows you how to use it. The second video has a similar example to your use case but I'd recommend you still watch both.
Hope this helps. Please let me know if this answered your question or if you need any more help or clarification please let me know.
This is easy to implement and there are a few ways you can do it.
One way is to pass it in as a parameter.
If you add the counter variable to be inside of the FirstScreen/SecondScreen widgets, you can then add them to the constructor.
Example:
class FirstScreen extends StatefulWidget {
int counter;
FirstScreen(counter);
#override
_FirstScreenState createState() => _FirstScreenState();
}
Then in your state's body you would change the text to Text("You pressed the button ${widget.counter} times.") and the setState function to setState(() {widget.counter++;});
You would do the same in the second widget making a parameter called counter or whatever you want and then make a constructor. You can also make it required or set it to have a default of 0 if it is not passed through.
Finally, to pass it through to the second widget you can just use Navigator.push(context, SecondScreen(widget.counter) and vice versa. This however, won't let you use named routes.
Another approach is to use arguments and named routes. I think this will suit your use case better.
In both of your screens where you navigate, just add an arguments parameter and pass in the counter Navigator.pushNamed(context, 'routePath', arguments: counter);. (P.S. You don't have to name the counters as firstCounter and secondCounter, they can both be called counter since they are in different widgets). Then just add to both widgets counter = ModalRoute.of(context).arguments. You also don't need to wrap your counter value in curly braces ({}). In the vide he needed the data as a map so he did that, but you just want a number. Hope this helps.
Here is a video I found which explains how to pass arguments in named routes if you find the text confusing. For context, this is a video series teaching Flutter and the app he is currently building is a world time app. Video Link.
If you are interested in the entire course here is the Video Playlist
I have a floating action button which opens the drawer. I want it to stay in its place when it opens the drawer and just change the icon to close icon with animation. I couldn't find a good solution to do this.
I tried wrapping inside my drawer with scaffold and adding a floating action button there and using hero widgets so make it look like it stays in its place but didn't help at all.
Is there anyway to achieve this?
U can use foldable Sidebar package to achieve this functionalities that's package is available on :
https://pub.dev/packages/foldable_sidebar
And check this piece of code how to use this :
child: Scaffold(
body: FoldableSidebarBuilder(
drawerBackgroundColor: Colors.deepOrange,
drawer: CustomDrawer(closeDrawer: (){
setState(() {
drawerStatus = FDBStatus.FDB_CLOSE; // For Closing the Sidebar
});
},),
screenContents: FirstScreen(), // Your Screen Widget
status: drawerStatus,
),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.deepOrange,
child: Icon(Icons.menu,color: Colors.white,),
onPressed: () {
// To Open/Close Sidebar
setState(() {
drawerStatus = drawerStatus == FDBStatus.FDB_OPEN ? FDBStatus.FDB_CLOSE : FDBStatus.FDB_OPEN;
});
}),
),
),
I have 3 screens - home, friends and profile. I want an AppBarand BottomNavigationBar to be always present in all my screens. Right now, I have the AppBar and BottomNavigationBar defined inside a Scaffold in the main.dart file. Whenever an option is tapped on BottomNavigationBar, the Scaffold body is replaced with the content of the screen of choice.
main.dart:
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
body: SafeArea(
child: Container(
child: _pageOptions[_selectedPage],
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedPage,
onTap: (int index) {
setState(() {
_selectedPage = index;
});
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text('Home')),
BottomNavigationBarItem(
icon: Icon(Icons.people), title: Text('Friends')),
BottomNavigationBarItem(
icon: Icon(Icons.person), title: Text('Profile'))
],
),
),
routes: <String, WidgetBuilder>{
'/home': (BuildContext context) => Home(),
'/friends': (BuildContext context) => Friends(),
},
);
}
Now, I have a custom avatar widget that I have reused in home screen. On clicking the avatar, I want to display the friends screen. I tried this code inside an onTap method in avatar:
{Navigator.pushNamed(context, '/friends')},
friends.dart:
Widget build(BuildContext context) {
return Container(
child: Text('Friends'),
);
}
On tapping avatar, I am getting the friends screen, but everything else is lost (AppBar, BottomNavigationBar etc).
How do I achieve this? Should I be returning a scaffold with AppBar, BottomNavigationBar etc. from all 3 of my screens? Does that negatively affect performance? Or is there a way to do this just by replacing the body of the Scaffold in main.dart?
Yes you need to return a scaffold with an appbar in all the three screens. You can put the appbar in a separate statelesswidget class and use it in all the three scaffolds. Same for bottom navigation bar. You can try to keep the returning widget in build method 'const'. Will not be possible for appbar since title will be varying. It won't get rebuilt so no worries on performance.
How do I create a fullscreenDialog that covers my bottomnavigationbar?
My mainscreen looks like this, where I have a bottomnavigationbar which navigates to three different screens.
#override
Widget build(BuildContext context) {
return new Scaffold(
body: PageView(
children: [new HomeTab(), new PresentationsTab(), new TestTab()],
controller: _pageController,
onPageChanged: pageChanged,
),
bottomNavigationBar: new BottomNavigationBar(
currentIndex: _page,
onTap: tapBottomNav,
items: [
new BottomNavigationBarItem(
icon: new Icon(Icons.home),
title: new Text('Home'),
),
new BottomNavigationBarItem(
icon: new Icon(Icons.pregnant_woman),
title: new Text('Presentation'),
),
new BottomNavigationBarItem(
icon: new Icon(Icons.pregnant_woman),
title: new Text('Presentation'),
)
],
),
);
}
And somewhere I have a screen which navigates to another screen with the fullscreenDialog flag set to true like this.
Navigator.push(
context,
new MaterialPageRoute(
builder: (BuildContext context) => new AddAudio(),
fullscreenDialog: true,
),
);
On my appbar I can see that the flag actually works because my backbutton arrow will become an x, but my bottomnavigationbar will still be visible, how do I resolve this?
I have an answer for you on my post: Flutter MaterialPageRoute as fullscreenDialog appears underneath BottomNavigationBar
But, to recap for you:
I assume you are presenting your modal from a page which is itself a Scaffold? In which case you have nested Scaffold objects, and this is where the problem lies. To ensure your modal appears above the BottomNavigationBar you need to include that widget in the child Scaffold and not in the root one.
This seems like an utter pain in the arse, and not a great solution with loads of boilerplate and code duplication; but it isn't. You just need to build a custom composition widget for BottomNavigationBar and then use ChangeNotifier with a state class to manage state. I've explained it in the post I reference above, and it is probably better to read the answer there so you understand the context.
That is probably the expected behavior, you should push a PageRoute (with a PageRouteBuilder) and use a custom transition to have the bottom to top slide animation.