Riverpod, состояние чтения вне BuildContext и Provider

Я изо всех сил пытаюсь понять, почему это не работает (в отличие от документации, в которой говорится, что это должно работать).

У меня провайдер примерно такой

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'),
    );
  }
}

в конечном итоге глубоко в каком-то виджете что-то вроде этого обновит состояние

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

теперь в какой-то другой части кода я управляю http-клиентом. Очевидно, что у него нет доступа к BuildContext и т. Д., Поэтому я делаю следующее, чтобы получить значение jwt из сохраненного состояния.

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);
  }
}

Это всегда ноль, а UserState почти пуст (все члены равны нулю).

В документации по Riverpod он говорит, что это должно работать

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

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

Может ли кто-нибудь помочь мне разобраться, что не так в моем примере выше?


person ptheofan    schedule 09.03.2021    source источник


Ответы (3)


ProviderContainer () создаст новый экземпляр ваших провайдеров, он не получит фактического состояния. Вам нужно сделать вашего клиента зависимым от состояния пользователя следующим образом:

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);
  }
}

когда ваше пользовательское состояние изменится, клиент будет повторно создан с новыми значениями

Если вы не хотите повторно создавать экземпляр каждый раз, вместо этого передайте метод чтения:

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);
  }
}
person moulte    schedule 10.03.2021
comment
ах, я понял, поэтому в моем виджете я могу вызвать final response = await context.read(clientProvider).post(url);. Нет ли способа чтения провайдера, подобного context.read (), вне дерева виджетов во флаттере? Это сделало бы все намного проще, проще и легковеснее, поскольку в этом случае, например, клиенту нужно только прочитать значение jwt при его вызове. На самом деле он не заботится о каждом его изменении (аналогично прецеденту buttonPressed - за исключением дерева / контекста виджетов) - person ptheofan; 10.03.2021
comment
теоретически вы можете поместить свой PorivderContainer в глобальную переменную, и он может работать, если вы не хотите создавать провайдера: Final container = ProviderContainer (); runApp (UncontrolledProviderScope (контейнер: контейнер, дочерний элемент: MaterialApp (главная: MainView (),),),); - person moulte; 10.03.2021
comment
но более чистое решение, если вы не хотите перестраивать своего клиента каждый раз, когда вы можете передать читателю ссылки на поставщика, я обновлю ответ, чтобы показать вам - person moulte; 10.03.2021
comment
Переходя в ридер напрямую? Хорошая идея xD - person ptheofan; 10.03.2021
comment
Таким образом, невозможно фактически прочитать значение поставщика, не передав BuildContext каким-либо образом в цепочке, верно? Я надеялся на что-то в направлении var jwt = userProvider.state.jwt; // outside the widget tree when the caller does not know the BuildContext - person ptheofan; 10.03.2021

Как отметил @moulte (действительно спасибо), может получить доступ к поставщикам как к глобальным переменным и независимо от контекста, создав экземпляр снаружи и вставив его в область виджета через UncontrolledProviderScope. Важная часть - не забыть избавиться от глобального поставщика до того, как приложение завершится, иначе оно никогда не завершится. Вот пример кода

/// 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);
  }
}
person ptheofan    schedule 10.03.2021

ProviderContainer () предназначен для использования RiverPod в Dart. Эквивалент во Flutter - ProviderScope (), но для этого требуется доступ через цепочку контекста виджета, аналогично пакету провайдера.

person Randal Schwartz    schedule 10.03.2021