How to do bottom navigation with different App Bars and Floating Action Buttons in Flutter right?

TL;DR I settled on the final method (here).

Background

Bottom navigation is a common UI navigation pattern on mobile, which is adapted by many popular apps such as IG, Facebook, etc.

Instgram UI using bottom navigation pattern.

by Marina D

As a cross platform UI toolkit, Flutter comes with widgets that can build such pattern, but developer needs to be careful when doing this to avoid some pitfalls.

Widgets to be covered

For the widgets will be used in the following section, there are official documents and introduction videos about them by Google. It is a good way to get yourself familiar about them.

Possible Solutions

There is a couple of ways to archive this. We will go through how to do it, and what are the pitfalls introduced by each methods in the following.

Wrap Scaffolds with Scaffold

Since Scaffold widget provides convenient way to add App Bar and Floating Action Button, and almost all Bottom Navigation tutorials will use root Scaffolds to start, this is the easiest way to do this.

In this way, different page is wrapped with Scaffolds which are placed within Tab Bar View, which is under the root Scaffold that comes with the Bottom Navigation Bar

Widget Tree

Code

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: DefaultTabController(
            length: 2,
            child: Scaffold(
                body: TabBarView(children: [
                  PageWidget(title: 'Page1'),
                  PageWidget(title: 'Page2'),
                ]),
                bottomNavigationBar:
                    TabBar(tabs: [Tab(text: "First"), Tab(text: "Second")]))));
  }
}

class PageWidget extends StatelessWidget {
  PageWidget({Key key, this.title}) : super(key: key);

  final String title;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Text(
          title,
          style: Theme.of(context).textTheme.headline4,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

The problem of this is, nesting scaffolds is problematic. It causes strange UI behaviours and Flutter will show warning in such case.

Use one root Scaffold with code to changes App Bar and Floating Action Button for different pages

To avoid the above situation, one may change those Scaffolds wrapping different pages to other kinds of container widgets, and wire up the navigation to change App Bar and Floating Action Button accordingly.

Widget Tree

Code

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: PagesWrap());
  }
}

class PagesWrap extends StatefulWidget {
  @override
  State createState() => _PageWrapState();
}

class _PageWrapState extends State<PagesWrap> {
  int _selectedPage = 0;

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
        length: 2,
        child: Scaffold(
          appBar: [
            AppBar(
              title: Text('Page1'),
            ),
            AppBar(
              title: Text('Page2'),
            )
          ][this._selectedPage],
          body: TabBarView(children: [
            PageWidget(title: 'Page1'),
            PageWidget(title: 'Page2'),
          ]),
          bottomNavigationBar: TabBar(
              onTap: (selected) {
                setState(() {
                  _selectedPage = selected;
                });
              },
              tabs: [Tab(text: "First"), Tab(text: "Second")]),
          floatingActionButton: [
            FloatingActionButton(
              onPressed: () {},
              tooltip: 'Increment',
              child: Icon(Icons.add),
            ),
            FloatingActionButton(
              onPressed: () {},
              tooltip: 'Increment',
              child: Icon(Icons.home),
            ),
          ][this._selectedPage],
        ));
  }
}

class PageWidget extends StatelessWidget {
  PageWidget({Key key, this.title}) : super(key: key);

  final String title;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        title,
        style: Theme.of(context).textTheme.headline4,
      ),
    );
  }
}

Result

Resulting page switching.

The warning do go away, and the widget tree is much smaller. However, this cause widgets belong to same page separated in different places, and introduce new stateful widget and extra logic to handling switching widgets, which introducing new problems:

Wrap different Scaffolds with Tab Bar View

In this method, each pages are wrapped with a Scaffold, and using a Tab Bar View to contain them. There are two widgets not covered above will be used, namely Colum (api document), Expanded (video) and Material (api document).

Widget Tree

Code

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: Material(
            child: DefaultTabController(
                length: 2,
                child: Column(children: [
                  Expanded(
                    child: TabBarView(children: [
                      PageWidget(title: 'Page 1'),
                      PageWidget(title: 'Page 2'),
                    ]),
                  ),
                  TabBar(tabs: [Tab(text: "First"), Tab(text: "Second")])
                ]))));
  }
}

class PageWidget extends StatelessWidget {
  PageWidget({Key key, this.title}) : super(key: key);

  final String title;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Text(
          title,
          style: Theme.of(context).textTheme.headline4,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: Icon(Icons.add),
      ),
    );
  }
}

You may ask “Wait, really? Can we put multiple Scaffold together?”

Yes, indeed, you can. just don’t nest them (source).

This is a much less talked about methods, but this one is relatively easy to implement and without drawbacks mentioned above. One pitfall is this requires a Material as parent of Default Tab Controller.

Closing

Now, here you have it, no warning, no widgets messing around, all good.

While I have settled on the last method, I would also like to know other methods implementing this.

I hope this helps Flutter developers work on UI. If you find this useful, welcome to share this.

If you like this article, remember to show your support by buy me a book.