flutterflutter-change-notifier

ValueListenableBuilder not triggering when setting the same value in Flutter


In the following Flutter code, a ValueListenableBuilder is used to show a dialog when a ValueNotifier changes. However, when the same value is set again, the dialog is not triggered.

ValueNotifier is only notifying listeners when the value changes. The goal is to trigger the dialog even if the same value (true) is set multiple times.

How can the dialog be triggered even if the same value (true) is set again to adWatchNeeded?

Lets assume a simple view model class like below, OR simple try it on here

Important: No state management solutions needed like bloc, riverpod etc. This is just for experimental purpose

class FeatureViewModel extends ChangeNotifier {
  ValueNotifier<bool> adWatchNeeded = ValueNotifier(false);

  void useFeature() {
    // Some API calls..
    // Unfortunately user must watch ads to use feature..
    adWatchNeeded.value = true;
  }
}

Then use on UI side

  Widget _buildAdWatchNeededDialog() {
    return ValueListenableBuilder<bool>(
      valueListenable: _viewModel.adWatchNeeded,
      builder: (context, adWatchNeeded, child) {
        WidgetsBinding.instance.addPostFrameCallback((_) {
          if (adWatchNeeded) {
            showAdWatchNeededDiaog(context);
          }
        });
        return const SizedBox.shrink();
      },
    );
  }

Then use it on tree

class SomeFeatureWidget extends StatefulWidget {
  const SomeFeatureWidget({Key? key}) : super(key: key);

  @override
  State<SomeFeatureWidget> createState() => _SomeFeatureWidgetState();
}

class _SomeFeatureWidgetState extends State<SomeFeatureWidget> {
  final _viewModel = FeatureViewModel();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        _buildFeatureUsageDialog(),
        _buildAdWatchNeededDialog(),
      ],
    );
  }

  Widget _buildFeatureUsageDialog() {
    return TextButton(onPressed: () => _viewModel.useFeature(), child: const Text('Use Feature'));
  }

  Widget _buildAdWatchNeededDialog() {
    return ValueListenableBuilder<bool>(
      valueListenable: _viewModel.adWatchNeeded,
      builder: (context, adWatchNeeded, child) {
        WidgetsBinding.instance.addPostFrameCallback((_) {
          if (adWatchNeeded) {
            showAdWatchNeededDiaog(context);
          }
        });
        return const SizedBox.shrink();
      },
    );
  }

  void showAdWatchNeededDiaog(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          content: const Text('Wath ads to use this feature'),
          actions: [
            TextButton(
                onPressed: () {
                  Navigator.of(context).pop();
                },
                child: const Text('Watch Ad')),
            TextButton(
                onPressed: () {
                  Navigator.of(context).pop();
                },
                child: const Text('No, thanks'))
          ],
        );
      },
    );
  }
}

Solution

  • the easiest way is to write your own version of ValueNotifier

    just copy the original sources of ValueNotifier from here (it's ~25 lines of code) and simply remove the if that checks if a new value is different that the old one in set value(T newValue) setter

    it should look, more or less, something like this:

    class MySuperDuperValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
      MySuperDuperValueNotifier(this._value) {
        if (kFlutterMemoryAllocationsEnabled) {
          ChangeNotifier.maybeDispatchObjectCreation(this);
        }
      }
    
      @override
      T get value => _value;
      T _value;
      set value(T newValue) {
        _value = newValue;
        notifyListeners();
      }
    
      @override
      String toString() => '${describeIdentity(this)}($value)';
    }