State Persistence Techniques for the Flutter Bottom Navigation Bar
Tags: flutter, mobile, tutorial, learning
Published: 2022-04-05 07:35:35

Building a bottom navigation bar is simple in Flutter given the [BottomNavigationBar Widget](https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html). However, we want to add features to the standard Widget. 

## Overview

- [Setup](#setup)
  - [What do we want to achieve?](#goals)
  - [Implementation](#setup-implementation)
  - [Demo](#setup-demo)
  - [Result](#setup-result)
- [1. Stack and OffStage](#1-stack)
  - [Implementation](#1-stack-implementation)
  - [Demo](#1-stack-demo)
  - [Result](#1-stack-result)
  - [Alternative - Indexed Stack](#1-stack-alternative)
- [2. AutomaticKeepAliveClientMixin](#2-mixin)
  - [Implementation](#2-mixin-implementation)
  - [Demo](#2-mixin-demo)
  - [Result](#2-mixin-result)
- [Conclusion](#conclusion)
- [Final Note](#final-note)

We'll set up a basic flutter app with bottom navigation and look at the problems we face. Like not initializing the bottom navigation bar pages every time we switch tabs or preserving the state of bottom tab pages.  
Then we'll try a couple of approaches for resolving them. We'll compare the results and we can decide which one to go forward with.

> Here's the [GitHub repo](https://github.com/nicks101/flutter_tutorials/tree/main/preserve_state_tabs) with all the code.

## Setup <a id="setup"></a>
We're going to start with the basic app containing bottom navigation with two tabs.
- **Tab 1**: Scrollable list of items.  
- **Tab 2**: Displaying the escaped seconds of a Timer.

### What do we want to achieve? <a id="goals"></a>
1. Create the navigation bar page only when we open the page.
1. Preserve scroll position of navigation bar page in **Tab 1**.
1. Preserve the escaped time of Timer in **Tab 2**.

### Implementation <a id="setup-implementation"></a>
Let's start with a new Flutter project.  

#### Parent Widget: Bottom navigation bar
 
We have a simple `Scaffold` with `BottomNavigationBar` containing two `Tabs`.

```dart
class BasicBottomNavigation extends StatefulWidget {
  const BasicBottomNavigation({Key? key}) : super(key: key);

  @override
  State<BasicBottomNavigation> createState() => _BasicBottomNavigationState();
}

class _BasicBottomNavigationState extends State<BasicBottomNavigation> {
  int currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: [         /// List of tab page widgets
        const _Tabbar1(),     
        const _Tabbar2(),
      ][currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: currentIndex,
        onTap: (index) {
          setState(() {
            currentIndex = index;     /// Switching tabs
          });
        },
        items: const [
          BottomNavigationBarItem(icon: Text("1"), label: "Tab"),
          BottomNavigationBarItem(icon: Text("2"), label: "Tab"),
        ],
      ),
    );
  }
}
```

#### Tab 1: A scrollable list of items

We have a `ListView` displaying the index inside a `ListTile`.

 ```dart
class _Tabbar1 extends StatelessWidget {
  const _Tabbar1({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print("Tabbar 1 build");

    return Scaffold(
      appBar: AppBar(title: const Text("Tab bar 1")),
      body: ListView.builder(
        itemBuilder: (context, index) {
          return ListTile(
            title: Text("${index + 1}"),
          );
        },
        itemCount: 50,
      ),
    );
  }
}
```
#### Tab 2: Displaying escaped seconds of a Timer

We are using a [Ticker](https://api.flutter.dev/flutter/scheduler/Ticker-class.html) to run the Timer and update our escaped duration every second. 

> <strong>Fun Fact:</strong> Ticker is used in Flutter for callbacks during [Animation](https://api.flutter.dev/flutter/animation/Animation-class.html) frames.

```dart

class _Tabbar2 extends StatefulWidget {
  const _Tabbar2({Key? key}) : super(key: key);

  @override
  State<_Tabbar2> createState() => _Tabbar2State();
}

class _Tabbar2State extends State<_Tabbar2>
    with SingleTickerProviderStateMixin {
  late final Ticker _ticker;
  Duration _escapedDuration = Duration.zero;

  get escapedSeconds => _escapedDuration.inSeconds.toString();

  @override
  void initState() {
    super.initState();
    print("Tabbar 2 initState");

    _ticker = createTicker((elapsed) {
      if (elapsed.inSeconds - _escapedDuration.inSeconds == 1) {
        setState(() {
          _escapedDuration = elapsed;
        });
      }
    });

    _ticker.start();
  }

  @override
  void dispose() {
    _ticker.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Tab bar 2")),
      body: Center(
        child: Text(escapedSeconds),
      ),
    );
  }
}
```

### Demo <a id="setup-demo"></a>

![demo_01.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1649090479821/aJ7-rddZK.gif)

### Result <a id="setup-result"></a>
1. Tabs are initialized only when we click on them.  
1. The scroll position is **not** preserved.
1. Escaped time of the Timer is **not** preserved.

Nothing was preserved. We create new tab pages every time we click on them. The scroll position is lost we switch back to `Tab 1`. The Timer starts from 0 whenever we open `Tab 2`. 

There is no problem with this approach as long as we don't need to preserve any state.   
But since we do, let's look at how we can achieve it.

## 1. Stack and OffStage <a id="1-stack"></a>

One way to persist the bottom navigation bar page is to use [Stack](https://api.flutter.dev/flutter/widgets/Stack-class.html) Widget.
We'll add all the pages as children of `Stack` with the order of the bottom tabs and display one child at a time with respect to the currently selected bottom tab.

### Implementation <a id="1-stack-implementation"></a>

We'll wrap the `Tabbar` widgets with [OffStage](https://api.flutter.dev/flutter/widgets/Stack-class.html) and the children list with `Stack`.
The offstage parameter takes a boolean value. If it's true, then the child is `hidden` or `offstage`, otherwise, the child is visible.  
There is no change in the Tabbar classes.

#### Parent Widget: Bottom navigation bar

```dart
 return Scaffold(
      body: Stack(       ///  Added Stack Widget
        children: [
          Offstage(          /// Wrap Tab with OffStage 
            offstage: currentIndex != 0,
            child: const _Tabbar1(),
          ),
          Offstage(
            offstage: currentIndex != 1,
            child: const _Tabbar2(),
          ),
        ],
      ),
```

### Demo <a id="1-stack-demo"></a>

![demo_02.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1649090662975/w-Q0YuxLZ.gif)

### Result <a id="1-stack-result"></a>
1. Tabs are **not** initialized only when we click on them.  
1. The scroll position is preserved.
1. Escaped time of the Timer is preserved.

All the tabs are initialized with the parent Widget. Hence the timer in `Tabbar 2` started before we even opened that Tab. The good thing is that it preserves the scroll position and escaped time.

If creating all the tabs at once does not affect the performance and is what we want, then we use this technique.

### 1. Alternative - Indexed Stack <a id="1-stack-alternative"></a>

Turns out there's a `Widget` (as always with Flutter 😇) called [IndexedStack](https://api.flutter.dev/flutter/widgets/IndexedStack-class.html) that we can use. It's less code with the same result.

#### Parent Widget: Bottom navigation bar 

```dart
return Scaffold(
      body: IndexedStack(      /// Replaced with IndexedStack
        index: currentIndex,
        children: const [
          _Tabbar1(),
          _Tabbar2(),
        ],
      ),
```

## 2. AutomaticKeepAliveClientMixin <a id="2-mixin"></a>

As the name suggests, this mixin makes the client (Tabbar child widgets) keep themselves alive (not disposed of) after we switch the tabs. It also creates the Tab only when it is first clicked and not with the Parent Widget like the above methods.

### Implementation <a id="2-mixin-implementation"></a>

AutomaticKeepAliveClientMixin needs a [PageView](https://api.flutter.dev/flutter/widgets/PageView-class.html) in the parent widget. So we'll wrap the body with PageView and pass the list of tabs as its children.

> <strong>Further Reading:</strong> Other than PageView, there's a [TabBarView](https://api.flutter.dev/flutter/material/TabBarView-class.html) (for top app bar tabs), which also makes AutomaticKeepAliveClientMixin work for tabs (child widgets) because it uses PageView internally.

#### Parent Widget: Bottom navigation bar

```dart
class AliveMixinDemo extends StatefulWidget {
  const AliveMixinDemo({Key? key}) : super(key: key);

  @override
  State<AliveMixinDemo> createState() => _AliveMixinDemoState();
}

class _AliveMixinDemoState extends State<AliveMixinDemo> {
  final PageController controller = PageController();  /// initializing controller for PageView

  int currentIndex = 0;
  final tabPages = [
    const _Tabbar1(),
    const _Tabbar2(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: PageView(        /// Wrapping the tabs with PageView
        controller: controller,
        children: tabPages,
        onPageChanged: (index) {
          setState(() {
            currentIndex = index;     /// Switching bottom tabs
          });
        },
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: currentIndex,
        onTap: (index) {
          controller.jumpToPage(index);    /// Switching the PageView tabs
          setState(() {
            currentIndex = index;
          });
        },
        items: const [
          BottomNavigationBarItem(icon: Text("1"), label: "Tab"),
          BottomNavigationBarItem(icon: Text("2"), label: "Tab"),
        ],
      ),
    );
  }
}
```

#### Tab 1: A scrollable list of items

We replacing to `StatefulWidget` here because `AutomaticKeepAliveClientMixin` only works with the `State` Class as defined in its [implementation](https://api.flutter.dev/flutter/widgets/AutomaticKeepAliveClientMixin-mixin.html).

> "A mixin with convenience methods for clients of AutomaticKeepAlive. Used with State subclasses."  

We need only two additions after this.  
First, call the `super.build()` inside the build method. Second, override the `wantKeepAlive` and return `true`.

```dart
class _Tabbar1 extends StatefulWidget {
  const _Tabbar1({Key? key}) : super(key: key);

  @override
  State<_Tabbar1> createState() => _Tabbar1State();
}

class _Tabbar1State extends State<_Tabbar1> 
    with AutomaticKeepAliveClientMixin {     /// Using the mixin
  @override
  Widget build(BuildContext context) {
    super.build(context);    /// Calling build method of mixin
    print("Tabbar 1 build");
    return Scaffold(
      appBar: AppBar(title: const Text("Tab bar 1")),
      body: ListView.builder(
        itemBuilder: (context, index) {
          return ListTile(
            title: Text("${index + 1}"),
          );
        },
        itemCount: 50,
      ),
    );
  }

  @override
  bool get wantKeepAlive => true;    /// Overriding the value to preserve the state
}
```

#### Tab 2: Displaying escaped seconds of a Timer

The changes are the same as with the `Tabbar 1` class above.

```dart
class _Tabbar2State extends State<_Tabbar2>
    with SingleTickerProviderStateMixin, 
    AutomaticKeepAliveClientMixin {    /// Using the mixin
  late final Ticker _ticker;
```

```dart
  @override
  Widget build(BuildContext context) {
    super.build(context);     /// Calling build method of mixin
    return Scaffold(
      appBar: AppBar(title: const Text("Tab bar 2")),
      body: Center(
        child: Text(escapedSeconds),
      ),
    );
  }

  @override
  bool get wantKeepAlive => true;   /// Overriding the value to preserve the state
}
```

### Demo <a id="2-mixin-demo"></a>

![demo_03.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1649090693156/_l1vm0M3S.gif)

### Result <a id="2-mixin-result"></a>
1. Tabs are initialized only when we click on them.  
1. The scroll position is preserved.
1. Escaped time of the Timer is preserved.

The `Tabbar 2` is initialized only the first time when we click on it. The Timer preserves its state and so does the scrolling position in `Tabbar 1`.

> If we want to programmatically change the `keepAlive` condition, then we can use the `updateKeepAlive()` method of `AutomaticKeepAliveClientMixin`. For further reading, refer to [this](https://stackoverflow.com/a/67618563/9468577) StackOverflow answer.

---

## Conclusion <a id="conclusion"></a>

We can choose any one approach from the above options according to our requirements. 
- Don't want to preserve any state -> standard `BottomBarNavigation`.
- Want to preserve state but fine with creating all the tabs at once -> `IndexedStack` or `Stack and OffStage`.
- Want to preserve state and build tabs only once when clicked on them -> `AutomaticKeepAliveClientMixin`.

> `IndexedStack` is the simplest approach while `AutomaticKeepAliveClientMixin` covers our need. Since we usually have API calls in tabs and don't want to call them every time we switch to that tab.

## Final Note <a id="final-note"></a>

Thank you for reading this article. If you enjoyed it, consider sharing it with other people.  
If you find any mistakes, please let me know.  
Feel free to share your opinions below.