Search code examples
flutterdartflutter-change-notifierchangenotifierflutter-change-notifier-provider

ChangeNotifierProvider not updating the listener


I have simplified my real app into the following code below to show the problem. I am basically trying to show the scroll offset of a list view on another widget (the real app is a lot more complex where the widget does other stuff with that offset).

I am returning the ViewController() as the body of my Scaffold in the main.dart. In my ViewController below, I have a stack which contains a ListView and a Text. I want the controller for the ListView to update the Text using the ChangeNotifierProvider. It doesn't work for some reason.

My ChangeNotifier is the scrollProvider which has been declared as a variable of the entire ViewController and I am passing the same scrollProvider into the ChangeNotifierProvider and also printing from the same.

What am I doing wrong?

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class ViewController extends StatefulWidget {
  const ViewController({super.key});

  @override
  State<ViewController> createState() => _ViewControllerState();
}

class _ViewControllerState extends State<ViewController> {
  final scrollController = ScrollController();
  final scrollProvider = ScrollProvider();

  @override
  void initState() {
    scrollController.addListener(scrollHandler);
    super.initState();
  }

  @override
  void dispose() {
    scrollController.removeListener(scrollHandler);
    super.dispose();
  }

  void scrollHandler() {
    scrollProvider.setCurrentOffset(scrollController.offset);
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        ListView.builder(
          controller: scrollController,
          itemBuilder: ((context, index) {
            return Text(
              "Index: $index",
              style: const TextStyle(fontSize: 40),
            );
          }),
          itemCount: 100,
        ),
        Positioned(
          left: 0,
          bottom: 0,
          right: 0,
          child: CupertinoButton(
            onPressed: (() {}),
            child: ChangeNotifierProvider(
              create: ((_) => scrollProvider),
              child: Text(
                "Offset: ${scrollProvider.currentOffset}",
                // "Offset: ${Provider.of<ScrollProvider>(context, listen: false).currentOffset}",
                style: const TextStyle(
                  fontSize: 32,
                  fontWeight: FontWeight.bold,
                  backgroundColor: Colors.black,
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

class ScrollProvider extends ChangeNotifier {
  double? currentOffset;
  void setCurrentOffset(valueToSet) {
    if (currentOffset != valueToSet) {
      currentOffset = valueToSet;
      print("currentOffset: $currentOffset");
      notifyListeners();
    }
  }
}

Solution

  • You need to take care of two things:

    1. If you want to create a provider from an already existing value, use the ChangeNotifierProvider.value named constructor.

    2. Wrap your Text widget into a Consumer<ScrollProvider> so that it gets notified when currentOffset is changed.

    (And always call super.initState in initState in the first line and do your own logic afterwards.)

    With some modifications in your code this is an example of how to get it working:

    import 'package:flutter/material.dart';
    import 'package:flutter/cupertino.dart';
    import 'package:provider/provider.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
      @override
      Widget build(BuildContext context) {
        return const MaterialApp(
          home: Scaffold(
            body: Center(
              child: SafeArea(child: ViewController()),
            ),
          ),
        );
      }
    }
    
    class ViewController extends StatefulWidget {
      const ViewController({super.key});
    
      @override
      State<ViewController> createState() => _ViewControllerState();
    }
    
    class _ViewControllerState extends State<ViewController> {
      final scrollController = ScrollController();
      final scrollProvider = ScrollProvider();
    
      @override
      void initState() {
        super.initState();
        scrollController.addListener(scrollHandler);
      }
    
      @override
      void dispose() {
        scrollController.removeListener(scrollHandler);
        super.dispose();
      }
    
      void scrollHandler() {
        scrollProvider.setCurrentOffset(scrollController.offset);
      }
    
      @override
      Widget build(BuildContext context) {
        return Stack(
          children: [
            ListView.builder(
              controller: scrollController,
              itemBuilder: ((context, index) => Text(
                    "Index: $index",
                    style: const TextStyle(fontSize: 40),
                  )),
              itemCount: 100,
            ),
            Positioned(
              left: 0,
              bottom: 0,
              right: 0,
              child: CupertinoButton(
                onPressed: (() {}),
                child: ChangeNotifierProvider.value(
                  value: scrollProvider,
                  child: Consumer<ScrollProvider>(
                    builder: (context, value, child) => Text(
                      "Offset: ${value.currentOffset}",
                      style: const TextStyle(
                        fontSize: 32,
                        fontWeight: FontWeight.bold,
                        backgroundColor: Colors.black,
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ],
        );
      }
    }
    
    class ScrollProvider extends ChangeNotifier {
      double? currentOffset;
      void setCurrentOffset(valueToSet) {
        if (currentOffset != valueToSet) {
          currentOffset = valueToSet;
          print("currentOffset: $currentOffset");
          notifyListeners();
        }
      }
    }