13

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?

ptheofan
  • 2,150
  • 3
  • 23
  • 36

3 Answers3

6

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);
  }
}
moulte
  • 521
  • 3
  • 5
  • ah, I get it, so then in my widget I can call `final response = await context.read(clientProvider).post(url);`. Isn't there some way of reading the provider similar to context.read() outside the widget tree in flutter? That would make things so much simpler, easier and lightweight since in this case for example, the Client only needs to read the jwt value when it called. It doesn't really care to get every change of it (similar to the buttonPressed usecase - except outside the widget tree / context) – ptheofan Mar 10 '21 at 08:10
  • in theory you can put your PorivderContainer in a global variable and it could work if you don't want to create a provider: Final container = ProviderContainer(); runApp( UncontrolledProviderScope( container: container, child: MaterialApp( home: MainView(), ), ), ); – moulte Mar 10 '21 at 08:14
  • but a cleaner solution if you don't want to rebuild your client each time you can pass the reader of the provider reference i'll update the answer to show you – moulte Mar 10 '21 at 08:17
  • Passing in the reader directly? Nice idea xD – ptheofan Mar 10 '21 at 08:35
  • 1
    So there is no way of actually reading a provider value without having passed BuildContext somehow in the chain right? I was hoping for something in the direction of `var jwt = userProvider.state.jwt; // outside the widget tree when the caller does not know the BuildContext` – ptheofan Mar 10 '21 at 08:40
  • This is the correct answer. You should always be able to obtain a ref and use it to read it something. ProviderContainer is not made for those sort of things. – Rémi Rousselet Dec 01 '21 at 18:06
5

As @moulte pointed out (really thanks) can access the providers as global variables and independent of context by instantiating outside and injecting it to the widget scope via UncontrolledProviderScope. The important part is to remember to dispose the global provider before the app terminates or it will never really terminate. Here's an example code

/// file /state/container.dart
import 'package:hooks_riverpod/hooks_riverpod.dart';
final container = ProviderContainer();

/// file /main.dart
void main() async {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyApp createState() => _MyApp();
}

class _MyApp extends State<MyApp> {

  @override
  void dispose() {
    super.dispose();
    // disposing the globally self managed container.
    container.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return UncontrolledProviderScope(container: container,
      child: MaterialApp(
      // The usual widget tree
    );
  }
}

/// Somewhere in a file that is not aware of the BuildContext
/// here's how client.dart accesses the provider
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:putin_flutter_client/state/container.dart';
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) {
    // Simply accessing the global container and calling the .read function
    var jwt = container.read(userProvider.state).jwt;
    request.headers['user-agent'] = 'putin_flutter::v1.0.0';
    request.headers['Content-Type'] = 'application/json';
    if (jwt != null) {
      request.headers['X-Auth-Token'] = jwt;
    }
    return _client.send(request);
  }
}
ptheofan
  • 2,150
  • 3
  • 23
  • 36
2

ProviderContainer() is meant for using RiverPod in Dart. The equivalent in Flutter is ProviderScope(), but that requires access by the widget context chain, similar to the provider package.

Randal Schwartz
  • 39,428
  • 4
  • 43
  • 70