Flutter documentation for ScrollController has this paragraph:
Scroll controllers are typically stored as member variables in State objects and are reused in each State.build. A single scroll controller can be used to control multiple scrollable widgets, but some operations, such as reading the scroll offset, require the controller to be used with a single scrollable widget.
Does this mean that we cannot pass the same ScrollController
to different ScrollView
widgets to read ScrollController.offset
?
What I'm trying to accomplish is this:
- There are two screens. Each screen has a
ListView.builder()
widget. - Through parameters I pass from screen 1 to screen 2 an object
ScrollController
and apply it toListView
. - I use scrolling and the
offset
value changes, but as soon as I move/return to another screen, theoffset
is knocked down to 0.0 and I see the beginning of the list. - The same
ScrollController
object is used all the time (hashcode is the same)
How can we use one ScrollController
object for different ScrollView
widgets, so that the offset
is not knocked down when moving from screen to screen?
This problem can be solved a bit if, when switching to another screen, we create a new ScrollController
object with initialScrollOffset = oldScrollController.offset
and pass it to ScrollView
.
Update:
I don't seem to understand how to use flutter_hooks. I created a simple example showing that if we use separate widgets and specify ScrollController
as a parameter, the scroll is reset to position 0.0.
Reference for an example: https://dartpad.dev/?id=d31f4714ce95869716c18b911fee80c1
How do we overcome this?
CodePudding user response:
For now, the best solution I can offer is to pass final ValueNotifier<double> offsetState;
instead of final ScrollController controller;
as a widget parameter.
Then, in each widget we create a ScrollController
. By listening to it via the useListenableSelector
hook we change the offsetState
.
To avoid unnecessary rebuilding, we use the useValueNotifier
hook.
A complete example looks like this:
void main() => runApp(
const MaterialApp(
debugShowCheckedModeBanner: false,
home: MyApp(),
),
);
class MyApp extends HookWidget {
const MyApp();
@override
Widget build(BuildContext context) {
print('#build $MyApp');
final isPrimaries = useState(true);
final offsetState = useValueNotifier(0.0);
return Scaffold(
appBar: AppBar(
title: Text(isPrimaries.value
? 'Colors.primaries List'
: 'Colors.accents List'),
actions: [
IconButton(
onPressed: () => isPrimaries.value = !isPrimaries.value,
icon: const Icon(Icons.ac_unit_sharp),
)
],
),
body: isPrimaries.value
? ListPrimaries(offsetState: offsetState)
: ListAccents(offsetState: offsetState),
);
}
}
class ListAccents extends HookConsumerWidget {
const ListAccents({
Key? key,
required this.offsetState,
}) : super(key: key);
final ValueNotifier<double> offsetState;
@override
Widget build(BuildContext context, WidgetRef ref) {
print('#build $ListAccents');
final controller =
useScrollController(initialScrollOffset: offsetState.value);
useListenableSelector(controller, () {
print(controller.positions);
if (controller.hasClients) {
offsetState.value = controller.offset;
}
return null;
});
return ListView(
primary: false,
controller: controller,
children: Colors.accents
.map((color) => Container(color: color, height: 100))
.toList(),
);
}
}
class ListPrimaries extends HookConsumerWidget {
const ListPrimaries({
Key? key,
required this.offsetState,
}) : super(key: key);
final ValueNotifier<double> offsetState;
@override
Widget build(BuildContext context, WidgetRef ref) {
print('#build $ListPrimaries');
final controller =
useScrollController(initialScrollOffset: offsetState.value);
useListenableSelector(controller, () {
print(controller.positions);
if (controller.hasClients) {
offsetState.value = controller.offset;
}
return null;
});
return ListView(
primary: false,
controller: controller,
children: Colors.primaries
.map((color) => Container(color: color, height: 100))
.toList(),
);
}
}
Another idea was to use useEffect
hook and give it a function to save the last value at the moment of dispose()
:
useEffect(() {
return () {
offsetState.value = controller.offset;
};
}, const []);
But the problem is that at this point, we no longer have clients.
Bonus:
If our task is to synchronize the scroll of the ListView
, another useListenableSelector
hook added to each of the widgets solves this problem. Remind that we cannot use the same `ScrollController' for two or more lists at the same time.
useListenableSelector(offsetState, () {
if (controller.hasClients) {
// if the contents of the ListView are of different lengths, then do nothing
if (controller.position.maxScrollExtent < offsetState.value) {
return;
}
controller.jumpTo(offsetState.value);
}
});