I have made a simple example with 2 tabs, each containing a ListView builder. My goal is to be able to scroll in the first list view, switch to the 2nd tab, and then switch back to the first and see the same scroll position from before.
I have tried adding Keys to each of the list views, but that was only a guess as I don't fully understand keys. That didn't help.
Why don't the ScrollControllers save the scroll position?
Here is the example main.dart:
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(
home: HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
ScrollController controllerA = ScrollController(keepScrollOffset: true);
ScrollController controllerB = ScrollController(keepScrollOffset: true);
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: <Widget>[
Text('controllerA'),
Text('controllerB'),
],
),
),
body: TabBarView(
children: <Widget>[
ListView.builder(
controller: controllerA,
itemCount: 2000,
itemBuilder: (context, i) {
return ListTile(
title: Text(
i.toString(),
textScaleFactor: 1.5,
style: TextStyle(color: Colors.blue),
));
}),
ListView.builder(
controller: controllerB,
itemCount: 2000,
itemBuilder: (context, i) {
return Card(
child: ListTile(
title: Text(i.toString()),
),
);
}),
],
),
),
);
}
}
Here is a hacky but working example of what I want. This doesn't feel like the correct way to do this though, as its rebuilding both controllers every frame.
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(
home: HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
double offsetA = 0.0;
double offsetB = 0.0;
@override
Widget build(BuildContext context) {
ScrollController statelessControllerA =
ScrollController(initialScrollOffset: offsetA);
statelessControllerA.addListener(() {
setState(() {
offsetA = statelessControllerA.offset;
});
});
ScrollController statelessControllerB =
ScrollController(initialScrollOffset: offsetB);
statelessControllerB.addListener(() {
setState(() {
offsetB = statelessControllerB.offset;
});
});
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: <Widget>[
Text('controllerA'),
Text('controllerB'),
],
),
),
body: TabBarView(
children: <Widget>[
ListView.builder(
controller: statelessControllerA,
itemCount: 2000,
itemBuilder: (context, i) {
return ListTile(
title: Text(
i.toString(),
textScaleFactor: 1.5,
style: TextStyle(color: Colors.blue),
));
}),
ListView.builder(
controller: statelessControllerB,
itemCount: 2000,
itemBuilder: (context, i) {
return Card(
child: ListTile(
title: Text(i.toString()),
),
);
}),
],
),
),
);
}
}
To scroll a Flutter ListView widget horizontally, set scrollDirection property of the ListView widget to Axis. horizontal. This arranges the items side by side horzontally.
You can make a dynamically created ListView by using the ListView. builder() constructor. This will create the ListView items only when they need to be displayed on the screen. It works like an Android RecyclerView but is a lot easier to set up.
You can use AutomaticKeepAliveClientMixin
to persist the states in Tab View.
For Example
class GetListView extends StatefulWidget{
@override
State<StatefulWidget> createState() =>_GetListViewState();
}
class _GetListViewState extends State<GetListView> with AutomaticKeepAliveClientMixin<GetListView>{
@override
Widget build(BuildContext context){
return ListView.builder(
itemCount: 2000,
itemBuilder: (context, i) {
return ListTile(
title: Text(
i.toString(),
textScaleFactor: 1.5,
style: TextStyle(color: Colors.blue),
));
});
}
@override
bool get wantKeepAlive => true;
}
Instead of using ListView.builder
in childern of TabBarView
use GetListView
.
For Example
TabBarView(
children: <Widget>[
GetListView(),
ListView.builder(
controller: controllerB,
itemCount: 2000,
itemBuilder: (context, i) {
return Card(
child: ListTile(
title: Text(i.toString()),
),
);
}),
],
),
)
The second way to achieve this is by using PageStorageKey
. PageStorageKey
is used by Scrollables to save the scroll offset. Each time a scroll completes, the scrollable's page storage is updated.
For Example
ListView.builder(
key: PageStorageKey<String>('controllerA'),
controller: statelessControllerA,
itemCount: 2000,
itemBuilder: (context, i) {
print("Rebuilded 1");
return ListTile(
title: Text(
i.toString(),
textScaleFactor: 1.5,
style: TextStyle(color: Colors.blue),
));
}),
Note: In the second example the widgets will be rebuilded everytime with a specific scroll offset. It's recommended to use the first solution.
you can use PageStorageKey which also preserve scroll position.
A key can be used to persist the widget state in storage after the destruction and will be restored when recreated.
ListView.builder(
key: PageStorageKey(0), //0 is Store index you should use a new one for each page you can also use string
)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With