Flutter: How to reset TabController index upon bottom navigation - flutter

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);

Related

Change AppBar content dynamically on Navigation through App?

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

Skip a tab while switching among tabs in TabBarView

In my flutter app, I use a simple tab-bar. I used the code from the flutter website and updated to make sure that I can keep the state of each tab using AutomaticKeepAliveClientMixin.
I have 3 tabs and each tab is fetching a list of data (why I need to use AutomaticKeepAliveClientMixin) from my backend API.
The problem is that when I switch between first and 3rd tabs (Page1 and Page3), the middle tab keeps rebuilding over and over again until I switch to that tab (Page2) and only at that point it doesn't get rebuilt anymore.
Every rebuild results in fetching data from API and that's not desirable.
Below, i have included a simplified code to reproduce this issue.
You can see in the debug console once switching between 1st and 3rd tab (without switching to 2nd tab) that it keeps printing "p2" (in my real app, it keeps fetching data for the 2nd tab).
Is there a way to switch between tabs without other tabs in between being built/rebuilt?
This is my code.
import 'package:flutter/material.dart';
void main() {
runApp(TabBarDemo());
}
class TabBarDemo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.directions_transit)),
Tab(icon: Icon(Icons.directions_bike)),
],
),
title: Text('Tabs Demo'),
),
body: TabBarView(
children: [
Page1(),
Page2(),
Page3(),
],
),
),
),
);
}
}
class Page1 extends StatefulWidget {
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1>
with AutomaticKeepAliveClientMixin<Page1> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('p1');
return Container(
child: Center(
child: Icon(Icons.directions_car),
),
);
}
}
class Page2 extends StatefulWidget {
#override
_Page2State createState() => _Page2State();
}
class _Page2State extends State<Page2>
with AutomaticKeepAliveClientMixin<Page2> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('p2');
return Container(
child: Center(
child: Icon(Icons.directions_transit),
),
);
}
}
class Page3 extends StatefulWidget {
#override
_Page3State createState() => _Page3State();
}
class _Page3State extends State<Page3>
with AutomaticKeepAliveClientMixin<Page3> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('p3');
return Container(
child: Center(
child: Icon(Icons.directions_bike),
),
);
}
}
I believe this isn't a bug with flutter, but ultimately comes down to your implementation.
Please take a look at the code I wrote for you.
import 'package:flutter/material.dart';
import 'dart:async';
class FakeApi {
Future<List<int>> call() async {
print('calling api');
await Future.delayed(const Duration(seconds: 3));
return <int>[for (var i = 0; i < 100; ++i) i];
}
}
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp() : super(key: const Key('MyApp'));
#override
Widget build(BuildContext context) => const MaterialApp(home: MyHomePage());
}
class MyHomePage extends StatelessWidget {
const MyHomePage() : super(key: const Key('MyHomePage'));
static const _icons = [
Icon(Icons.directions_car),
Icon(Icons.directions_transit),
Icon(Icons.directions_bike),
];
#override
Widget build(BuildContext context) => DefaultTabController(
length: _icons.length,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [for (final icon in _icons) Tab(icon: icon)],
),
title: Text('Tabs Demo'),
),
body: TabBarView(
children: [
Center(child: _icons[0]),
StaggeredWidget(_icons[1]),
Center(child: _icons[2]),
],
),
),
);
}
class StaggeredWidget extends StatefulWidget {
const StaggeredWidget(this.icon)
: super(key: const ValueKey('StaggeredWidget'));
final Icon icon;
#override
_StaggeredWidgetState createState() => _StaggeredWidgetState();
}
class _StaggeredWidgetState extends State<StaggeredWidget> {
Widget _child;
Timer _timer;
#override
void initState() {
super.initState();
_timer = Timer(const Duration(milliseconds: 150), () {
if (mounted) {
setState(() => _child = MyApiWidget(widget.icon));
}
});
}
#override
void dispose() {
_timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) => _child ?? widget.icon;
}
class MyApiWidget extends StatefulWidget {
const MyApiWidget(this.icon, [Key key]) : super(key: key);
final Icon icon;
#override
_MyApiWidgetState createState() => _MyApiWidgetState();
}
class _MyApiWidgetState extends State<MyApiWidget>
with AutomaticKeepAliveClientMixin {
final _api = FakeApi();
#override
Widget build(BuildContext context) {
print('building `MyApiWidget`');
super.build(context);
return FutureBuilder<List<int>>(
future: _api(),
builder: (context, snapshot) => !snapshot.hasData
? const Center(child: CircularProgressIndicator())
: snapshot.hasError
? const Center(child: Icon(Icons.error))
: ListView.builder(
itemBuilder: (context, index) => ListTile(
title: Text('item $index'),
),
),
);
}
#override
bool get wantKeepAlive => true;
}

how to stop navigator to reload view?

I'm new to flutter and have a question about navigator.
I have 2 views one called Home and List. I created a drawer that is persistent in these two views. In each view I'm creating a reference to Firebase using FutureBuilder. The problem I'm running into is that every time I go to either Home or List initState is being called again. I believe the problem comes from selecting the page from the drawer. My question How can I still move to different pages without having to called InitState everytime I change screens.
title: Text('Go to page 1'),
onTap: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => Listdb()));
This is where I think the screen rebuilds itself. Is there a way to avoid rebuilding?
Thank you for your help!
You can use the AutomaticKeepAliveClientMixin to prevent reloading everytime you change page, combining with PageView for better navigation. I'll included an example here:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final PageController _pageController = PageController();
Widget build(BuildContext context) {
return Scaffold(
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
DrawerHeader(
child: Text('Drawer Header'),
decoration: BoxDecoration(
color: Colors.blue,
),
),
ListTile(
title: Text('Item 1'),
onTap: () {
_pageController.jumpToPage(0);
Navigator.pop(context);
},
),
ListTile(
title: Text('Item 2'),
onTap: () {
_pageController.jumpToPage(1);
Navigator.pop(context);
},
),
],
),
),
body: PageView(
controller: _pageController,
children: <Widget>[
PageOne(),
PageTwo(),
],
),
);
}
}
class PageOne extends StatefulWidget {
#override
_PageOneState createState() => _PageOneState();
}
class _PageOneState extends State<PageOne> with AutomaticKeepAliveClientMixin {
#override
void initState() {
print("From PageOne - This will only print once");
super.initState();
}
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
backgroundColor: Colors.red,
);
}
}
class PageTwo extends StatefulWidget {
#override
_PageTwoState createState() => _PageTwoState();
}
class _PageTwoState extends State<PageTwo> with AutomaticKeepAliveClientMixin {
#override
void initState() {
print("From PageTwo - This will only print once");
super.initState();
}
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
backgroundColor: Colors.blue,
);
}
}

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.

Flutter: How to pass data from the parent stateful widget to one of the tabs in the BottomNavigationBar?

import 'package:flutter/material.dart';
class HomeScreen extends StatefulWidget {
HomeScreen();
#override
_HomeScreenState createState() => new _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _currentIndex = 0;
final List<Widget> _children = [
MapsScreen(),
HistoryScreen(),
];
#override
void initState() {
super.initState();
RestAPI.loadMapsFromNetwork();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home screen'),
),
body: _children[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
onTap: onTabTapped, // new
currentIndex: _currentIndex,
items: [
BottomNavigationBarItem(
icon: new Icon(Icons.map),
title: new Text('Maps'),
),
BottomNavigationBarItem(
icon: new Icon(Icons.change_history),
title: new Text('History'),
)
],
),
);
}
void onTabTapped(int index) {
setState(() {
_currentIndex = index;
});
}
}
This home.dart makes a network call in initState method.
How do I pass the list of maps that the client received from the network to one of the tabs like MapsScreen? Do I need to use ScopedModel or InheritedWidget or is there a better approach? All the logic to render is within the MapsScreen class.
You can pass the value from the json response like this.
class MapScreen extends StatefulWidget {
Map<List<String,dynamic>> data ;
MapScreen({this.data}) ;
_MapScreenState createState() => _MapScreenState() ;
}
class _MapScreenState extends State<MapScreen> {
#override
Widget build(BuildContext context) {
return Container(
child: ListView(
/* use the data over here */
),
);
}
}