Initializing riverpod Provider with a custom ChangeNotifier

Issue

I’m just trying out the new river_pod, flutter state management library. My goal here is simple. GestureDetector in the main page listens to vertical drags and updates the animation controller accordingly. And I’d like to listen to this animation somewhere else. I have written the following code, and it’s working as expected. But I don’t feel like I’m initializing the provider in the right way.

// a custom notifier class
class AnimationNotifier extends ChangeNotifier {
  final AnimationController _animationController;

  AnimationNotifier(this._animationController) {
    _animationController.addListener(_onAnimationControllerChanged);
  }

  void forward() => _animationController.forward();
  void reverse() => _animationController.reverse();

  void _onAnimationControllerChanged() {
    notifyListeners();
  }

  @override
  void dispose() {
    _animationController.removeListener(_onAnimationControllerChanged);
    super.dispose();
  }

  double get value => _animationController.value;
}

// provider variable, (not initialized here)
var animationProvider;

// main Widget
class GestureControlledAnimationDemo extends StatefulWidget {
  @override
  _GestureControlledAnimationDemoState createState() =>
      _GestureControlledAnimationDemoState();
}

class _GestureControlledAnimationDemoState
    extends State<GestureControlledAnimationDemo>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;

  double get maxHeight => 420.0;

   @override
  void initState() {
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    );
    // provider is initialized here
    animationProvider = ChangeNotifierProvider((_) {
      return AnimationNotifier(_controller);
    });
    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return CustomScaffold(
      title: 'GestureControlled',
      body: GestureDetector(
        onVerticalDragUpdate: _handleDragUpdate,
        onVerticalDragEnd: _handleDragEnd,
        child: Container(
          color: Colors.red,
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  'Yo',
                  style: TextStyle(color: Colors.white),
                ),
                NotifierTest(),
              ],
            ),
          ),
        ),
      ),
    );
  }

  void _handleDragUpdate(DragUpdateDetails details) {
    _controller.value -= details.primaryDelta / maxHeight;
  }

  void _handleDragEnd(DragEndDetails details) {
    if (_controller.isAnimating ||
        _controller.status == AnimationStatus.completed) return;

    final double flingVelocity =
        details.velocity.pixelsPerSecond.dy / maxHeight;
    if (flingVelocity < 0.0) {
      _controller.fling(velocity: max(2.0, -flingVelocity));
    } else if (flingVelocity > 0.0) {
      _controller.fling(velocity: min(-2.0, -flingVelocity));
    } else {
      _controller.fling(velocity: _controller.value < 0.5 ? -2.0 : 2.0);
    }
  }
}

// Widget which uses the provider
class NotifierTest extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final animationNotifier = useProvider(animationProvider);
    double count = animationNotifier.value * 1000.0;
    return Container(
      child: Text(
        '${count.floor()}',
        style: TextStyle(color: Colors.white),
      ),
    );
  }
}

Since an animation controller instance is required to create an instance of AnimationNotifier, this can be done only after _controller initialization. So in the initState(), I’ve initialized both _controller and animationProvider. Is this the right way to use riverpod Provider?
If not, what modifications can be made?

Solution

First off I would highly recommend using hooks – it would reduce the boilerplate of your code significantly, for example, your class declaration will turn into:


class GestureControlledAnimationDemo extends HookWidget {
  double get maxHeight => 420.0;

  @override
  Widget build(BuildContext context) {
    final _controller = useAnimationController(duration: Duration(seconds: 1));
    ...
  }

This removes the need for initState, dispose, etc.

Second, you don’t necessarily want to create non-static providers inside classes. Instead, you could create it in global scope, or in this case, it makes sense to add as a static member on your custom notifier.

class AnimationNotifier extends ChangeNotifier {
  ...
  static final provider = ChangeNotifierProvider((_) {
    return AnimationNotifier(controller);
  });
}

But wait, we don’t have any variable named controller in this scope, so how do we get access? We can create a provider for an AnimationController, or we can turn your provider into a family so we can accept an AnimationController as a parameter. I will demonstrate the approach with families:

class AnimationNotifier extends ChangeNotifier {
  ...
  static final provider = ChangeNotifierProvider.autoDispose.family<AnimationNotifier, AnimationController>((_, AnimationController controller) {
    return AnimationNotifier(controller);
  });
}

I added autoDispose as you likely want your controllers disposed when they are no longer needed. Now, we use the provider:


class GestureControlledAnimationDemo extends HookWidget {
  double get maxHeight => 420.0;

  @override
  Widget build(BuildContext context) {
    final controller = useAnimationController(duration: Duration(seconds: 1));
    final provider = useProvider(AnimationNotifier.provider(controller));
    ...
  }

If you do use hooks, make sure you change your riverpod dependency to hooks_riverpod.

EDIT:

It looks like for your use case you could potentially store the current controller in a StateProvider, then read it from the ChangeNotifierProvider instead of using families.

final controllerProvider = StateProvider<AnimationController>((_) => null);

class AnimationNotifier extends ChangeNotifier {
  ...
  static final provider = ChangeNotifierProvider.autoDispose<AnimationNotifier>((ref) {
    final controller = ref.read(controllerProvider)?.state;
    return AnimationNotifier(controller);
  });
}

class GestureControlledAnimationDemo extends HookWidget {
  double get maxHeight => 420.0;

  @override
  Widget build(BuildContext context) {
    final controller = useAnimationController(duration: Duration(seconds: 1));

    final currentController = useProvider(controllerProvider);
    currentController.state = controller;
    
    final notifier = useProvider(AnimationNotifier.provider);
    ...
  }

This should work. Note that when Riverpod 0.6.0 is released, you can also autodispose the StateProvider.

Answered By – Alex Hartford

Answer Checked By – Gilberto Lyons (FlutterFixes Admin)

Leave a Reply

Your email address will not be published. Required fields are marked *