How to use a Riverpod provider in initState

6k 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
Sayyid J 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
AshishB 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
Mike 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.