Dynamic TabBar and TabBarView in Flutter

Introduction

The other day I was making quite complex and customisable TabBar and TabBarView in Flutter.

Prior that I was searching for more tutorials and information on how to do it and I and saw quite some complicated ones.
I’ve decided to simplify everything (as I love lean approach) and create a simplified version of it.

Source code can be taken from here.

Limitations

While making this tutorial, I’ve discovered some problems with it. I will update this article once issues are fixed.

  1. You cannot add more Tab’s than you started with. For example if you started with 4, you cannot add more than 4. Your UI will overflow. However, you can decrease it to 3, 2, 1 or even 0.
  2. After removing a Tab, you can still scroll horizontally and access TabBarView of that removed Tab. After doing so, you TabController will crash. Quick fix for that would be not to allow users to navigate while scrolling and implement buttons for that matter.
  3. After removing a Tab, TabBarView still displays wrong Tabs. Fix — to force set value.

I’ll provide Bonus section where these current issues can be avoided.

Let’s get on it

Everything will be in one class, just for easiness of it.

Starting my removing all not needed code and on start, running MainPage page.

import 'package:flutter/material.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 Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MainPage());
}
}

MainPage is our main single screen as Stateful widget because we will need to update our screen on certain interactions.

class MainPage extends StatefulWidget {
@override
_MainPageState createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> with TickerProviderStateMixin {
..
..
..

@override
Widget build(BuildContext context) { .. }
..
..
}

We have to use TickerProviderStateMixin mixin in order our TabBar to behave as we expect.

We will declare several variables:

final int _startingTabCount = 4;

List<Tab> _tabs = List<Tab>();
List<Widget> _generalWidgets = List<Widget>();
TabController _tabController;

_startingTabCount — how many tabs we will start with once app starts.

_tabs — list of Tabs. These are items which will be visible as Tabs.

_generalWidgets — list of TabBarView children items. These are widgets will be displayed once Tab is clicked.

_tabController — controller of the Tabs(duh). In order to sync everything together, you’ve to have it. Key thing to notice here is that TabBarView children count has to be same as TabBar children. Otherwise you will experience an issue.

Then we have initState and dispose methods. initState will get called first time page is loaded. We add desired amount of tabs into _tabs list and initialise _tabController

@override
void initState() {
_tabs = getTabs(_startingTabCount);
_tabController = getTabController();
super.initState();
}

@override
void dispose() {
_tabController.dispose();
super.dispose();
}
List<Tab> getTabs(int count) {
_tabs.clear();
for (int i = 0; i < count; i++) {
_tabs.add(getTab(i));
}
return _tabs;
}
TabController getTabController() {
return TabController(length: _tabs.length, vsync: this);
}

Tab getTab(int widgetNumber) {
return Tab(
text: "$widgetNumber",
);
}

While initialising TabController you need to provide several required parameters. First one is length — how many tabs you will have. That’s the amount which should match among TabBar and TabBarViews . Another property is vsync which we reference to this because we are using TickerStateProviderMixin mixin.

getTabs method simply clears whole _tabs list, loops through how many pages you want to have and adds them to the list.

Our build method method:

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Dynamic TBV"),
bottom: TabBar(
tabs: _tabs,
controller: _tabController,
),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: _addAnotherTab,
),
IconButton(
icon: Icon(Icons.remove),
onPressed: _removeTab,
),
],
),
body: TabBarView(
controller: _tabController,
children: getWidgets(),
),
);
}

We use Scaffold by default because we need to have appBar property.

appBar: AppBar(
title: Text("Dynamic TBV"),
bottom: TabBar(
tabs: _tabs,
controller: _tabController,
),
actions: ..
),

AppBar widget has bottom property which is used on the bottom of AppBar . In current scenario we will be using it for our TabBar . We also have to assign our _tabController in order things behave as they should.

actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: _addAnotherTab,
),
IconButton(
icon: Icon(Icons.remove),
onPressed: _removeTab,
),
],

AppBar also have actions property which we will utilise and add two IconButtons — add and remove. On pressing add we will call _addAnotherTab method and on pressing remove we will call _removeTab method.

void _addAnotherTab() {
_tabs = getTabs(_tabs.length + 1);
_tabController.index = 0;
_tabController = getTabController();
_updatePage();
}

void _removeTab() {
_tabs = getTabs(_tabs.length - 1);
_tabController.index = 0;
_tabController = getTabController();
_updatePage();
}

void _updatePage() {
setState(() {});
}

_addAnotherTab will increase Tabs count by one. We force set _tabController index value to 0, because somehow, it doesn’t get updated. Then reinitialise _tabController because we want to ensure that length property is set correctly. And when we call _updatePage method which will update UI of the page. It’s my personal preference to control each step therefore I love having setState separated. Also it feels much cleaner and easier to understand the flow.

_removeTab works exactly like _addAnotherTab method, but instead of adding count by one, it decreases it by one.

Finally we have our body of Scaffold .

body: TabBarView(
controller: _tabController,
children: getWidgets(),
),

Here we declare TabBarView . We want to assign controller to it so everything is in sync with TabBar .

List<Widget> getWidgets() {
_generalWidgets.clear();
for (int i = 0; i < _tabs.length; i++) {
_generalWidgets.add(getWidget(i));
}
return _generalWidgets;
}

getWidgets method is similar to getTabs method. It clears current list, but now, it gets widget count from the length of _tabs list. Because we want to ensure that length synchronisation.

…aaand that’s it! We have our beautiful dynamic TabBar with dynamic TabBarViews!

Bonus

Source code from another branch.

I won’t go much into details on it. High level — we add two buttons on the bottom of the page. We do checking, if page is first, if page is last — based on that we display various texts or functionality.
I’ve also included simple protection for overflowing UI after adding critical amount of Tabs.

And in the meanwhile, waiting for current issues to be fixed.

Please let me know if you have any questions. And hopefully this article will inspire to build you something more awesome and dynamic!

Cheers

I build kick-ass mobile apps @ https://isawthatguy.com || Product Virtuoso and Startup Freak

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store