Search code examples
flutterflutter-providerstatefulwidgetstatelesswidget

Flutter StatelessWidget and Provider not updating or calling build unnecessarily


I'm trying to use Provider with a Stateless widget page to update a countdown UI.

When I run all the below, I get both print messages in the console on each 1 second tick of the stopwatch:

e.g.

flutter: build()
flutter: _buildCountDown()

Also, the remaining value doesn't actually update in the UI.

But if I:

  • make the page a StatefulWidget
  • replace the variable access with Consumer<CountdownProvider> syntax e.g.
return Consumer<CountdownProvider>(
  builder: (_, countdownProvider, __) {
    return Text(countdownProvider.remaining.toString()),
});

...everything works as expected: the UI updates properly and I don't get the build() printout, just the _buildCountDown(), which is what I wanted.

Q) What am I doing wrong that means I can't make this a Stateless widget that updates as expected?


  1. main.dart I have the Provider like so:
return MultiProvider(
  providers: [        
    ChangeNotifierProvider(
      create: (_) => CountdownProvider(),
    ),
  ],
)
  1. I have written a simple countdown provider - CountdownProvider:
class CountdownProvider extends ChangeNotifier {

  int remaining = 0;
  final Stopwatch _stopwatch = Stopwatch();
  late Timer _timer;

  void start(int val) {
    remaining = val;
    _timer = Timer.periodic(const Duration(seconds: 1), _onTick);
    _stopwatch.start();
  }

  void _onTick(Timer timer) {
    remaining--;
    if (remaining < 0) {
      return;
    }
    notifyListeners();
  }

  void stop(bool skipNotify) {
    _timer.cancel();
    _stopwatch.stop();

    if (skipNotify) {
      _stopwatch.reset();
      remaining = 0;
      return;
    }
    notifyListeners();
  }
}
  1. Page to display it: PageCountdown:
class PageCountdown extends StatelessWidget {
  const PageCountdown({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 10 seconds default countdown
    Provider.of<CountdownProvider>(context, listen: false).start(10);

    print("build()");
    return Scaffold(
      body: _buildContent(context),
    );
  }

  Widget _buildContent(BuildContext context) {
    return SafeArea(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          _buildCountDown(context),
        ],
      ),
    );
  }

  Widget _buildCountDown(BuildContext context) {
    int remaining = Provider.of<CountdownProvider>(context).remaining;

    print("_buildCountDown()");

    return Text(
      remaining.toString(),
    );
  }
}


Solution

  • PageCountdown is the single widget, which is rebuilt when the counter ticks. start(10) in the beginning of build will reset remaining to 10 every rebuild. Something like this will work:

    class PageCountdown extends StatelessWidget {
      const PageCountdown({
        Key? key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        // 10 seconds default countdown
        Provider.of<CountdownProvider>(context, listen: false).start(10);
    
        print("build()");
        return const Scaffold(
          body: Countdown(),
        );
      }
    }
    
    class Countdown extends StatelessWidget {
      const Countdown({
        Key? key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return SafeArea(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              _buildCountDown(context),
            ],
          ),
        );
      }
    
      Widget _buildCountDown(BuildContext context) {
        int remaining = Provider.of<CountdownProvider>(context).remaining;
    
        print("_buildCountDown()");
    
        return Text(
          remaining.toString(),
        );
      }
    }