In this article we'll be creating a BottomNavigationBar
with the ability to switch between different tabs using IndexedStack. You'll want to use this when creating tab based user interface(s) with their own navigation stack.
Our example application will be simple. It'll consist of four screens total, three of which are "main" tab pages, the final one being a detail page that we push on the navigation stack.
New Project
Let's create a new Flutter project in the terminal:
# New Flutter project
$ flutter create flutter_shopping
# Open in editor
$ cd flutter_tabs && code .
Page Creation
We can start off by creating our main pages that will be displayed in our tab system. These will be simple StatelessWidget
s and the content doesn't matter for our tutorial.
/// lib/presentation/shop/pages/shop_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_shopping/presentation/product_detail/pages/product_detail_page.dart';
class ShopPage extends StatelessWidget {
static Route<dynamic> route() => MaterialPageRoute(
builder: (context) => ShopPage(),
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Shop"),
),
body: Center(
child: FlatButton(
onPressed: () => Navigator.of(context).push(
ProductDetailPage.route(),
),
child: Text("Navigate to Product Detail Page"),
),
),
);
}
}
/// lib/presentation/home/pages/home_page.dart
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
static Route<dynamic> route() => MaterialPageRoute(
builder: (context) => HomePage(),
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Home"),
),
body: Center(
child: Text("Hello, Home!"),
),
);
}
}
/// lib/presentation/search/pages/search_page.dart
import 'package:flutter/material.dart';
class SearchPage extends StatelessWidget {
static Route<dynamic> route() => MaterialPageRoute(
builder: (context) => SearchPage(),
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Search"),
),
body: Center(
child: Text("Hello, Search!"),
),
);
}
}
/// lib/presentation/product_detail/pages/product_detail_page.dart
import 'package:flutter/material.dart';
class ProductDetailPage extends StatelessWidget {
static Route<dynamic> route() => MaterialPageRoute(
builder: (context) => ProductDetailPage(),
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Product Detail"),
),
body: Center(
child: Text("Hello, Product!"),
),
);
}
}
TabPage and BottomNavigationBar
Now that we've got the ability to show various pages (i.e. HomePage
, SearchPage
, and so on), we'll create a TabPage
. This page will use IndexedStack
to display a different body depending on the current tab selected by the user.
In order to make this easier to generate, let's create a TabNavigationItem
to hold information about our tab:
/// lib/presentation/tabs/models/tab_navigation_item.dart
import 'package:flutter/widgets.dart';
class TabNavigationItem {
final Widget page;
final Widget title;
final Icon icon;
TabNavigationItem({
@required this.page,
@required this.title,
@required this.icon,
});
static List<TabNavigationItem> get items => [
TabNavigationItem(
page: HomePage(),
icon: Icon(Icons.home),
title: Text("Home"),
),
TabNavigationItem(
page: ShopPage(),
icon: Icon(Icons.shopping_basket),
title: Text("Shop"),
),
TabNavigationItem(
page: SearchPage(),
icon: Icon(Icons.search),
title: Text("Search"),
),
];
}
We can then use this and our other pages to create a TabsPage
which will use our aforementioned IndexedStack
with a _currentIndex
that changes whenever a user taps
a tab.
import 'package:flutter/material.dart';
import 'package:flutter_shopping/presentation/home/pages/home_page.dart';
import 'package:flutter_shopping/presentation/search/pages/search_page.dart';
import 'package:flutter_shopping/presentation/shop/pages/shop_page.dart';
import 'package:flutter_shopping/presentation/tabs/models/tab_navigation_item.dart';
class TabsPage extends StatefulWidget {
@override
_TabsPageState createState() => _TabsPageState();
}
class _TabsPageState extends State<TabsPage> {
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: [
for (final tabItem in TabNavigationItem.items) tabItem.page,
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (int index) => setState(() => _currentIndex = index),
items: [
for (final tabItem in TabNavigationItem.items)
BottomNavigationBarItem(
icon: tabItem.icon,
title: tabItem.title,
)
],
),
);
}
}
How does IndexedStack work?
Given a list of Widgets (i.e. the children
of HomePage
, ShopPage
and SearchPage
), it'll display the Widget where the children
matches the currentIndex
.
For example, if our currentIndex
was 1
and our children
array looked like:
children: [
HomePage()
ShopPage()
SearchPage()
]
Our IndexedStack
would display the ShopPage
.
We'll now be able to update our main.dart
to place TabsPage
as our home
entry:
import 'package:flutter/material.dart';
import 'package:flutter_shopping/presentation/tabs/pages/tabs_page.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App',
theme: ThemeData(
primarySwatch: Colors.green,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: TabsPage(),
debugShowCheckedModeBanner: false,
);
}
}
This gives us the following result:
Summary
We've now used the IndexedStack
to switch our displayed widget depending on the item selected by the user!
Code for this article: https://github.com/PaulHalliday/flutter_indexed_stack_tab_view
Top comments (1)
I've been teaching myself Flutter and have struggled to wrap my head around getting the bottom navigation bar to work. This was crazy clear and simple. Thank you!