1

I use Riverpod as state management in my web app. I am trying to build an AppBar that scrolls automatically to certain parts of a ListView.

I created a ScrollController as a provider for this purpose.

final scrollControllerProvider = StateProvider<ScrollController?>((ref) => ScrollController());

To scroll, I use .animateTo from the AppBar actions.

ref.read(scrollControllerProvider)!.animateTo(
  0,
  duration: const Duration(milliseconds: 500),
  curve: Curves.easeInOut,
);

The scrolling works, but it throws an exception

The provided ScrollController is currently attached to more than one ScrollPosition.

I have read that I should be using a StatefulWidget. However, using a ConsumerStatefulWidget I can't create the ScrollController using Riverpod, because I need to initiate it in initState() and I can't access to a provider from it. Is possible to have these two elements together?

JAgüero
  • 403
  • 1
  • 4
  • 14

3 Answers3

0

ScrollController is a ChangeNotifier class, instead of StateProvider, use ChangeNotifierProvider, keep in mind if you can't attach it to multiple scrollable.

final myScrollControllerProvider =
    ChangeNotifierProvider((ref) => ScrollController());

maybe we can make it into new class, create a like forceDispose() method :

class MyScrollController extends ScrollController {
  void forceDispose(){
    this.dispose();
  }
}

but since i am not sure how you use this, and where you will dispose it in some state,either we can't initialize it in some initState(), lets just leave the provider autoDispose its self:

 final myScrollControllerProvider =
    ChangeNotifierProvider.autoDispose((ref) => ScrollController());

i test it, i create New Consumer to Imitate Another Widget, but you must sure the actuall scrollable still mounted :

class MyWidget extends ConsumerWidget {
  const MyWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, ref) {
    final sc = ref.watch(myScrollControllerProvider);
    return Scaffold(
      body: Column(
        children: [
          Row(
            children: [
              Consumer(
                builder: (BuildContext context, WidgetRef ref, Widget? _) {
                  final scFromAnotherWidget = ref.watch(myScrollControllerProvider);
                  return TextButton(
                      onPressed: () {
                        scFromAnotherWidget.jumpTo(sc.offset + 30);
                      },
                      child: const Text("Animate to"));
                },
              ),
              TextButton(
                  onPressed: () {
                    Navigator.pushReplacement(
                        context,
                        MaterialPageRoute(
                            builder: (context) => const NewScreen()));
                  },
                  child: const Text("Navigate to brand new Screen"))
            ],
          ),
          Expanded(
            child: ListView.builder(
              controller: sc,
              itemCount: 100,
              itemBuilder: (context, index) => ListTile(
                title: Text('$index'),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class NewScreen extends StatelessWidget {
  const NewScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: TextButton(
            onPressed: () {
              Navigator.pushReplacement(context,
                  MaterialPageRoute(builder: (context) => const MyWidget()));
            },
            child: const Text('Back to My Widget')),
      ),
    );
  }
}
Sayyid J
  • 1,215
  • 1
  • 4
  • 18
  • the controller is the provider, I need to access it from initState – JAgüero Dec 29 '22 at 19:11
  • 1
    sorry i was misunderstand before, but scroll controller is use localy, its useless to use it as global variable and use everywhere. its only can attach and listen to single scrollable, and you need to dispose it localy to avoid memory leaks. if you could point me the reason why you need to use provider to hold scroll controller, maybe i can help with in spesific case – Sayyid J Dec 30 '22 at 09:56
  • I need to store it globally because I want to access the scrolling from a different widget. I explain the whole situation better in this other question https://stackoverflow.com/questions/74956984/flutter-push-a-new-page-and-scroll-down-to-certain-widget because I was trying with a different approach with GlobalKeys instead – JAgüero Dec 30 '22 at 18:38
  • i edited the answer maybe can help you in your case? – Sayyid J Dec 31 '22 at 06:47
0

You have access to ref inside any function that resides in the ConsumerStatefulWidget. So, you can simply call your provider in initState() function and it should behave normally

...
  class _MyClassState extends ConsumerState<MyClass> {

  late ScrollController _scrollController;
  
  @override
    void initState() {
      super.initState();
      _scrollController = ref.read(scrollControllerProvider); // Doesn't throw error
    }
...
AshishB
  • 747
  • 7
  • 18
  • It does throw an error: `The argument type 'StateProvider' can't be assigned to the parameter type 'ProviderListenable'.` – JAgüero Dec 29 '22 at 20:59
  • Correction: it does not throw an error if used with `!`, but the Riverpod documentation says the watch method should not be used in initState(). I used read instead, but now the controller goes scrolls down to the required position and then goes up again. – JAgüero Dec 29 '22 at 21:09
  • You cannot use ref.watch in init method – Mike Aug 30 '23 at 20:40
  • 1
    Sure. But nothing is stopping you from using ref.read as well, right? I have edited my answer to ref.read now – AshishB Sep 01 '23 at 01:29
0

I thimk a solution is to use ref.read(...) In the init method. It would not update the view and take the correct valute.

Mike
  • 335
  • 1
  • 8