Disable scrolling of CustomScrollView while scrolling without setState Flutter

1,800

Solution 1

When the build method is called, all widgets in that build method will be rebuild except for const widgets, but const widget cannot accept dynamic arguments (only a constant values).

Riverpod provides a very good solution in this case, With ProviderScope you can pass arguments by inherited widget instead of widget constructor (as when passing arguments using navigation) so the contractor can be const.

Example :

Data module

TLDR you need to use Freezed package or override the == operator and the hashCode almost always because of dart issue.

class DataClass {
  final int age;
  final String name;

  const DataClass(this.age, this.name);

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;

    return other is DataClass && other.age == age && other.name == name;
  }

  @override
  int get hashCode => age.hashCode ^ name.hashCode;
}

setting our ScopedProvider as a global variable

final dataClassScope = ScopedProvider<DataClass>(null);

The widget we use in our list

class MyChildWidget extends ConsumerWidget {
  const MyChildWidget();

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final data = watch(dataClassScope);

    // Note for better optimaization
    // in case you are sure the data you are passing to this widget wouldn't change
    // you can just use StatelessWidget and set the data as:
    // final data = context.read(dataClassScope);
    // use ConsumerWidget (or Consumer down in this child widget tree) if the data has to change

    print('widget with name\n${data.name} rebuild');

    return SliverToBoxAdapter(
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 20),
        child: Text(
          'Name : ${data.name}\nAge ${data.age}',
          textAlign: TextAlign.center,
        ),
      ),
    );
  }
}

finally the main CustomScrollView widget

class MyMainWidget extends StatefulWidget {
  const MyMainWidget();

  @override
  State<MyMainWidget> createState() => _MyMainWidgetState();
}

class _MyMainWidgetState extends State<MyMainWidget> {
  bool canScroll = true;

  void changeCanScrollState() {
    setState(() => canScroll = !canScroll);
    print('canScroll $canScroll');
  }

  final dataList = List.generate(
    20,
    (index) => DataClass(10 * index, '$index'),
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: () {
          changeCanScrollState();
        },
        child: CustomScrollView(
          shrinkWrap: true,
          physics: canScroll
              ? BouncingScrollPhysics()
              : NeverScrollableScrollPhysics(),
          slivers: [
            for (int i = 0; i < dataList.length; i++)
              ProviderScope(
                overrides: [
                  dataClassScope.overrideWithValue(dataList[i]),
                ],
                child: const MyChildWidget(),
              ),
          ],
        ),
      ),
    );
  }
}

Don't forget to wrap the MaterialApp with ProviderScope.

  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );

Solution 2

Try this solution use const constructor for child widget so it won't rebuild unless widget changed
class MyHomePage extends StatelessWidget {
  ValueNotifier<ScrollPhysics> canScroll =
      ValueNotifier(const BouncingScrollPhysics());

  MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NotificationListener(
        onNotification: (ScrollNotification notif) {
          if (notif is ScrollUpdateNotification) {
            if (canScroll.value.runtimeType == BouncingScrollPhysics &&
                notif.metrics.pixels > 100) {
              canScroll.value = const NeverScrollableScrollPhysics();
              debugPrint("End false");
            }
          }
          if (notif is ScrollEndNotification) {
            if (canScroll.value.runtimeType == NeverScrollableScrollPhysics) {
              debugPrint("End");
              Future.delayed(const Duration(milliseconds: 300),
                  () => canScroll.value = const BouncingScrollPhysics());

              debugPrint("End1");
            }
          }
          return true;
        },
        child: ValueListenableBuilder(
          valueListenable: canScroll,
          builder:
              (BuildContext context, ScrollPhysics scrollType, Widget? child) =>
                  CustomScrollView(
            physics: scrollType,
            slivers: [
              SliverToBoxAdapter(
                child: Container(
                  height: 200,
                  color: Colors.black,
                ),
              ),
              SliverToBoxAdapter(
                child: Column(
                  children: [
                    Container(
                      height: 100,
                      color: Colors.blue,
                    ),
                    Container(
                      height: 200,
                      color: Colors.grey,
                    ),
                    Container(
                      height: 200,
                      color: Colors.blue,
                    ),
                    Container(
                      height: 200,
                      color: Colors.grey,
                    ),
                    Container(
                      height: 200,
                      color: Colors.blue,
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
Share:
1,800
nicover
Author by

nicover

Updated on January 01, 2023

Comments

  • nicover
    nicover over 1 year

    I have multiple widget and lists within CustomScrollView and I would like to stop CustomScrollView to scroll while scrolling on some pixels bound condition.

    I can use NeverScrollPhysics() to stop it but I don't want to use setState() function here because the CustomScrollview content with lists is big enough to make the screen laggy while reloading on scroll.

    Also tried with Provider but the builder is providing only a child widget which is not working with sliver list.

    Here is the code using setState() :

                  NotificationListener(
                      onNotification: (ScrollNotification notif) {
                        if(notif is ScrollUpdateNotification) {
                          if (canScroll && notif.metrics.pixels > 100) {
                            canScroll = false;
                            setState(() {});
                          }
                        }
                        if(notif is ScrollEndNotification) {
                          if(!canScroll) {
                            canScroll = true;
                            setState(() {});
                          }
                        }
                        return true;
                      },
                      child: CustomScrollView(
                          shrinkWrap: true,
                          physics: canScroll ? BouncingScrollPhysics() : NeverScrollableScrollPhysics(), 
                          slivers: [
                            SliverToBoxAdapter(),                                              
                            List(),
                            List(),
                          ],
                        ),
                    ),
    

    Is there a way to reload only the CustomScrollView without its child ? Otherwise any workaround to prevent scrolling in this case ?

    Thanks for help

    • Benyamin
      Benyamin over 2 years
      use Stream instead of setState.
    • Tom3652
      Tom3652 over 2 years
      Did you mean StreamBuilder ? It's the same as Provider in this case, would reload the entire content of the CustomScrollView.
    • Mohammed Alfateh
      Mohammed Alfateh over 2 years
      You need a state management solution, such as bloc or riverpod.
    • nicover
      nicover over 2 years
      @7mada I'm already using Provider but it doesn't solve this
    • Mohammed Alfateh
      Mohammed Alfateh over 2 years
      I know Provider won't solve this problem, but RiverPod and Bloc can, if you want to use RiverPod I can write you an answer that will solve the problem.
    • nicover
      nicover over 2 years
      If i understand correctly, with Riverpod i am able to not reload items in CustomScrollView when it's rebuilt ? And only reload constructor param ? If yes, then please write an answer and I'll test . If it's working I'll migrate my project to RiverPod. Thanks for help
    • Mäddin
      Mäddin over 2 years
      Why do you use shrinkWrap: true? Without that line it should not be laggy with setState(...).
    • nicover
      nicover over 2 years
      @Mäddin I'm using this because shrinkWrap is killing the top BouncingPhysics but no at bottom . I need this. And I tried with and without there no difference
    • Mohammed Alfateh
      Mohammed Alfateh over 2 years
      Sorry for being late @nicover, I didn't get a reply notification :(. Any way I will write the answer now.
  • nicover
    nicover over 2 years
    I can't create const Constructor unfortunately . My lists contain users item with multiple param variable . Thanks for help
  • dhruvanbhalara
    dhruvanbhalara over 2 years
    @nicover above solution work mostly with less rebuild time can you give a try? And try to split slivers content with stateless & stateful widget
  • nicover
    nicover over 2 years
    I tried your code. Unfortunately I can't do to this in my use case . slivers item are rebuilt and my lists make the screen laggy on scroll. Anyway your answer is useful for another case
  • nicover
    nicover over 2 years
    Thanks a lot for this . Didn't know about Riverpod