更改选项卡时如何让 Flutter ScrollController 保存 ListView.builder() 的位置?
How to get Flutter ScrollController to save position of ListView.builder() when changing tabs?
我用 2 个选项卡做了一个简单的示例,每个选项卡都包含一个 ListView 构建器。我的目标是能够在第一个列表视图中滚动,切换到第二个选项卡,然后切换回第一个并查看与之前相同的滚动位置。
我已经尝试将键添加到每个列表视图,但这只是一个猜测,因为我不完全理解键。那没有帮助。
为什么 ScrollControllers 不保存滚动位置?
示例如下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()),
),
);
}),
],
),
),
);
}
}
这是我想要的一个 hacky 但有效的例子。不过,这感觉不是正确的方法,因为它每帧都会重建两个控制器。
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()),
),
);
}),
],
),
),
);
}
}
您可以使用 AutomaticKeepAliveClientMixin
在选项卡视图中保留状态。
例如
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;
}
不要在 TabBarView
的子代中使用 ListView.builder
使用 GetListView
.
例如
TabBarView(
children: <Widget>[
GetListView(),
ListView.builder(
controller: controllerB,
itemCount: 2000,
itemBuilder: (context, i) {
return Card(
child: ListTile(
title: Text(i.toString()),
),
);
}),
],
),
)
实现此目的的第二种方法是使用 PageStorageKey
。 PageStorageKey
被Scrollables用来保存滚动偏移量。每次滚动完成时,滚动的页面存储都会更新。
例如
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),
));
}),
注意:在第二个示例中,每次都会使用特定的滚动偏移重建小部件。推荐使用第一种方案。
你可以像这样使用 pageStorage
final PageStorageBucket appBucket = PageStorageBucket();
saveScrollOffset(BuildContext context, double offset, String key) =>
appBucket.writeState(context, offset, identifier: ValueKey(key));
double currentPageScrollOffset(BuildContext context, String key) =>
appBucket.readState(context, identifier: ValueKey(key)) ?? 0.0;
main() {
runApp(MaterialApp(
home: HomeScreen(),
));
}
class HomeScreen extends StatelessWidget {
HomeScreen();
@override
Widget build(BuildContext context) {
return PageStorage(
bucket: appBucket,
child: Scaffold(
body: Container(
child: Center(
child: TextButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (_) => NeedToSaveScrollPosition()));
},
child: Text('push')),
),
),
),
);
}
}
class NeedToSaveScrollPosition extends StatelessWidget {
final String bucketOffsetKey = 'thisPageOffsetKey';
@override
Widget build(BuildContext context) {
return Material(
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification pos) {
if (pos is ScrollEndNotification) {
saveScrollOffset(context, pos.metrics.pixels, bucketOffsetKey);
print(currentPageScrollOffset(context, bucketOffsetKey));
}
return true;
},
child: CustomScrollView(
controller: ScrollController(
initialScrollOffset:
currentPageScrollOffset(context, bucketOffsetKey)),
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => Text("$index"),
childCount: 500))
],
)),
);
}
}
使用 NotificationListener 或 scrollController 侦听器获取 listView 位置
您可以使用 PageStorageKey 来保留滚动位置。
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
)
我还必须构建一个类似的功能,其中 ListView.builder
应该保存当前滚动位置,并在用户第二天每次打开应用程序时从该位置开始。
我使用了 scrollable_positioned_list 包来实现它。
步骤- 1. 首先安装并导入包。
步骤 - 2. 用 ScrollablePositionedList.builder
代替 ListView.builder
ScrollablePositionedList.builder(
itemCount: 100,
itemBuilder: (context, index) {
return Text('item number $index');
});
第三步:添加ItemPositionsListener
获取当前滚动位置。
ItemScrollController
用于下次滚动到该位置。
final ItemScrollController itemScrollController = ItemScrollController();
final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create();
ScrollablePositionedList.builder(
itemCount: 100,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
itemBuilder: (context, index) {
return Text('item number $index');
});
第 4 步:像这样在屏幕上显示第一个和最后一个项目。
第 5 步:并在 sharedPreferences 中保存第一项。
//step-4...
Widget get positionsView => ValueListenableBuilder<Iterable<ItemPosition>>(
valueListenable: itemPositionsListener.itemPositions,
builder: (context, positions, child) {
int? firstItem;
int? lastItem;
if (positions.isNotEmpty) {
// Determine the first visible item by finding the item with the
// smallest trailing edge that is greater than 0. i.e. the first
// item whose trailing edge in visible in the viewport.
firstItem = positions
.where((ItemPosition position) => position.itemTrailingEdge > 0)
.reduce((ItemPosition first, ItemPosition position) =>
position.itemTrailingEdge < first.itemTrailingEdge ? position : first)
.index;
// Determine the last visible item by finding the item with the
// greatest leading edge that is less than 1. i.e. the last
// item whose leading edge in visible in the viewport.
lastItem = positions
.where((ItemPosition position) => position.itemLeadingEdge < 1)
.reduce((ItemPosition last, ItemPosition position) =>
position.itemLeadingEdge > last.itemLeadingEdge ? position : last)
.index;
}
//Step-5....
sharedPreferences?.setInt('scrollPosition', firstItem ?? 0);
return SizedBox.shrink();
},
);
第 6 步:将此 positionsView getter 添加到构建方法中,位于 ScrollablePositionedList.builder
上方或下方
Column(
children: [
ScrollablePositionedList.builder(
itemCount: 100,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
itemBuilder: (context, index) {
return Text('item number $index');
}),
positionsView,
]
);
Step-7: 将 sharedPreferences 中保存的滚动位置添加到 ScrollablePositionedList.builder
作为 initialScrollIndex:
.
完成。
Column(
children: [
ScrollablePositionedList.builder(
itemCount: 100,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
initialScrollIndex: sharedPreferences?.getInt('scrollPosition'),
itemBuilder: (context, index) {
return Text('item number $index');
}),
positionsView,
]
);
下次构建可滚动定位列表时,将从上次保存的滚动位置开始。
我用 2 个选项卡做了一个简单的示例,每个选项卡都包含一个 ListView 构建器。我的目标是能够在第一个列表视图中滚动,切换到第二个选项卡,然后切换回第一个并查看与之前相同的滚动位置。
我已经尝试将键添加到每个列表视图,但这只是一个猜测,因为我不完全理解键。那没有帮助。
为什么 ScrollControllers 不保存滚动位置?
示例如下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()),
),
);
}),
],
),
),
);
}
}
这是我想要的一个 hacky 但有效的例子。不过,这感觉不是正确的方法,因为它每帧都会重建两个控制器。
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()),
),
);
}),
],
),
),
);
}
}
您可以使用 AutomaticKeepAliveClientMixin
在选项卡视图中保留状态。
例如
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;
}
不要在 TabBarView
的子代中使用 ListView.builder
使用 GetListView
.
例如
TabBarView(
children: <Widget>[
GetListView(),
ListView.builder(
controller: controllerB,
itemCount: 2000,
itemBuilder: (context, i) {
return Card(
child: ListTile(
title: Text(i.toString()),
),
);
}),
],
),
)
实现此目的的第二种方法是使用 PageStorageKey
。 PageStorageKey
被Scrollables用来保存滚动偏移量。每次滚动完成时,滚动的页面存储都会更新。
例如
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),
));
}),
注意:在第二个示例中,每次都会使用特定的滚动偏移重建小部件。推荐使用第一种方案。
你可以像这样使用 pageStorage
final PageStorageBucket appBucket = PageStorageBucket();
saveScrollOffset(BuildContext context, double offset, String key) =>
appBucket.writeState(context, offset, identifier: ValueKey(key));
double currentPageScrollOffset(BuildContext context, String key) =>
appBucket.readState(context, identifier: ValueKey(key)) ?? 0.0;
main() {
runApp(MaterialApp(
home: HomeScreen(),
));
}
class HomeScreen extends StatelessWidget {
HomeScreen();
@override
Widget build(BuildContext context) {
return PageStorage(
bucket: appBucket,
child: Scaffold(
body: Container(
child: Center(
child: TextButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (_) => NeedToSaveScrollPosition()));
},
child: Text('push')),
),
),
),
);
}
}
class NeedToSaveScrollPosition extends StatelessWidget {
final String bucketOffsetKey = 'thisPageOffsetKey';
@override
Widget build(BuildContext context) {
return Material(
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification pos) {
if (pos is ScrollEndNotification) {
saveScrollOffset(context, pos.metrics.pixels, bucketOffsetKey);
print(currentPageScrollOffset(context, bucketOffsetKey));
}
return true;
},
child: CustomScrollView(
controller: ScrollController(
initialScrollOffset:
currentPageScrollOffset(context, bucketOffsetKey)),
slivers: [
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => Text("$index"),
childCount: 500))
],
)),
);
}
}
使用 NotificationListener 或 scrollController 侦听器获取 listView 位置
您可以使用 PageStorageKey 来保留滚动位置。
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
)
我还必须构建一个类似的功能,其中 ListView.builder
应该保存当前滚动位置,并在用户第二天每次打开应用程序时从该位置开始。
我使用了 scrollable_positioned_list 包来实现它。
步骤- 1. 首先安装并导入包。
步骤 - 2. 用 ScrollablePositionedList.builder
ListView.builder
ScrollablePositionedList.builder(
itemCount: 100,
itemBuilder: (context, index) {
return Text('item number $index');
});
第三步:添加ItemPositionsListener
获取当前滚动位置。
ItemScrollController
用于下次滚动到该位置。
final ItemScrollController itemScrollController = ItemScrollController();
final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create();
ScrollablePositionedList.builder(
itemCount: 100,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
itemBuilder: (context, index) {
return Text('item number $index');
});
第 4 步:像这样在屏幕上显示第一个和最后一个项目。
第 5 步:并在 sharedPreferences 中保存第一项。
//step-4...
Widget get positionsView => ValueListenableBuilder<Iterable<ItemPosition>>(
valueListenable: itemPositionsListener.itemPositions,
builder: (context, positions, child) {
int? firstItem;
int? lastItem;
if (positions.isNotEmpty) {
// Determine the first visible item by finding the item with the
// smallest trailing edge that is greater than 0. i.e. the first
// item whose trailing edge in visible in the viewport.
firstItem = positions
.where((ItemPosition position) => position.itemTrailingEdge > 0)
.reduce((ItemPosition first, ItemPosition position) =>
position.itemTrailingEdge < first.itemTrailingEdge ? position : first)
.index;
// Determine the last visible item by finding the item with the
// greatest leading edge that is less than 1. i.e. the last
// item whose leading edge in visible in the viewport.
lastItem = positions
.where((ItemPosition position) => position.itemLeadingEdge < 1)
.reduce((ItemPosition last, ItemPosition position) =>
position.itemLeadingEdge > last.itemLeadingEdge ? position : last)
.index;
}
//Step-5....
sharedPreferences?.setInt('scrollPosition', firstItem ?? 0);
return SizedBox.shrink();
},
);
第 6 步:将此 positionsView getter 添加到构建方法中,位于 ScrollablePositionedList.builder
Column(
children: [
ScrollablePositionedList.builder(
itemCount: 100,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
itemBuilder: (context, index) {
return Text('item number $index');
}),
positionsView,
]
);
Step-7: 将 sharedPreferences 中保存的滚动位置添加到 ScrollablePositionedList.builder
作为 initialScrollIndex:
.
完成。
Column(
children: [
ScrollablePositionedList.builder(
itemCount: 100,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
initialScrollIndex: sharedPreferences?.getInt('scrollPosition'),
itemBuilder: (context, index) {
return Text('item number $index');
}),
positionsView,
]
);