'Flutter - ListView inside on a TabBarView loses its scroll position

I have a very simple Flutter app with a TabBarView with two views (Tab 1 and Tab 2), one of them (Tab 1) has a ListView with many simple Text Widgets, the problem with this is that after I scroll down the ListView elements of Tab 1, if I swipe from Tab 1 to Tab 2 and finally I swipe from Tab 2 to Tab 1, the previous scroll position in the ListView of Tab 1 get lost.

Here is the 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: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  late TabController controller;

  @override
  void initState() {
    super.initState();
    controller = TabController(
      length: 2,
      vsync: this,
    );
  }

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

  @override
  Widget build(BuildContext context) {
    var tabs = const <Tab>[
      Tab(icon: Icon(Icons.home), text: 'Tab 1'),
      Tab(icon: Icon(Icons.account_box), text: 'Tab 2')
    ];

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: TabBarView(controller: controller, children: <Widget>[
        ListView(children: <Widget>[
          Column(children: <Widget>[
            for (int i = 0; i < 48; i++) Text('Data $i'),
          ])
        ]),
        const Center(child: Text('Tab 2'))
      ]),
      bottomNavigationBar: Material(
        color: Colors.deepOrange,
        child: TabBar(controller: controller, tabs: tabs),
      ),
    );
  }
}

I have even separated the TabBarView childrens (Tab 1 and Tab 2) in another classes and I have noticed that the

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

method of each child (Tab 1 and Tab 2) is executed every time I swipe to it's container tab.

My questions are:

1.- How can I keep the scroll of the ListView even if I move from tab to tab?

2.- Is there a way to execute the

@override
Widget build(BuildContext context) {

}

method only once if I separate the TabBarView childrens (Tab 1 and Tab 2) to another classes? I mean, if I have to retrieve data when the Tab 1 and Tab 2 are created I don't want to do this every time it's Tab is swiped in. That would be expensive.

3.- In general, Is there a way to prevent that a tab view (including it's variables, data, etc.) be rebuilt every time I swipe to that tab?

Thank you in advance.



Solution 1:[1]

If you give each TabBarView a PageStorageKey the scroll offset will be saved. See more info about PageStorageKey here.

Solution 2:[2]

To be more specific, you can use PageStorageKey with any scrollable view to keep the scrolling position, e.g.:

new ListView.builder(key: new PageStorageKey('myListView'), ...)

Solution 3:[3]

Output:

enter image description here


Code:

@override
Widget build(BuildContext context) {
  return DefaultTabController(
    length: 2,
    child: Scaffold(
      appBar: AppBar(
        title: Text("PageStorageKey"),
        bottom: TabBar(
          tabs: [
            Tab(icon: Icon(Icons.looks_one), text: "List1"),
            Tab(icon: Icon(Icons.looks_two), text: "List2"),
          ],
        ),
      ),
      body: TabBarView(
        children: [
          _buildList(key: "key1", string: "List1: "),
          _buildList(key: "key2", string: "List2: "),
        ],
      ),
    ),
  );
}

Widget _buildList({String key, String string}) {
  return ListView.builder(
    key: PageStorageKey(key),
    itemBuilder: (_, i) => ListTile(title: Text("${string} ${i}")),
  );
}

Solution 4:[4]

Actually You don't need PageStorageKey. The Problem is that the tab1 widget would be rebuild when you swipe to tab2 and then swipe back. So you lost your position. The most simple solution is to use AutomaticKeepAliveClientMixin and override "wantToKeepAlive" method. Then TabbarView will be kept in memory automatically and would not be rebuild. So the problem would be resolved. You can see more details from https://api.flutter.dev/flutter/widgets/AutomaticKeepAliveClientMixin-mixin.html

Solution 5:[5]

Can't add a comment, so left it as answer.

If you using PageStorageKey, have a lot of items in the ListView, scroll down plenty items and have lag on tab switch, the solution is to provide itemExtent to ListView.

As I understand, without itemExtent ListView don't know which items immediately show, because using PageStorageKey cashed only position in pixels, and ListView need calculate height of all items scrolled from top. In another hand, with itemExtent ListView can easily calculate item to show, for example: index = cashedPositionInPixels / itemExtent.

Solution 6:[6]

Is there a way to execute Widget build(BuildContext context) method only once...

Imho, idea of flutter is to be ready for rebuilding always. It should be cheap. If you have some expensive actions, you can use State to "cache" results. E.g. you can do network request in initState and via setState rebuild when receive response. For tabs, you can prepare and save data in parent widget. You can find more info in flutter tutorial about managing state

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Jared Burrows
Solution 2 Oleg Khalidov
Solution 3 CopsOnRoad
Solution 4 passerbywhu
Solution 5 D31
Solution 6