Flutter comes with great set of built-in widgets and one of them is Bottom Navigation Bar. It is a common used navigation type for mobile applications.

Bottom Navigation Bar is easy to implement in Flutter.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Home(),
    );
  }
}

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  int _selectedIndex = 0;

  List<Widget> _widgets = <Widget>[
    Center(
      child: Text("Home"),
    ),
    Center(
      child: Text("Profile"),
    )
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('BottomNavigationBar'),
      ),
      body: Center(
        child: _widgets.elementAt(_selectedIndex),
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            title: Text('Home'),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            title: Text('Profile'),
          ),
        ],
        currentIndex: _selectedIndex,
        onTap: _onItemTapped,
      ),
    );
  }
}

Now, having a text at the center of the page is fun but let's create two new widgets that have will have an infinite list.

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          title: Text("Home"),
          subtitle: Text("$index"),
        );
      },
    );
  }
}

class ProfilePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          title: Text("Profile"),
          subtitle: Text("$index"),
        );
      },
    );
  }

We should update_widgets too.

List<Widget> _widgets = <Widget>[HomePage(), ProfilePage()];
Notice that scroll position isn't preserved between tab changes

Great, we have an infinite scrollable list in both pages but it seems like scroll position isn't preserved when switching tabs. That is because Flutter disposes and rebuilds your widgets each time you switch to a tab. This is probably something that we wouldn't want in our app. There are several ways to preserve state between tab switches. We are going to use AutomaticKeepAliveClientMixin and PageView together to maintain our widget's state.

First let's convert HomePage and ProfilePage widgets to StatefulWidget and add with AutomaticKeepAliveClientMixin to our class. We also need to override wantKeepAlive after using the mixin.

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;
  
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ListView.builder(
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          title: Text("Home"),
          subtitle: Text("$index"),
        );
      },
    );
  }
}

class ProfilePage extends StatefulWidget {
  @override
  _ProfilePageState createState() => _ProfilePageState();
}

class _ProfilePageState extends State<ProfilePage>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;
  
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          title: Text("Profile"),
          subtitle: Text("$index"),
        );
      },
    );
  }
}

Let's add a PageController and implement _onPageChanged method.

PageController pageController = PageController();

void _onPageChanged(int index) {
    setState(() {
        _selectedIndex = index;
    });
}

We need to update _onItemTapped method. Instead of changing index, we are going to jump to page.

void _onItemTapped(int index) {
    pageController.jumpToPage(index);
}

Last, change your Scaffold body with a PageView widget. We are setting scroll physics as NeverScrollableScrollPhysics() to disable sliding between pages.

body: PageView(
    controller: pageController,
    onPageChanged: _onPageChanged,
    children: _widgets,
    physics: NeverScrollableScrollPhysics(),
),

That is it, if you reload the app and switch tabs, scroll position is preserved. Widgets aren't disposed on tab switches anymore and initState is only called once when you switch to a tab for the first time. You can now maintain your widget's state when navigating between tabs and different routes.

Complete example

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Home(),
    );
  }
}

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  int _selectedIndex = 0;

  List<Widget> _widgets = <Widget>[HomePage(), ProfilePage()];

  PageController pageController = PageController();

  void _onItemTapped(int index) {
    pageController.jumpToPage(index);
  }

  void _onPageChanged(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('BottomNavigationBar'),
      ),
      body: PageView(
        controller: pageController,
        onPageChanged: _onPageChanged,
        children: _widgets,
        physics: NeverScrollableScrollPhysics(),
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            title: Text('Home'),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            title: Text('Profile'),
          ),
        ],
        currentIndex: _selectedIndex,
        onTap: _onItemTapped,
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ListView.builder(
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          title: Text("Home"),
          subtitle: Text("$index"),
        );
      },
    );
  }
}

class ProfilePage extends StatefulWidget {
  @override
  _ProfilePageState createState() => _ProfilePageState();
}

class _ProfilePageState extends State<ProfilePage>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ListView.builder(
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          title: Text("Profile"),
          subtitle: Text("$index"),
        );
      },
    );
  }
}