When we need to read (not watch) a provider inside another one the documentation is clear:
"DON'T CALL READ INSIDE THE BODY OF A PROVIDER"
final myProvider = Provider((ref) {
// Bad practice to call `read` here
final value = ref.read(anotherProvider);
});
And it suggest to pass to the value exposed the Reader function: https://riverpod.dev/docs/concepts/combining_providers#can-i-read-a-provider-without-listening-to-it
final userTokenProvider = StateProvider<String>((ref) => null);
final repositoryProvider = Provider((ref) => Repository(ref.read));
class Repository {
Repository(this.read);
/// The `ref.read` function
final Reader read;
Future<Catalog> fetchCatalog() async {
String token = read(userTokenProvider);
final response = await dio.get('/path', queryParameters: {
'token': token,
});
return Catalog.fromJson(response.data);
}
}
And that's ok, but what is the best practice when I need to read a provider inside a FutureProvider?
I find myself in this situation many time because I expose the api as providers and inside the FutureProvider I call watch to get the api I need.
But I noticed that, because I'm watching the Api provider inside the userProvider, this won't gets disposed after been used.
Here's an example of what I'm trying to say:
API CLASS
final userApiProvider = Provider((ref) => UserApi(ref.read));
class UserApi {
final Dio _dio;
const UserApi(Reader read):
_dio = read(dioProvider);
Future<Response> getUser(String id, { CancelToken? cancelToken }) async{
final _url = '$URL_TO_API/$id';
return _dio.get(_url, cancelToken: cancelToken);
}
}
When using the API inside a FutureProvider
final userProvider = FutureProvider.autoDispose.family<User, int>((ref, userId) async {
final userApi = **ref.watch(userApi);**
final cancelToken = CancelToken();
ref.onDispose(() { cancelToken.cancel(); });
final user = await userApi.getUser(cancelToken: cancelToken);
return user;
});
The same logic applies.
By "Don't use read inside a provider", it isn't talking about the class Provider specifically, but any provider – so FutureProvider included.
In general, you should avoid using read as much as possible.
Related
Note: I've seen similar questions all over Stackoverflow, but none of them exactly answer this.
Context:
I have an authentication cubit that has a variety of user states:
/// User exists, includes user's access and refresh tokens.
class User extends AuthenticationState {
final Tokens? tokens;
final bool justRegistered;
final bool tokensAvailable;
User({
required this.tokens,
this.justRegistered = false,
this.tokensAvailable = true,
});
#override
List<Object?> get props => [tokens, justRegistered, tokensAvailable];
}
/// No authenticated user exists.
class NoUser extends AuthenticationState {}
/// User is currently being registered or signed in.
class UserLoading extends AuthenticationState {}
Problem:
I have a Posts Cubit that needs to manage API calls and pass in the current user's access token to them in order to work. However, this access token is only stored inside the Authentication Cubit's state (in the User state, inside the tokens variable).
Is there a (good practice) way to get this token from the Authentication Cubit's User state inside the Post Cubit so I can use it to send API requests that require tokens for authentication to return posts?
Thanks!
You can create a class something like API Provider and authenticateService, pass and use in your Cubit to call API :
For example:
Future<Response<dynamic>> _callAndRetryDelete(
String url, {
int validStatus,
int retries,
}) async =>
retry(
() async {
final Map<String, String> authHeaders = await _authenticationService.getAuthHeaders;
if (authHeaders == null) {
throw ApiError('Could not get authorization', endpoint: url, method: HTTPMethod.Delete);
}
_client.options = _options(success: validStatus, authHeaders: authHeaders);
try {
return await _client.delete(url);
} on DioError catch (error) {
final String message = _error(error);
throw ApiError(message, endpoint: url, method: HTTPMethod.Delete);
}
},
maxAttempts: retries,
);
AuthenticaionService.class
Future<void> _doLogin(Map<String, dynamic> params) async {
final Response<Map<String, dynamic>> response =
await _handler.post(_loginEndpoint, body: params,
validStatus: 200);
final Map<String, String> res =
response?.data?.map((String key, dynamic value) => MapEntry<String, String>(key, value.toString()));
final Authentication authentication = Authentication.parse(res);
await _parseAndStore(authentication);
return authentication;}
AuthCubit:
Future<void> signIn() async => cubitAction(
action: () async {
emit(AuthenticationLoading());
await _authenticationService.signIn(payload);
emit(AuthenticationDone);
}
);
I'm trying to use Riverpod for my project, however I'm hitting some issues.
I am not sure that I'm using it very well so don't hesitate to tell me if you see anything wrong with it :)
First I have my authProvider:
final authRepoProvider = ChangeNotifierProvider.autoDispose((ref) {
return AuthRepository();
});
class AuthRepository extends ChangeNotifier {
String? token;
Future signIn(String username, String password) async {
// Do the API calls...
token = tokenReturnedByAPI;
notifyListeners();
}
}
Then I have a service, let's say it allows to fetch blog Articles, with a stream to get live update about those.
class ArticleService {
StreamController<Article> _streamCtrl;
String? _token;
API _api;
ArticleService(this._api) : _streamCtrl = StreamController<Article>() {
_api.onLiveUpdate((liveUpdate) {
_streamCtrl.add(liveUpdate);
});
}
Stream<Article> get liveUpdates => _streamCtrl.stream;
Future markArticleAsRead(String id) async {
await _api.markAsRead(_token, id);
}
}
For that article service I would like to keep the current token up to date, but I don't want to rebuild the entire service every time the token changes as there are listeners and streams being used.
For that I would prefer to listen to the changes and update it myself, like such:
final articleServiceProvider = Provider.autoDispose((ref) {
final service = ArticleService(
ref.read(apiProvider),
);
ref.listen<AuthRepository>(authRepositoryProvider, (previous, next) {
service._token = next.token;
}, fireImmediately: true);
return service;
});
That piece of code seems correct to me, however when I authenticate (authRepository.token is definitely set) and then try to invoke the markArticlesAsRead method I end up with an empty token.
The ref.listen is never called, even tho AuthRepository called notifyListeners().
I have a feeling that I'm using all that in a wrong way, but I can't really pinpoint what or where.
Try ref.watch
final articleServiceProvider = Provider.autoDispose((ref) {
final service = ArticleService(
ref.read(apiProvider),
);
final repo = ref.watch<AuthRepository>(authRepositoryProvider);
service._token = repo.token;
return service;
});
I have a StateNotifierProvider that depends on a FutureProvider. Currently they look like below.
final catalogProvider = StateNotifierProvider<CatalogNotifier, CatalogState>((ref) {
final network = ref.watch(networkProvider.future); // future provider
return CatalogNotifier(network: network);
});
this makes my CatalogNotifier accept a Future<NetworkProvider> instead of NetworkProvider and requires me to do things like below.
await (await network).doGet(...)
What's the best way to avoid having to await multiple and allow CatalogNotifier to accept a bare NetworkProvider so I can write like await network.doGet(...) ?
for completeness as requested, below is the other related providers
final networkProvider = FutureProvider<Network>((ref) async {
final cache = await ref.watch(cacheProvider.future);
return Network(cacheManager: cache);
});
final cacheProvider = FutureProvider<CacheManager>((ref) async {
final info = await ref.watch(packageInfoProvider.future);
final key = 'cache-${info.buildNumber}';
return CacheManager(Config(
key,
stalePeriod: const Duration(days: 30),
maxNrOfCacheObjects: 100,
));
I'm sure I can take my cache provider as a future into the network provider, so it doesn't have to be a FutureProvider, but I'm interested in how to solve the issue above, since in another scenario, if I depend on say 3 or 4 FutureProviders, this may not be an option.
this makes my CatalogNotifier accept a Future instead of >NetworkProvider and requires me to do things like below.
I can't think of a way to get your desired result.
Could you not just accept an AsyncValue and handle it in the statenotifier?
final catalogProvider = StateNotifierProvider<CatalogNotifier, CatalogState>((ref) {
final network = ref.watch(networkProvider); // future provider
return CatalogNotifier(network: network);
});
Then you can:
void someFunction() async {
network.maybeWhen(
data: (network) => AsyncData(await network.doGet(...)),
orElse: () => state = AsyncLoading(),
);
}
with riverpod v2 and its codegen features this has become much easier since you no longer have to decide the type of the provider. (unless you want to)
StateNotifier in riverpod 2
#riverpod
Future<CatalogController> catalog(CatalogRef ref) async {
final network = await ref.watch(networkProvider.future);
return CatalogController(network: network);
}
Alternative approch in Riverpod 2
Quite often you want to have a value calculated and have a way to explicitely redo that calculation from UI. Like a list from network, but with a refresh button in UI. This can be modelled as below in riverpod 2.
#riverpod
Future<CatalogState> myFeed(MyFeedRef ref) async {
final json = await loadData('url');
return CatalogState(json);
}
// and when you want to refresh this from your UI, or from another provider
ref.invalidate(myFeedProvider);
// if you want to also get the new value in that location right after refreshing
final newValue = await ref.refresh(myFeedProvider);
Riverpod 2 also has loading and error properties for the providers. You can use these to show the UI accordingly. Though if you want to show the last result from the provider while your feed is loading or in an error state, you have to model this yourself with a provider that returns a stream/BehaviorSubject, caches the last value .etc.
you can make AsyncValue a subtype of StateNotifier, I use the Todo list as an example.
as follows:
class TodoNotifier extends StateNotifier<AsyncValue<List<Todo>>> {
TodoNotifier(this._ref) : super(const AsyncValue.loading()) {
_fetchData();
}
final Ref _ref;
Future<void> _fetchData() async {
state = const AsyncValue.loading();
// todoListProvider is of type FutureProvider
_ref.read(todoListProvider).when(data: (data) {
state = AsyncValue.data(data);
}, error: (err, stackTrace) {
state = AsyncValue.error(err, stackTrace: stackTrace);
}, loading: () {
state = const AsyncValue.loading();
});
}
void addTodo(Todo todo) {
if (state.hasValue) {
final todoList = state.value ?? [];
state = AsyncValue.data(List.from(todoList)..add(todo));
}
}
....
}
I've writen some code that provides a ApiService to a StateNotifier. The ApiService has a dependency on a authenticatorclient - The auth client has to be created asynchronously as it uses sharedprefs to get a token.
Im just trying to figure out if theirs a more elegant way to how I've written this. Basically when the service apiService is injected into the StateNotifier it could be nullable... That to me is a bit of a code smell.
So in brief this is what im doing...
use a FutureProvider to Instantiate the RestClientwith a Dio
authenticatorClient = FutureProvider<RestClient>((ref) async {
final prefs = await SharedPreferences.getInstance();
final dio = Dio();
...
return RestClient(dio);
}
And then I watch that and use a MaybeWhen to return the service
final clientCreatorWatchProvider = Provider<ApiService?>((ref) => ref
.watch(authenticatorClient)
.whenData((value) => ApiService(value))
.maybeWhen(
data: (service) => service,
orElse: () => null,
));
So the bit I dont like is the orElse returning null
And then my StateNotifier is watching...
final AppState = StateNotifierProvider<AppNotifier, String>(
(ref) => AppNotifier(ref.watch(clientCreatorWatchProvider)));
class AppNotifier extends StateNotifier<String> {
final ApiService? apiService;
AppNotifier(this.apiService) : super("loading") {
init();
}
...
}
Any thoughts on the above approach?
Thanks
One way to solve this problem is to initialize SharedPreferences outside of a provider. You can then use ProviderScope to override a synchronous provider, eliminating the need to work with AsyncValue.
When you initialize your app, do the following:
final sharedPreferences = Provider<SharedPreferences>((_) => throw UnimplementedError());
Future<void> main() async {
final sharedPrefs = await SharedPreferences.getInstance();
runApp(
ProviderScope(
overrides: [
sharedPreferences.overrideWithValue(sharedPrefs),
],
child: MyApp(),
),
);
}
Now you could write your providers like so:
final authenticatorClient = Provider<RestClient>((ref) {
final prefs = ref.watch(sharedPreferences);
final dio = Dio();
...
return RestClient(dio);
}
final clientCreatorWatchProvider = Provider<ApiService>((ref) {
final authClient = ref.watch(authenticatorClient);
return ApiService(authClient);
});
I'm trying to use riverpod for login with a laravel backend. Right now I'm just returning true or false from the repository. I've set a form that accepts email and password. The isLoading variable is just to show a circle indicator. I've run the code and it works but not sure if I'm using riverpod correctly. Is there a better way to do it ?
auth_provider.dart
class Auth{
final bool isLogin;
Auth(this.isLogin);
}
class AuthNotifier extends StateNotifier<Auth>{
AuthNotifier() : super(Auth(false));
void isLogin(bool data){
state = new Auth(data);
}
}
final authProvider = StateNotifierProvider((ref) => new AuthNotifier());
auth_repository.dart
class AuthRepository{
static String url = "http://10.0.2.2:8000/api/";
final Dio _dio = Dio();
Future<bool> login(data) async {
try {
Response response = await _dio.post(url+'sanctum/token',data:json.encode(data));
return true;
} catch (error) {
return false;
}
}
}
login_screen.dart
void login() async{
if(formKey.currentState.validate()){
setState((){this.isLoading = true;});
var data = {
'email':this.email,
'password':this.password,
'device_name':'mobile_phone'
};
var result = await AuthRepository().login(data);
if(result){
context.read(authProvider).isLogin(true);
setState((){this.isLoading = false;});
}else
setState((){this.isLoading = false;});
}
}
Since I'm not coming from mobile background and just recently use flutter+riverpod in my recent project, I cannot say this is the best practice. But there are some points I'd like to note:
Use interface such IAuthRepository for repository. Riverpod can act as a dependency injection.
final authRepository = Provider<IAuthRepository>((ref) => AuthRepository());
Build data to send in repository. You should separate presentation, business logic, and explicit implementation for external resource if possible.
Future<bool> login(String email, String password) async {
try {
var data = {
'email': email,
'password': password,
'device_name':'mobile_phone'
};
Response response = await _dio.post(url+'sanctum/token',data:json.encode(data));
return true;
} catch (error) {
return false;
}
}
Do not call repository directly from presentation/screen. You can use the provider for your logic, which call the repository
class AuthNotifier extends StateNotifier<Auth>{
final ProviderReference ref;
IAuthRepository _authRepository;
AuthNotifier(this.ref) : super(Auth(false)) {
_authRepository = ref.watch(authRepository);
}
Future<void> login(String email, String password) async {
final loginResult = await_authRepository.login(email, password);
state = Auth(loginResult);
}
}
final authProvider = StateNotifierProvider((ref) => new AuthNotifier(ref));
On screen, you can call provider's login method
login() {
context.read(authProvider).login(this.email, this.password);
}
Use Consumer or ConsumerWidget to watch the state and decide what to build.
It also helps that instead of Auth with isLogin for the state, you can create some other state. At the very least, I usually create an abstract BaseAuthState, which derives to AuthInitialState, AuthLoadingState, AuthLoginState, AuthErrorState, etc.
class AuthNotifier extends StateNotifier<BaseAuthState>{
...
AuthNotifier(this.ref) : super(AuthInitialState()) { ... }
...
}
Consumer(builder: (context, watch, child) {
final state = watch(authProvider.state);
if (state is AuthLoginState) ...
else if (state is AuthLoadingState) ...
...
})
Instead of using a bool, I like to use enums or class for auth state
enum AuthState { initialize, authenticated, unauthenticated }
and for login state
enum LoginStatus { initialize, loading, success, failed }