Search code examples
flutterprovider

Why won't Provider rerun its create() when its parent StatelessWidget is re-running build()?


I know the more common ways to handle this kind of a thing involve using a ChangeNotifierProvider(CNP), etc., but I want to understand how Provider actually works.

If I have a StatelessWidget with a Provider somewhere in its widget tree descendents, and that StatelessWidget gets recreated (i.e., its build() method runs again), I would expect a new Provider to be created. So, if the Provider is being created from a value that has been passed to that StatelessWidget, I would expect a new Provider to be created with that new value. Then, any downstream widgets that are using Provider.of<T>(context, listen:true) would update...I would think.

...But that doesn't happen. When that parent StatelessWidget is rebuilt with a new input value, the Provider doesn't rebuild itself. Why not?

Again, I'm not saying this would be a great approach, I just want to understand why this is happening like this.

Here is some basic code to show what I mean: Here it is in DartPad.

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

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: CounterUpper(),
        ),
      ),
    );
  }
}

class _CounterUpperState extends State<CounterUpper>
    with TickerProviderStateMixin {
  Ticker? ticker;
  double animationValue = 0;

  @override
  void initState() {
    print('Running _CenterUpperState.initState()');
    super.initState();
    ticker = Ticker((Duration elapsed) {
      if (elapsed.inSeconds - animationValue > 1) {
        setState(() {
          print(
              'Running _CenterUpperState.setState() with animationValue: $animationValue and elapsed.inSeconds: ${elapsed.inSeconds}');
          animationValue = elapsed.inSeconds.toDouble();
        });
      }
      if (elapsed.inSeconds > 5) {
        ticker?.stop();
      }
    });
    ticker!.start();
  }

  @override
  Widget build(BuildContext context) {
    print(
        'Running _CenterUpperState.build() with animationValue: $animationValue');
    return ProviderHolder(animationValue: animationValue);
  }
}

class ProviderHolder extends StatelessWidget {
  const ProviderHolder({super.key, required this.animationValue});

  final double animationValue;

  @override
  Widget build(BuildContext context) {
    print(
        'Running ProviderHolder.build() with animationValue: $animationValue');
    return Provider<ValueWrapper>(
      create: (_) {
        print('Running Provider.create() with animationValue: $animationValue');
        return ValueWrapper(animationValue);
      },
      child: ContentHolder(),
    );
  }
}

class ContentHolder extends StatelessWidget {
  const ContentHolder({super.key});

  @override
  Widget build(BuildContext context) {
    final double providerValue =
        Provider.of<ValueWrapper>(context, listen: true).value;
    print('Running ContentHolder.build() with providerValue: $providerValue');
    return Text(providerValue.toString());
  }
}

class ValueWrapper {
  const ValueWrapper(this.value);

  final double value;
}

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

  @override
  _CounterUpperState createState() => _CounterUpperState();
}

I've tried re-arranging this many different ways (different versions of the parent widget including StatefulWidget). I understand that the most common way to do this would be to use a Provider of some object that can itself be manipulated. Something like a basic class that contains an integer as well as some methods to alter the value of that integer. Then, instead of re-creating the parent widget, you would just use those methods to change the value.

But still...why doesn't this work? It is acting like the Provider is defined as const even though it has been passed a value that is not const.


Solution

  • Provider doesn't call create again on rebuild, it has it's own handling to look at the build context that you give it to find a state object and re-use it. as you had already deduced from your own logging, the create log only gets called once, this is how the create function is handled inside of provider (Even though rebuild runs, provider chooses to ignore your create script as to not recreate objects that already exist, this prevents expensive recreation on a value that already exists).

    As with programming that leaves a few options,

    1. Handle the rebuild of the ProviderHolder to dispose entirely of the Provider everytime that it rebuilds, this will cause children to rebuild and the children to grab a new copy from the value provider. This is inneficient because in memory you are destroying the whole object and recreating it. Plus any initialization it may invoke in larger classes.
    2. Don't use provider, if you're doing something small like updating an animation value down 2 levels of your app, just pass the values through manually, it might look messy having a value passed directly but it will be more maintainable than an entire state object. Especially for localized animation items, unless you are doing something quite complex.
    3. As you already knew about, move your logic into your provider so that you can have 1 state object that gets created once, updates its own paramaters and then sends the build instruction to the widgets that needs it based on who is listening to updates with a change notifier.

    Hope this makes sense, as with any package, you have to understand although that some choices may not make sense to you as you'd rather it destroy it's previous state and rebuild from scratch, optimizations may be made to instead assist the general public who already have a provider and want it's own state to remain unchanged. Hence why worded as oncreate to avoid confusion that it is getting rebuilt on every local state change.

    Options 3 here is what I would do, something like this (done quickly so could be optimized, however it works as a rudimentary example):

    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    import 'package:flutter/scheduler.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return const MaterialApp(
          debugShowCheckedModeBanner: false,
          home: Scaffold(
            body: Center(
              child: CounterUpper(),
            ),
          ),
        );
      }
    }
    
    class CounterUpper extends StatefulWidget {
      const CounterUpper({super.key});
    
      @override
      State<CounterUpper> createState() => _CounterUpperState();
    }
    
    class _CounterUpperState extends State<CounterUpper> with TickerProviderStateMixin {
      @override
      Widget build(BuildContext context) {
        return ChangeNotifierProvider<ValueWrapper>(create: (_) => ValueWrapper(), child: const ContentHolder());
      }
    }
    
    class ContentHolder extends StatelessWidget {
      const ContentHolder({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Text(Provider.of<ValueWrapper>(context, listen: true).animationValue.toString());
      }
    }
    
    class ValueWrapper with ChangeNotifier {
      ValueWrapper() : animationValue = 0 {
        ticker = Ticker((Duration elapsed) {
          if (elapsed.inSeconds - animationValue > 1) {
            updateAnimationValue(elapsed.inSeconds.toDouble());
            animationValue = elapsed.inSeconds.toDouble();
          }
          if (elapsed.inSeconds > 5) {
            ticker.stop();
          }
        });
        ticker.start();
      }
    
      double animationValue;
      late Ticker ticker;
    
      void updateAnimationValue(double value) {
        animationValue = value;
        notifyListeners();
      }
    }