Search code examples
flutterdartriverpod

Riverpod, reading state in outside BuildContext and Provider


I am struggling to figure out why this is not working (as opposed to the documentation which states it should be working).

I have a provider something like this

import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:putin_flutter_client/api/client.dart';
import 'package:putin_flutter_client/api/storage.dart';

final userProvider = StateNotifierProvider((_) => UserNotifier());

class UserNotifier extends StateNotifier<UserState> {
  UserNotifier() : super(UserState());

  set username(String username) {
    state = UserState(username: username, password: state.password, jwt: state.jwt);
    secureStorageWrite('username', username);
  }

  set password(String password) {
    state = UserState(username: state.username, password: password, jwt: state.jwt);
    secureStorageWrite('password', password);
  }

  set jwt(String jwt) {
    state = UserState(username: state.username, password: state.password, jwt: jwt);
    Client.jwt = jwt;
    secureStorageWrite('jwt', jwt);
  }

  String get jwt {
    return state.jwt;
  }

  Future<void> initState() async {
    final user = await UserState.load();
    state.username = user.username;
    state.password = user.password;
    state.jwt = user.jwt;
  }
}

class UserState {
  String username;
  String password;
  String jwt;

  UserState({
    this.username,
    this.password,
    this.jwt,
  });

  static Future<UserState> load() async {
    return UserState(
      username: await secureStorageRead('username'),
      password: await secureStorageRead('password'),
      jwt: await secureStorageRead('jwt'),
    );
  }
}

eventually deep in some widget something like this will update the state

// usilizing the setter on the provider to update the state...
user.jwt = data['token'];

now in some other part of the code I manage the http client. This obviously has no access to BuildContext etc. so I do the following to retrieve the jwt value from the stored state.

import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:putin_flutter_client/state/user.dart';

class Client extends http.BaseClient {
  final http.Client _client = http.Client();

  Future<http.StreamedResponse> send(http.BaseRequest request) {
    // Get the container as per riverpod documentation
    final container = ProviderContainer();
    // Access the value through the getter on the provider
    final jwt = container.read(userProvider).jwt;

    request.headers['user-agent'] = 'myclient::v1.0.0';
    request.headers['Content-Type'] = 'application/json';
    if (jwt != null) {
      request.headers['X-Auth-Token'] = jwt;
    }
    return _client.send(request);
  }
}

This is always null and the UserState is pretty much empty (all members are null).

In the riverpod documentation it says that this should be working

test('counter starts at 0', () {
  final container = ProviderContainer();

  StateController<int> counter = container.read(counterProvider);
  expect(counter.state, 0);
});

Can someone please help me out figure out what is wrong in my example above?


Solution

  • ProviderContainer() create a new instance of your providers it won't get the actual state. You need to make your client dependent of the user state like this :

    final clientProvider = Provider<Client>((ref){
        return Client(ref.watch(userProvider.state))
    });
    class Client extends http.BaseClient {
      Client(this._userState);
      final UserState _userState;
      final http.Client _client = http.Client();
    
      Future<http.StreamedResponse> send(http.BaseRequest request) {
        
        final jwt = _userState.jwt;
    
        request.headers['user-agent'] = 'myclient::v1.0.0';
        request.headers['Content-Type'] = 'application/json';
        if (jwt != null) {
          request.headers['X-Auth-Token'] = jwt;
        }
        return _client.send(request);
      }
    }
    

    when your user state will change the client will be re-instancied with new values

    If you don't want to re-instancie each time pass the read method instead :

    final clientProvider = Provider<Client>((ref){
        return Client(ref.read)
    });
    class Client extends http.BaseClient {
      Client(this._reader);
      final Reader _reader;
      final http.Client _client = http.Client();
    
      Future<http.StreamedResponse> send(http.BaseRequest request) {
        
        final jwt = _reader(userProvider.state).jwt;
    
        request.headers['user-agent'] = 'myclient::v1.0.0';
        request.headers['Content-Type'] = 'application/json';
        if (jwt != null) {
          request.headers['X-Auth-Token'] = jwt;
        }
        return _client.send(request);
      }
    }