63

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 its 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 its Tab is swiped in. That would be expensive.

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

Thank you in advance.

danronmoon
  • 3,814
  • 5
  • 34
  • 56
SaloGala
  • 1,904
  • 3
  • 14
  • 19

7 Answers7

78

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

Jared Burrows
  • 54,294
  • 25
  • 151
  • 185
Jordan Nelson
  • 1,162
  • 2
  • 8
  • 9
  • 4
    I have a huge lag on tab switch with this solution if you scroll a lot of elements. Any ideas? – Kuva Jan 03 '19 at 13:46
  • 1
    @Kuva Are you using ListView() or ListView.builder()? – Jordan Nelson Jan 29 '19 at 22:41
  • This was a super simple working solution, though I also had the issue of a noticeable lag when switching screens (I'm using ListView.builder()). – kue Apr 23 '19 at 02:51
  • Keep in mind that the Navigator adds automatically a PageStorage for each route, so if you lose the scroll position you might need to create a static PageStorageBucket() and use that within a PageStorage to wrap your widget tree within the route. – avioli Sep 18 '20 at 07:57
45

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'), ...)
Oleg Khalidov
  • 5,108
  • 1
  • 28
  • 29
  • 4
    I have a huge lag on tab switch with this solution if you scroll a lot of elements. Any ideas? – Kuva Jan 03 '19 at 13:47
  • @Kuva I also had this issue. Inside ListView.builder more than 100+ items were there. That lags entire page while switching tabs. Did you found any solution ? – Sharath B Naik Aug 30 '23 at 06:05
39

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}")),
  );
}
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
  • 1
    Do you know to change the tab item for each section when scrolling? Like this https://www.youtube.com/watch?v=LrOR5QOCHBI – Vinoth Vino Jul 14 '20 at 06:30
  • @VinothVino Use a `ScrollController` in your `ListView`, and the logic would be to check if the offset is greater than certain height, you'd simple do a `TabController.animateTo` on your `TabBar` – CopsOnRoad Jul 14 '20 at 06:33
  • I want to change the tab when the section comes near to the tab bar. The list item is dynamic I don't know how to calculate the height for each section of the list item. Okay, let me try. Thanks for your reply bro. – Vinoth Vino Jul 14 '20 at 06:45
  • 1
    Best Answer, key:PageStorageKey('myOwnKey') was the answer – Mohsen Feb 28 '21 at 10:16
  • 1
    Thank you so much for the solution. I think this is the most simplest way. – Darari Nur Amali Jun 02 '21 at 15:53
17

Jordan Nelson's answer is the correct one. Don't use mine.


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

Ok, it wasn't so easy as I thought but I think I managed to do it.

My idea is to keep listview's offset in HomePageState, and when we scroll listview we just get offset from notifier and set it via setter (please make it cleaner and share!).

Then when we rebuild listview we just ask our main widget to give us saved offset and by ScrollController we initialize list with that offset.

I also changed your listview since it had one column element with 50 texts to use 50 elements with one text each. Hope you don't mind :)

The code:

import 'package:flutter/material.dart';

void main() {
  runApp(new MyApp());
}

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

typedef double GetOffsetMethod();
typedef void SetOffsetMethod(double offset);

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

  final String title;

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

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  TabController controller;
  double listViewOffset=0.0;

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

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

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

    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new TabBarView(
      controller: controller,
      children: <Widget>[
        new StatefulListView(
          getOffsetMethod: () => listViewOffset,
          setOffsetMethod: (offset) => this.listViewOffset = offset,
        ),
        new Center(child: new Text('Tab 2'))
      ]),
      bottomNavigationBar: new Material(
        color: Colors.deepOrange,
        child: new TabBar(controller: controller, tabs: tabs),
      ),
    );
  }
}

class StatefulListView extends StatefulWidget {
  StatefulListView({Key key, this.getOffsetMethod, this.setOffsetMethod}) : super(key: key);

  final GetOffsetMethod getOffsetMethod;
  final SetOffsetMethod setOffsetMethod;

  @override
  _StatefulListViewState createState() => new _StatefulListViewState();
}

class _StatefulListViewState extends State<StatefulListView> {

  ScrollController scrollController;

  @override
  void initState() {
    super.initState();
    scrollController = new ScrollController(
      initialScrollOffset: widget.getOffsetMethod()
    );
  }

  @override
  Widget build(BuildContext context) {
    return new NotificationListener(
      child: new ListView.builder(
        controller: scrollController,
        itemCount: 50,
        itemBuilder: (BuildContext context, int index) {
          return new Text("Data "+index.toString());
        },
      ),
      onNotification: (notification) {
        if (notification is ScrollNotification) {
          widget.setOffsetMethod(notification.metrics.pixels);
        }
      },
    );
  }
}
General Grievance
  • 4,555
  • 31
  • 31
  • 45
Marcin Szałek
  • 4,609
  • 5
  • 30
  • 43
  • I really had no knowledge about **ScrollController**, It was my fault for not reading the documentation. No doubt your answer solved my problem, thank you very much! By the way, I think the way you get the offset and you assign it is very clean already. – SaloGala Aug 03 '17 at 07:25
  • I modified a little bit your code, to be able to use it in more places, and created a medium post about it - https://medium.com/@boldijar.paul/flutter-keeping-list-view-index-while-changing-page-view-c260352f35f8 – Boldijar Paul Mar 09 '18 at 11:33
10

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

passerbywhu
  • 522
  • 5
  • 8
2

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.

D31
  • 112
  • 1
  • 10
1

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

German Saprykin
  • 6,631
  • 2
  • 29
  • 26
  • Actually, the Gallery app Tabs demo doesn't keep the scroll in the ListViews when you swipe from "LEFT" tab to "RIGHT" tab, I have the latest version of Flutter Gallery app available on the Play Store in my Android device and have the same problem. Have you tested it? About the State to "cache" results, I'll investigate about that right now. Thank you so much! – SaloGala Jul 28 '17 at 08:06
  • I tested , but did mistake. It has issues with app bar which overlaps list after going back. – German Saprykin Jul 28 '17 at 08:42