How to use a Riverpod provider in initState

5.9k Views Asked by At

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?

3

There are 3 best solutions below

4
On

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')),
      ),
    );
  }
}
4
On

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
    }
...
0
On

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