What will we achieve in this tutorial

In this tutorial we'll create and customize a bottom navigation bar and maintain the scrolling position when switch back to previous screen or page. here is an image for what we'll achieve at the end of this tutorial.

fluttur_bottom_tabbar

Create a new Flutter project

In this tutorial I'm gonna use VScode to generate a new flutter project, you can use whatever you want. if you have been creating the fresh project now let's create a bunch of screens that we will need in this tutorial. so go to the lib folder and create those screens:

lib/
    - home.dart
    - search.dart
    - notifications.dart
    - profile.dart
    - settings.dart
    - nav_screen.dart
    - main.dart

the main.dart file is created by default when generate a new flutter project. the files in the lib should look like this : vscode-flutter-project

Now let's clean the main.dart and it should look like this:

import 'package:flutter/material.dart';
import 'package:flutter_learning/nav_screen.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter BottomNavBar',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: NavScreen(),
    );
  }
}

You may notice that the home page page is our NavScreen() you should import this class in order to use it, this NavScreen() should be a StatefulWidget so it should look like this for now, later we will add our necessary code:

import 'package:flutter/material.dart';

class NavScreen extends StatefulWidget {
  @override
  _NavScreenState createState() => _NavScreenState();
}

class _NavScreenState extends State<NavScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold();
  }
}

For the other pages they should only be a StatelessWidget I'm gonna name them like this: HomeScreen, NotificationsScreen, ProfileScreen, SearchScreen.

here is how my home.dart looks like for now you can design and customize just like how you want.

import 'package:flutter/material.dart';

class SearchScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(child: Text('Search Screen'));
  }
}

Create a bottom navigation bar in flutter

Now it's time to open our nav_screen.dart again and work on our bottom navigation bar.

create an array of screens of type widget

The first thing we're gonna do is to write an array of screens that we want to appear in the navigation bar.

  final List<Widget> _screens = [
    HomeScreen(),
    SearchScreen(),
    ProfileScreen(),
    NotificationsScreen(),
    SettingsScreen(),
  ];

create an array of icons of type IconData

Now let's create icons to attach for our screens, each screen has it's own icon:

  final List<IconData> _icons = [
    Icons.home,
    Icons.search,
    Icons.person,
    Icons.notifications,
    Icons.settings,
  ];

Create a variable _selectedIndex of type int initialized with 0

Now let's create a variable of type int initialized with zero let's name it _selectedIndex, this variable will help us to know which tab is taped and selected so that we'll know which screen to show.

int _selectedIndex = 0;

wrap the scaffold widget with DefaultTabController

No let's wrap the Scaffold() widget with DefaultTabController() which requires length attribute we'll provide our screens length just like bellow:

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: _screens.length,
      child: Scaffold(),
    );
  }

Customize the bottomNavigationBar

We know that the scaffold() has the bottomNavigationBar let's give our custom widget that we will create and name it CustomTabBarWidget this widget will take in it's constructor those properties: icons, selectedIndex, onTap

Scaffold(
        bottomNavigationBar: CustomTabBarWidget(
          icons: _icons,
          selectedIndex: _selectedIndex,
          onTap: (index) => setState(() => {_selectedIndex = index}),
        ),
      ),

Create the custom widget CustomTabBarWidget

Now in the lib folder let's create a new folder and name it widgets inside this folder let's create a our custom_tabbar.dart inside this file let's create a stateLess widget and name it CustomTabBarWidget and let's define 3 properties:

  final List<IconData> icons;
  final int selectedIndex;
  final Function(int) onTap;

let's create a constructor for those 3 properties which all be required:

  const CustomTabBarWidget({
    Key key,
    @required this.icons,
    @required this.selectedIndex,
    @required this.onTap,
  }) : super(key: key);

Now in the build method let's return a TabBar which is a material flutter predefined widget that took a bunch of properties in it's constructor like indicator, indicatorPadding, tabs, onTap...

tabs is a list of widgets, we are interested here in our icons we want to display the icons, so let's iterate around our list of icons that are provider in the constructor, we want to change the color of the selected icon to be blue that's why we'll use MapEntry to check against index of the icon and the actual selectedIndex here is how we'll do it:

tabs: icons
          .asMap()
          .map((i, e) => MapEntry(
                i,
                Tab(
                  icon: Icon(
                    e,
                    color: i == selectedIndex ? Colors.blue : Colors.black45,
                    size: 30,
                  ),
                ),
              ))
          .values
          .toList(),

Here is the final code of the custom_tabbar.dart file:

import 'package:facebook_ui/config/palette.dart';
import 'package:flutter/material.dart';

class CustomTabBarWidget extends StatelessWidget {
  final List<IconData> icons;
  final int selectedIndex;
  final Function(int) onTap;

  const CustomTabBarWidget({
    Key key,
    this.icons,
    this.selectedIndex,
    this.onTap,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return TabBar(
      indicatorPadding: const EdgeInsets.all(0),
      indicator: BoxDecoration(
        border: Border(
          top: BorderSide(
            color: Palette.facebookBlue,
            width: 3,
          ),
        ),
      ),
      tabs: icons
          .asMap()
          .map((i, e) => MapEntry(
                i,
                Tab(
                  icon: Icon(
                    e,
                    color: i == selectedIndex
                        ? Palette.facebookBlue
                        : Colors.black45,
                    size: 30,
                  ),
                ),
              ))
          .values
          .toList(),
      onTap: onTap,
    );
  }
}

Finish the nav_screen.dart file:

in the nav_screen.dart now we want to display the screen and selected screen, that's why in the body property of the Scaffold() widget let's use the TabBarView() widget:

body: TabBarView(children: _screens),

But if you want to maintain the scrolling position while you're scrolling you can use IndexedStack instead of TabBarView,

body: IndexedStack(
          index: _selectedIndex,
          children: _screens,
        ),

So the final code of the nav_screen.dart looks like this:

import 'package:facebook_ui/screens/home_screen.dart';
import 'package:facebook_ui/widgets/custom_tabbar.dart';
import 'package:flutter/material.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';

class NavScreen extends StatefulWidget {
  @override
  _NavScreenState createState() => _NavScreenState();
}

class _NavScreenState extends State<NavScreen> {
  final List<Widget> _screens = [
    HomeScreen(),
    Center(child: Text("Screen1")),
    Center(child: Text("Screen2")),
    Center(child: Text("Screen3")),
    Center(child: Text("Screen4")),
    Center(child: Text("Screen5")),
  ];

  final List<IconData> _icons = [
    Icons.home,
    Icons.ondemand_video,
    MdiIcons.accountCircleOutline,
    MdiIcons.accountGroupOutline,
    MdiIcons.bellOutline,
    Icons.menu,
  ];

  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: _icons.length,
      child: Scaffold(
        body: IndexedStack(
          index: _selectedIndex,
          children: _screens,
        ),
        bottomNavigationBar: CustomTabBarWidget(
          icons: _icons,
          selectedIndex: _selectedIndex,
          onTap: (index) => setState(() => {_selectedIndex = index}),
        ),
      ),
    );
  }
}

Test and run the app

now let's save our files and just tun our application. great if you have done everything correctly you may have just like I did and everything should work just fine.