Change AppBar content dynamically on Navigation through App? - flutter

I've got my Navigation in my App working, but I would like to change the AppBar Title dynamically, depending on which page is currently displayed.
Main Problem is, I'm using a TabBarView, to show the main pages, but I can also get to some sub pages. That part is now working fine, but I can't figure out, how I could make the AppBar change depending on the displayed page / sub-page
Here is the main class, which holds the AppBar:
class AppView extends StatefulWidget {
const AppView({Key? key}) : super(key: key);
#override
State<AppView> createState() => _AppViewState();
}
class _AppViewState extends State<AppView> with TickerProviderStateMixin{
late TabController _controller;
#override
void initState(){
super.initState();
_controller = TabController(length: 6, vsync: this);
_controller.addListener(() {
if(navKey.currentState!.canPop()){
navKey.currentState!.pop();
}
});
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
if(navKey.currentState!.canPop()) {
navKey.currentState!.pop();
}
return false;
},
child: Scaffold(
appBar: AppBar(
title: HeadLine(
"Test",
color: white,
),
),
body: Navigator(
key: navKey,
onGenerateRoute: (_) => MaterialPageRoute(
builder: (_) => TabBarView(
controller: _controller,
physics: NeverScrollableScrollPhysics(),
children: [
HomeScreen(),
ChatOverviewScreen(),
ForumOverviewScreen(),
CalendarScreen(),
GroupScreen(),
ProfileScreen()
],
)
),
),
bottomNavigationBar: BottomBar(
controller: _controller,
)),
);
}
}
For Testing, theres currently just a button which calls a sub page on one of these pages:
class ChatOverviewScreen extends StatelessWidget {
const ChatOverviewScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: PrimaryButton(
text: "Do Stuff",
onPressed: () {
navKey.currentState!.push(ChatDetailScreen.route());
},
),
);
}
}
navKey is set as global variable in my main file via
final GlobalKey<NavigatorState> navKey = GlobalKey<NavigatorState>();
Basicaly I want to set the Title of the AppBar depending on which page / subpage is shown and when it is a sub page, there should also be an arrow to get back.

You should do something like this:
Widget _buildAppBarTitle(int index) {
String title = '';
switch (index) {
case 0:
title = 'YOUR TITLE';
break;
... other case...
}
return Text(title);
}
Scaffold(
appBar: AppBar(
title: _buildAppBarTitle(_currentIndex),
),
body: Navigator(
...
),
bottomNavigationBar: BottomNavigationBar(
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
currentIndex: _currentIndex,
items: [...],
),
)
You can use enum to identify the pages, also, you can use some state manager.
Flutter State management

Related

Why doesn't BottomNavigatorBarItem currentIndex get updated?

I am new to flutter and I did find similar questions on SO but am too novice to understand the nuance so apologies if this question is too similar to an already asked one.
I have a BottomNavigationBar which has 3 icons (Home, Play and Create) which should navigate between these 3 routes/pages.
main.dart
routes: {
"/home": (context) => MyHomePage(title: "STFU"),
"/play": (context) => Play(),
"/create": (context) => Create(),
"/settings": (context) => Settings(),
},
I extracted my navbar into a custom class so my 3 separate pages could use it:
bottom-nav.dart
class MyBottomNavBar extends StatefulWidget {
MyBottomNavBar({
Key? key,
}) : super(key: key);
#override
State<MyBottomNavBar> createState() => _MyBottomNavBarState();
}
class _MyBottomNavBarState extends State<MyBottomNavBar> {
int _selectedIndex = 0;
void _onTapped(int index) => {
print("_onTapped called with index = $index"),
setState(
() => _selectedIndex = index,
)
};
#override
Widget build(BuildContext context) {
return BottomNavigationBar(
backgroundColor: Colors.orangeAccent[100],
currentIndex: _selectedIndex,
onTap: (value) => {
print("value is $value"),
// find index and push that
_onTapped(value),
if (value == 0)
{Navigator.pushNamed(context, "/home")}
else if (value == 1)
{Navigator.pushNamed(context, "/play")}
else if (value == 2)
{Navigator.pushNamed(context, "/create")}
},
items: [
BottomNavigationBarItem(label: "Home", icon: Icon(Icons.home_filled)),
BottomNavigationBarItem(
label: "Play", icon: Icon(Icons.play_arrow_rounded)),
BottomNavigationBarItem(label: "Create", icon: Icon(Icons.create)),
],
);
}
}
so now i just set this MyBottomNavBar class to the bottomNavigationBar property of the Scaffold widget my inside Home page, Play page and Create page for eg.
home.dart
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Home"),
),
body: Column(
children: [
Container(
padding: EdgeInsets.all(20.00),
child: Text("inside home"),
),
],
),
bottomNavigationBar: MyBottomNavBar(),
);
}
}
play.dart
class Play extends StatefulWidget {
const Play({super.key});
#override
State<Play> createState() => _PlayState();
}
class _PlayState extends State<Play> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text("inside play page"),
SizedBox(
height: 30.00,
),
Text("some text"),
],
),
),
bottomNavigationBar: MyBottomNavBar(),
);
}
}
The nav bar buttons work to switch between pages but for some reason the currentIndex value isn't getting updated and stays at 0 (i.e on the "Home" icon). When I debug it I can see _selectedIndex getting updated inside inside the _onTapped function which should update the currentIndex value but it doesn't appear to do so. Any help would be appreciated
What you have here is three different pages with their separate BottomNavBar class instance. Instead you should have a shared Scaffold and one bottomNavBar so that when you navigate bottomNavbar state does not reset.
You can use PageView to do this.
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
void initState() {
super.initState();
}
List<Widget> pages = const [Home(), Play(), Create()];
final _pageController = PageController();
int _selectedIndex = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) {
_pageController.jumpToPage(index);
_selectedIndex = index;
setState(() {});
},
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.play_arrow), label: 'Play'),
BottomNavigationBarItem(icon: Icon(Icons.create), label: 'Create'),
],
backgroundColor: Colors.greenAccent,
),
body: PageView(
controller: _pageController,
children: pages,
),
);
}
}
class Home extends StatelessWidget {
const Home({super.key});
#override
Widget build(BuildContext context) {
return const Placeholder();
}
}
class Play extends StatelessWidget {
const Play({super.key});
#override
Widget build(BuildContext context) {
return const Placeholder();
}
}
class Create extends StatelessWidget {
const Create({super.key});
#override
Widget build(BuildContext context) {
return const Placeholder();
}
}

How to show navigation bar on page which doesn't belong to this bar?

edit: SOLUTION
i used package custom_navigator
In navigation bar I have 2 pages to redirect, but I want to navigate to third page and still want to see navigation bar (this one with 2 pages) there.
Is it possible to do? Do I have to make my own navigation bar for this page?
class Bar extends StatefulWidget {
#override
BarState createState() => BarState();
}
class BarState extends State<Bar> {
int tabIndex = 0;
List<Widget> pages = [
FirstPage(),
SecondPage(),
];
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(kToolbarHeight),
child: SafeArea(
child: BottomNavigationBar(
iconSize: 25,
elevation: 4.0,
items: <BottomNavigationBarItem>[
barItem(Icons.message),
barItem(Icons.camera_enhance),
barItem(Icons.person),
],
currentIndex: tabIndex,
onTap: (int index) {
setState(() {
tabIndex = index;
});
},
),
)),
body: Container(
child: pages.elementAt(tabIndex),
),
);
}
}
this is what i try:
List<Widget> pages = [
Container(
color: Colors.green,
child: Center(
child: ElevatedButton(
onPressed: state
),
),
),
SecondPage(),
ThirdPage()
];
state() {
tabIndex = 2;
setState(() {
});
}
The simplest way, if you don't mind it animating would be to init an AppBar in your navigator and pass it to the pages and they would use it in there scaffold.
class MyFlow extends StatefulWidget {
const MyFlow({Key? key}) : super(key: key);
#override
_MyFlowState createState() => _MyFlowState();
}
class _MyFlowState extends State<MyFlow> {
#override
Widget build(BuildContext context) {
final appBar = AppBar();
return Navigator(
onPopPage: (route, result) => true,
pages: [
MaterialPage(child: PageOne(appBar: appBar)),
MaterialPage(child: PageTwo(appBar: appBar)),
MaterialPage(child: PageThree(appBar: appBar)),
],
);
}
}
class PageOne extends StatelessWidget {
const PageOne({Key? key, required this.appBar}) : super(key: key);
final AppBar appBar;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: appBar,
);
}
}

Flutter: How to reset TabController index upon bottom navigation

I have a Flutter app with a Cupertino bottom navigation bar. The first page has tabbed views like this.
What I'm trying to achieve
Upon navigating away from that first page (or when tapping on any of the bottom navigation items/icons), I want the index of the tab controller on that first page to reset to 0 so that when I return to that first page, I see the initial tab by default (i.e. the car tab). The current default behaviour is that it will display whichever tab I left the page on.
How do I achieve the above? I've pasted in sample code below to somewhat replicate my scenario. I created the _resetTabIndex function that calls tabController.previousIndex and then tried to call that function whenever user navigates away from the page, but I couldn't get that to work. Thanks in advance for any help with this!
(NOTE: I have to stick with Cupertino bottom navigation because of other requirements in the real app)
main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'page1.dart';
void main() {
runApp(TabBarDemo());
}
class TabBarDemo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
enum TabItem { page1, page2 }
class TabItemData {
const TabItemData({#required this.title, #required this.icon});
final String title;
final IconData icon;
static const Map<TabItem, TabItemData> allTabs = {
TabItem.page1: TabItemData(title: 'Page 1', icon: Icons.shopping_cart_outlined),
TabItem.page2: TabItemData(title: 'Page 2', icon: Icons.person_outline_rounded),
};
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
TabItem _currentTab = TabItem.page1;
Map<TabItem, WidgetBuilder> get widgetBuilders {
return {
TabItem.page1: (_) => Page1(),
TabItem.page2: (_) => Scaffold(
appBar: AppBar(
title: Center(child: Text('Page 2')),
),
body: Center(child: Text('Page 2'))),
};
}
void _resetTabIndex(TabItem tabItem) {
setState(() => _currentTab = tabItem); // Ignore this. Set up for a behaviour in the complete app.
// How can I amend this function to trigger the resetTabIndex method in page1.dart (which one alternative I thought might work)?
}
#override
Widget build(BuildContext context) {
return CupertinoHomeScaffold(
currentTab: _currentTab,
onSelectTab: _resetTabIndex,
widgetBuilders: widgetBuilders,
);
}
}
class CupertinoHomeScaffold extends StatelessWidget {
CupertinoHomeScaffold({
Key key,
#required this.currentTab,
#required this.onSelectTab,
#required this.widgetBuilders,
}) : super(key: key);
final TabItem currentTab;
final ValueChanged<TabItem> onSelectTab;
final Map<TabItem, WidgetBuilder> widgetBuilders;
#override
Widget build(BuildContext context) {
return CupertinoTabScaffold(
tabBar: CupertinoTabBar(
items: [
_buildItem(TabItem.page1),
_buildItem(TabItem.page2),
],
onTap: (index) => onSelectTab(TabItem.values[index]),
),
tabBuilder: (context, index) {
final item = TabItem.values[index];
return CupertinoTabView(
builder: (context) => widgetBuilders[item](context),
);
},
);
}
BottomNavigationBarItem _buildItem(TabItem tabItem) {
final itemData = TabItemData.allTabs[tabItem];
final color = currentTab == tabItem ? Colors.indigo : Colors.grey;
return BottomNavigationBarItem(
icon: Icon(itemData.icon, color: color),
title: Text(
itemData.title,
style: TextStyle(color: color),
),
);
}
}
page1.dart
import 'package:flutter/material.dart';
class Page1 extends StatefulWidget {
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1> with SingleTickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: 2);
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
resetTabIndex() {
setState(() {
_tabController.previousIndex;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: _tabController,
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.directions_bike)),
],
),
title: Center(child: Text('Page 1')),
),
body: TabBarView(
controller: _tabController,
children: [
Icon(Icons.directions_car),
Icon(Icons.directions_bike),
],
),
);
}
}
When navigating away from that first page do this
_tabController.animateTo(0,duration: Duration(milliseconds: 200),curve:Curves.easeIn);

Screen navigation in flutter

Update:
The app has two stateful widget screens: Home, and Search. Both screens have search boxes and a bottom navigation.
The problem that needs to be solved is when a user taps the search box at the top of the home screen, the app should take them to the search screen without hiding the bottom navigation (just like what the eBay app does).
I have tried calling the Search class when the user taps the search box on the Home screen. And this approach works. However, the new screen hides the navigation bar at the bottom.
The following code handles the navigation between screens.
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
int _currentIndex = 0;
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Navigator(
key: _navigatorKey,
onGenerateRoute: (RouteSettings settings) {
switch (settings.name) {
case 'Search':
return MaterialPageRoute(builder: (context) => Search());
default:
return MaterialPageRoute(builder: (context) => UserHome());
}
}),
bottomNavigationBar: BottomNavigationBar(
onTap: _onTap,
items: [
BottomNavigationBarItem(icon: Icon(Icons.home),title: Text('Home'))
BottomNavigationBarItem(icon: Icon(Icons.search), title: Text('Search'))
],
),
);
}
void _onTap(int tappedIndex) {
setState(() => _currentIndex = tappedIndex);
switch (tappedIndex) {
case 0:
_navigatorKey.currentState.pushReplacementNamed('Home');
break;
case 1:
_navigatorKey.currentState.pushReplacementNamed('Search');
break;
}
}
}
If you are trying to do this for automated testing. You can do so using widget testing. Widget tests in flutter can simulate button taps and check for the expected output
class TestPage extends StatefulWidget {
#override
_TestPageState createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
bool home;
#override
void initState() {
super.initState();
home = true;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: home ? UserHome() : Search(),
bottomNavigationBar: BottomNavigationBar(
onTap: _onTap,
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Home')),
BottomNavigationBarItem(
icon: Icon(Icons.search), title: Text('Search'))
],
),
);
}
void _onTap(int tappedIndex) {
setState(() {
if (tappedIndex == 0) {
home = true;
} else {
home = false;
}
});
}
}
class UserHome extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Center(
child: Container(
color: Colors.yellow,
child: Text('USER HOME'),
),
)
],
);
}
}
class Search extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Center(
child: Container(
color: Colors.green,
child: Text('SEARCH'),
),
)
],
);
}
}
I know this is not exactly very similar to your initial solution but it achieves the behavior you intend.

BottomNavigationBar page change causing StreamBuilder data reload

Inside BottomNavigation I have 2 page. 1st Page loading data from the network and showing in SliverList and In another page showing static list data.
After moving from 1st to 2nd page then 1st page all network data are gone. I have used PageStorageKey but still, it's not working. but the 2nd page never reloaded.
Why is 1st page not saving its state which has StreamBuilder?
My code:
Bottom Navigation Page:
class MainActivity extends StatefulWidget {
#override
_MainActivityState createState() => _MainActivityState();
}
class _MainActivityState extends State<MainActivity> {
final Key keyHome = PageStorageKey('pageHome');
final Key keyChat = PageStorageKey('pageChat');
int currentTab = 0;
HomePage home;
Chat chat;
List<Widget> pages;
Widget currentPage;
final PageStorageBucket bucket = PageStorageBucket();
#override
void initState(){
home = HomePage(
key: keyHome,
);
chat = Chat(
key: keyChat,
);
pages = [home, chat];
currentPage = home;
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageStorage(
child: currentPage,
bucket: bucket,),
//Bottom Navigation Bar added
bottomNavigationBar: BottomNavigationBar(
currentIndex: currentTab,
onTap: (int index){
setState(() {
currentTab = index;
currentPage = pages[index];
});
},
type: BottomNavigationBarType.fixed,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Home')),
BottomNavigationBarItem(icon: Icon(Icons.chat), title: Text('Chat')),
BottomNavigationBarItem(
icon: Icon(Icons.payment), title: Text('Pay')),
BottomNavigationBarItem(
icon: Icon(Icons.account_circle), title: Text('Me')),
],
//currentIndex: _selectedIndex,
fixedColor: Colors.deepPurple,
),
);
}
}
Home Page:
class HomePage extends StatefulWidget {
HomePage({
Key key,
}) : super(key: key);
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>{
#override
Widget build(BuildContext context) {
final NewsCatalogBlog newsBloc = BlocProvider.of<NewsCatalogBlog>(context);
//bloc.fetchAllNews();
return Scaffold(
appBar: PreferredSize(child: HomePageGradientAppBar(),
preferredSize: const Size.fromHeight(100.0),),
body: StreamBuilder(
stream: newsBloc.outNewsList,
builder: (context, AsyncSnapshot<List<Data>> snapshot) {
Widget newsList;
newsList = new SliverList(
delegate: new SliverChildBuilderDelegate((context,index){
print("$index");
return NewsListRow(snapshot.data, newsBloc, index);
},
childCount: (snapshot.data == null ? 0 : snapshot.data.length) + 30),
);
return new CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(child: TabPanel(),),
SliverToBoxAdapter(child: UrlButtonPanel(),),
SliverToBoxAdapter(child: ChatNowAd(),),
newsList,
],
);
},
),
);
}
}
Chat Page:
class Chat extends StatelessWidget {
Chat({
Key key,
}): super (key: key);
#override
Widget build(BuildContext context) {
return ListView.builder(
itemExtent: 250.0,
itemCount: 20,
itemBuilder: (context, index) => Container(
padding: EdgeInsets.all(10.0),
child: Material(
elevation: 4.0,
borderRadius: BorderRadius.circular(5.0),
color: index % 2 == 0 ? Colors.cyan : Colors.deepOrange,
child: Center(
child: Text('Mir{$index}'),
),
),
),
);
}
}
Better way is to use IndexedStack instead of PageStorage or AutomaticKeepAliveClientMixin.
class _MainActivityState extends State<MainActivity> {
int _selectedPage = 0;
List<Widget> pageList = List<Widget>();
#override
void initState() {
pageList.add(HomePage());
pageList.add(ChatPage());
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: IndexedStack(
index: _selectedPage,
children: pageList,
),
//Bottom Navigation Bar added
bottomNavigationBar: BottomNavigationBar(
.....
**IndexedStack Widget is sub-class of Stack Widget
It shows single child from list of provided Childs.
Its size as big as largest child.
It keep state of all Childs.**