arrow-left arrow-right brightness-2 chevron-left chevron-right facebook-box facebook loader magnify menu-down rss-box star twitter-box youtube-box twitter white-balance-sunny window-close
How to Create a BottomNavigationBar with Flutter
3 min read

How to Create a BottomNavigationBar with Flutter

How to Create a BottomNavigationBar with Flutter

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 StatelessWidgets 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