Search code examples
flutterdartriverpodstate-management

How to continuously access updated state of a StateNotifier within a service class in Flutter using Riverpod?


I am using Riverpod in my Flutter project and have set up a StateNotifier to manage my state. However, I have run into a challenge when trying to access the updated state of the StateNotifier from within a service class, especially when the state changes over time.

Here’s a simplified version of my setup:

final myNotifierProvider = StateNotifierProvider<MyNotifier, MyState>((ref) {
  return MyNotifier();
});

class MyService {
  // I want to be able to access the updated state of myNotifierProvider
  // from within this service class, even when the state changes over time.
  
  void doSomething() {
    // Access and react to the updated state of myNotifierProvider here.
  }
}

I have considered passing the current state of the StateNotifier to the function within the service each time I call it. While this method works, it requires passing the state every time a function is called, which doesn't seem like the most efficient or clean solution, especially if the state changes frequently over time.

class MyService {
  void doSomething(MyState state) {
    // Access and react to the passed state here.
  }
}

I've also considered passing a ProviderReference (ref) to the service methods or constructors, but I'm uncertain if this is a recommended practice.

class MyService {
  final ProviderReference ref;
  
  MyService(this.ref);
  
  void doSomething() {
    final state = ref.read(myNotifierProvider);
    // Access and react to the state here.
  }
}

I am looking for a more idiomatic or clean solution that would allow me to access the updated state within the service, even as it changes over time, without having to pass the state or ref every time.

Has anyone encountered this situation and found a clean or idiomatic solution using Riverpod? Any insights or recommendations would be greatly appreciated!


Solution

  • Your answer lies in the plane of the first paragraph:

    ..., I have run into a challenge when trying to access the updated state of the StateNotifier from within a service class, ...

    This means that your service is becoming less and less like a “regular stupid service with methods and fields”, and more like a full-fledged notifier that depends on some state.

    Before giving a full answer, it is worth saying that StateNotifierProvider is deprecated, and that NotifierProvider should be used instead (or AsyncNotifierProvider for easy management of asynchronous state).

    And this is how I see it:

    1. Your service is Provider:

    final myNotifierProvider = StateNotifierProvider<MyNotifier, MyState>((ref) {
      return MyNotifier();
    });
    
    
    final myServiceProvider = Provider<MyService>((ref) {
      final myNotifierState = ref.watch(myNotifierProvider);
      return MyService(myNotifierState);
    });
    
    class MyService {
      MyService(this.myNotifierState);
    
    
      // todo: make private
      final MyState myNotifierState;
      
      void doSomething() {
        // Always have access to the latest myNotifierState
      }
    }
    

    On the one hand, MyService is still a regular service class. It's easy to test, easy to call its methods. And it is always up to date. You also can:

    • call doSomething either manually at any time. MyState data will always be up to date.
    • hang the listener on the state. The ref.listen listener is hung inside myServiceProvider, and based on the specific state (or always) any method is called. You don't even have to drag the myNotifierState dependency inside the class. The same thing can be done by calling doSomething inside the MyService constructor depending on the state. This is the kind of reactivity that results.

    2. Your service is NotifierProvider:

    I’ll say right away that this approach may be semantically incorrect, because Notifier requires some kind of state. I'll show it anyway so you can use this example to rework your myNotifierProvider:

    final myNotifierProvider = StateNotifierProvider<MyNotifier, MyState>((ref) {
      return MyNotifier();
    });
    
    
    final myServiceProvider = NotifierProvider<MyService, void>(MyService.new);
    });
    
    class MyService extends Notifier<void> {
    
      MyState _myNotifierState;
    
      @override
      void build() {
        // it is safe to call `ref.watch` or `ref.listen` here
        _myNotifierState = ref.watch(myNotifierProvider);
        
        // or
        ref.listen(myNotifierProvider, 
          (prev, next) {
            /* do something, for example, call the method doSomething */
          });
        return;
      }
    
      void doSomething() {
        // Always have access to the latest _myNotifierState
      }
    }
    

    MyService has now become more tied into the riverpod framework, which may have difficulties with testability. And the semantics of Notifier tells us that MyService. We get around this by using void, and saying "we have no state".


    Whatever you choose as a solution, if your service depends directly on the state and must respond in every possible way to its change - you need to use the methods above.

    If you just need to sometimes call the doSomething method, then simply pass the necessary data for its operation as parameters. This is the best option, and the ..Service postfix seems justified.