Flutter autoDispose riverpod StateNotifierProvider - flutter

This is my shared riverpod class that i want to use that on multiple screens, but after navigate to another screen using ref.listen couldn't dispose or cancel and using another ref.listen work twice, how can i cancel each ref.listen on screen which i used that? for example you suppose i have two screen A and B and into A screen i have
A screen
final future = ref.watch(requestProvider);
ref.listen<NetworkRequestState<int?>>(requestProvider, (
NetworkRequestState? previousState,
NetworkRequestState newState,
) {});
on this ref.listen i navigate to another screen when server return 200 ok? now in B screen which i have ref.listen again:
B screen
final future = ref.watch(requestProvider);
ref.listen<NetworkRequestState<int?>>(requestProvider, (
NetworkRequestState? previousState,
NetworkRequestState newState,
) {});
without sending any request to server this listener work and listen to previous listener
requestProvider on this class shared between multiple screens and autoDispose don't work for that, because after creating another StateNotifierProvider such as requestProviderA_Screen work fine without problem, for example:
final requestProvider = StateNotifierProvider.autoDispose<RequestNotifier,
NetworkRequestState<int?>>(
(ref) => RequestNotifier(ref.watch(requestRepositoryProvider)));
final requestProviderA_Screen = StateNotifierProvider.autoDispose<RequestNotifier,
NetworkRequestState<int?>>(
(ref) => RequestNotifier(ref.watch(requestRepositoryProvider)));
my request riverpod class:
final requestRepositoryProvider =
Provider.autoDispose<Repository>((ref) => Repository(ref.read));
final requestProvider = StateNotifierProvider.autoDispose<RequestNotifier,
NetworkRequestState<int?>>(
(ref) => RequestNotifier(ref.watch(requestRepositoryProvider)));
class Repository {
final Reader _reader;
Repository(this._reader);
Future<int?> getResponse(
HTTP method, String endPoint, Map<String, dynamic> parameters) async {
try {
const r = RetryOptions(maxAttempts: 3);
final response = await r.retry(
() => _submit(method, endPoint, parameters),
retryIf: (e) => e is SocketException || e is TimeoutException,
);
return response.statusCode;
} on DioError catch (e) {
throw (e.response != null
? e.response!.statusCode
: e.error.osError.errorCode) as Object;
}
}
Future<Response> _submit(
HTTP method, String endPoint, Map<String, dynamic> parameters) {
final Options options = Options(
headers: {'Content-Type': 'application/json'},
);
late Future<Response> _r;
switch (method) {
case HTTP.GET:
_r = _reader(dioProvider).get(
endPoint,
queryParameters: parameters,
options: options,
);
break;
case HTTP.POST:
_r = _reader(dioProvider).post(
endPoint,
queryParameters: parameters,
options: options,
);
break;
}
return _r.timeout(const Duration(seconds: 30));
}
}
class RequestNotifier extends RequestStateNotifier<int?> {
final Repository _repository;
RequestNotifier(this._repository);
Future<NetworkRequestState<int?>> send({
required HTTP method,
required String endPoint,
required Map<String, dynamic> parameters,
}) =>
makeRequest(
() => _repository.getResponse(method, endPoint, parameters));
}
and one of screen which i use this class:
class SignUp extends HookConsumerWidget {
final String mobileNumber;
const SignUp({Key? key, required this.mobileNumber}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final _formKey = useMemoized(() => GlobalKey<FormState>());
final _nameFamily = useTextEditingController();
final future = ref.watch(requestProvider);
useEffect(() {
_nameFamily.dispose();
}, [_nameFamily]);
ref.listen<NetworkRequestState<int?>>(requestProvider, (
NetworkRequestState? previousState,
NetworkRequestState newState,
) {
newState.when(
idle: () {},
//...
}
success: (status) {
//...
Routes.seafarer.navigate(
'/complete-register',
params: {
'mobile_number': mobileNumber.trim(),
'name_family': _nameFamily.text.trim()
},
);
},
error: (error, stackTrace) {
//...
});
});
final _onSubmit = useMemoized(
() => () {
if (_nameFamily.text.trim().isEmpty) {
//...
} else {
//..
ref.read(requestProvider.notifier).send(
method: HTTP.GET,
endPoint: Server.$updateNameFamily,
parameters: {
'mobile_number': mobileNumber,
'name_family': _nameFamily.text.trim()
});
}
},
[_formKey],
);
return Scaffold(
//...
);
}
}

Related

How can I use Future<> riverpod example on Flutter?

#riverpod
Future<String> boredSuggestion(BoredSuggestionRef ref) async {
final response = await http.get(
Uri.https('https://www.boredapi.com/api/activity'),
);
final json = jsonDecode(response.body) as Map;
return json['activity']! as String;
}
class Home extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
final boredSuggestion = ref.watch(boredSuggestionProvider);
// Perform a switch-case on the result to handle loading/error states
return boredSuggestion.when(
loading: () => const Text('loading'),
error: (error, stackTrace) => Text('error: $error'),
data: (data) => Text(data),
);
}
}
I am trying to copy the simple example from Riverpod homepage. However, I get
Undefined class 'BoredSuggestionRef'. Try changing the name to the name of an existing class, or creating a class with the name 'BoredSuggestionRef'.
Error and I am trying to build it.
final testingP = StateProvider<Future<String>>((ref) async {
final response = await http.get(
Uri.https('https://www.boredapi.com/api/activity'),
);
final json = jsonDecode(response.body) as Map;
return json['activity']! as String;
});
#override
Widget build(BuildContext context, WidgetRef ref) {
final testing = ref.watch(testingP);
return testing.when(
loading: () => const Text('loading'),
error: (error, stackTrace) => Text('error: $error'),
data: (data) => Text(data),
);
}
And, in this example, I get The method 'when' isn't defined for the type 'Future'. Try correcting the name to the name of an existing method, or defining a method named 'when' error.
How can I use that example in this case?
Try the following code:
final testingP = FutureProvider.autoDispose<String>((ref) async {
final response = await http.get(
Uri.https('https://www.boredapi.com/api/activity'),
);
final json = jsonDecode(response.body) as Map;
return json['activity']! as String;
});
The method When is not defined for the type StateProvider, try using a Future provider since you're awaiting a future response.The code would look like:
final testingP = FutureProvider<String>((ref) async {
final response = await http.get(
Uri.https('https://www.boredapi.com/api/activity'),
);
final json = jsonDecode(response.body) as Map;
return json['activity']! as String;
});

type 'Null' is not a subtype of type 'Future<bool>'

I'm getting the below error while I'm trying to implement bloc testing in my flutter project
type 'Null' is not a subtype of type 'Future<bool>'
package:mynovatium/features/signup/repositories/signup_repository.dart 10:16 MockRepository.createAccountsignup
Following are the corresponding files that might help identify the cause of the error
signup_bloc_test.dart
class MockRepository extends Mock implements SignUpRepository {}
void main() async {
await configureInjection(inj.Environment.test);
group('SignupBloc', () {
late SignUpBloc signUpBloc;
late SignUpRepository signupRepositoryMock;
setUp(() {
signupRepositoryMock = MockRepository();
signUpBloc = SignUpBloc(signUpRepository: signupRepositoryMock);
});
test('initial state of the bloc is [AuthenticationInitial]', () {
expect(SignUpBloc(signUpRepository: signupRepositoryMock).state,
SignupInitial(),);
});
group('SignUpCreateAccount', () {
blocTest<SignUpBloc, SignUpState>(
'emits [SignUpCreateAccountLoading, SignupInitial] '
'state when successfully Signed up',
setUp: () {
when(signupRepositoryMock.createAccount(
'Nevil',
'abcd',
'nikunj#gmail.com',
'english',
),).thenAnswer((_) async => Future<bool>.value(true));
},
build: () => SignUpBloc(signUpRepository: signupRepositoryMock),
act: (SignUpBloc bloc) => bloc.add(
const SignUpCreateAccount(
'Nevil',
'abcd',
'nikunj#gmail.com',
'english',
),
),
expect: () => [
SignUpCreateAccountLoading(),
SignupInitial(),
],
);
});
});
}
signup_repository.dart
This is the code for the signup repository.
class SignUpRepository {
Future<bool> createAccount(String _firstName, String _lastName, String _eMailAddress, String _language) async {
final Response _response;
try {
_response = await CEApiRequest().post(
Endpoints.createCustomerAPI,
jsonData: <String, dynamic>{
'firstName': _firstName,
'lastName': _lastName,
'email': _eMailAddress,
'language': _language,
'responseUrl': Endpoints.flutterAddress,
},
);
final Map<String, dynamic> _customerMap = jsonDecode(_response.body);
final CustomerModel _clients = CustomerModel.fromJson(_customerMap['data']);
if (_clients.id != null) {
return true;
} else {
return false;
}
} on KBMException catch (e) {
final KBMException _exception = e;
throw _exception;
}
}
}
If anyone has any ideas on what might be the issue here, please help!!
Okay so in the above code you need to stub the methods within the mock repository as well and override it to have it return something incase null is being returned.
class MockRepository extends Mock implements SignUpRepository {
#override
Future<bool> createAccount(String? _firstName, String? _lastName, String? _eMailAddress, String? _language) =>
super.noSuchMethod(Invocation.method(#createAccount, [_firstName, _lastName, _eMailAddress, _language]),
returnValue: Future<bool>.value(false),);
}
Doing something like that done in the above code works well.

how to set values in initState method using riverpod

right now i have a ChangeNotifierProvider, and i want to set some values straight in the initState method.
those values come from a backend API, that are retrieved in that provider.
I am stuck is this situation for a while now, hope i can get some help.
Here is the ChangeNotifierProvider
final userProvider = ChangeNotifierProvider.autoDispose.family<UserProxy, String>((ref, id) {
var notifier = UserProxy(userId: id);
notifier.load();
return notifier;
});
class UserProxy extends ChangeNotifier {
String userId;
User? user;
UserProxy({this.userId = ""});
void load() async {
await getUser().then((value) => generateObject(value));
}
Future<String> getUser() async {
Map<String, String> queryParams = {
"id": userId,
};
var url = Uri.https("asdadas.asdasd.com", "endpoint", queryParams);
Map<String, String> headers = {
'content-type': "application/json",
};
var response = await http.get(url,
headers: headers,);
print(response.body);
return response.body;
}
User generateObject(String jsonString) {
this.user = User.fromJson(jsonDecode(jsonString));
notifyListeners();
return this.user ?? User();
}
}
For this case I would suggest
FutureProvider.autoDispose.family<UserProxy, String>((ref, id) async { .... })
then change your StateWidget to ConsumerStatefulWidget and ConsumerState<>
then
ref.watch(provider(11)).when(
loading: (){},
error: (Object err, StackTrace? st){ },
data: (user){
// build widget with result here.
},
)

Cannot call Flutter Singleton from another package

I am trying to import an asynchronous function in Flutter to handle securely storing user data. The problem is I keep getting the following error:
packages/authentication_repository/lib/src/authentication_repository.dart:64:15:
Error: Method not found: 'set'. await SecureStorageService.set(
^^^
Here is my code:
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureStorageService {
static SecureStorageService _intance;
FlutterSecureStorage flutterSecureStorage;
SecureStorageService._internal() {
this.flutterSecureStorage = new FlutterSecureStorage();
}
static Future<SecureStorageService> getInstance() async {
if (_intance == null) {
_intance = SecureStorageService._internal();
}
return _intance;
}
Future<void> set(String key, String value) async {
await this.flutterSecureStorage.write(key: key, value: value);
}
Future<String> get(String key) async {
return await this.flutterSecureStorage.read(key: key);
}
Future<void> clear() async {
await this.flutterSecureStorage.deleteAll();
}
}
And then I import the code like follows:
import 'package:crowdplan_flutter/storage_util.dart';
...
class AuthenticationRepository {
final _controller = StreamController<AuthenticationStatus>();
final secureStorage = SecureStorageService.getInstance();
...
try {
final response = await http.post(
url,
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'email': email,
'password': password,
'client_id': clientId,
}),
);
if (response.statusCode == 200) {
print(response.body);
print(json.decode(response.body)['access_token']);
print(json.decode(response.body)['refresh_token']);
await secureStorage.set(
key: 'access_token',
value: json.decode(response.body)['access_token']);
await secureStorage.set(
key: 'refresh_token',
value: json.decode(response.body)['refresh_token']);
await secureStorage.set(
key: 'user_id', value: json.decode(response.body)['user_id']);
_controller.add(AuthenticationStatus.authenticated);
}
} catch (error, stacktrace) {
print('Exception occurred: $error stackTrace: $stacktrace');
}
}
My Singleton is initiated in my main.dart file like so.
void main() async {
await SecureStorageService.getInstance();
runApp(App(
authenticationRepository: AuthenticationRepository(),
userRepository: UserRepository(),
));
}
I am new to Flutter so this might be a new noob error.
The set method isn't static and can't be accessed with SecureStorageService.set
Future<void> set(String key, String value) async {
await this.flutterSecureStorage.write(key: key, value: value);
}
I see in the 2nd code snippet that you've assigned the singleton to secureStorage.
Did you mean to access it with something like?:
secureStorage.set()
Part 2 - Code Example
Perhaps the async getInstance() in the singleton class is tripping you up. It doesn't need to be async (nor should it be). (In some cases you may want an async initializer instead of a constructor. See the bottom of the Example code here for a use-case.)
SecureStorageService (the singleton) gets instantiated in your main() method so inside AuthenticationRepository it'll use that same instance and be ready to use.
class AuthenticationRepository {
final secureStorage = SecureStorageService.getInstance;
// ↑ will get the same instance created in main()
The code sample in the question doesn't specify where/when the http.post method is being called, but I'm guessing it's an initialization / setup for AuthenticationRepository so I've mocked up an initStorage() method inside it.
This initStorage() call will use the SecureStorageService singleton, with a call to its secureStorage.set() method.
Hopefully this example helps you spot a difference between our code samples to figure out what's going wrong.
import 'package:flutter/material.dart';
/// Mocking FlutterSecureStorage
/// Note that the actual package FlutterSecureStorage does not have an async
/// constructor nor initializer
class FlutterSecureStorage {
Map<String,String> data = {};
Future<void> write({String key, String value}) async {
data[key] = value;
}
Future<String> read({String key}) async {
print('FSS read - returning value: ${data[key]}');
return data[key];
}
}
class SecureStorageService {
/// for singleton ↓ instance should be final and uses private constructor
static final SecureStorageService _instance = SecureStorageService._internal();
FlutterSecureStorage flutterSecureStorage;
/// Private constructor, not async
SecureStorageService._internal() {
flutterSecureStorage = FlutterSecureStorage();
}
/// This doesn't need to be async. FlutterSecureStorage (FSS) doesn't have an async initializer
/// and constructors are generally never async
/*static Future<SecureStorageService> getInstance() async {
if (_instance == null) {
_instance = SecureStorageService._internal();
}
return _instance;
}*/
/// static singleton instance getter, not async
static SecureStorageService get getInstance => _instance;
/// don't need "this" keyword & could use FSS methods directly, but leaving as is
Future<void> set({String key, String value}) async {
await flutterSecureStorage.write(key: key, value: value);
}
Future<String> get({String key}) async {
return flutterSecureStorage.read(key: key);
}
}
class Response {
int statusCode = 200;
Response() {
print('http post completed');
}
}
class AuthenticationRepository {
final secureStorage = SecureStorageService.getInstance;
String accessToken = '';
/// Encapsulates the slow init of a http.post call. When all ready, returns
/// the AuthenticationRepository in a usable state
Future<AuthenticationRepository> initStorage() async {
try {
// Mock http response
final response = await Future.delayed(Duration(seconds: 2), () => Response());
if (response.statusCode == 200) {
accessToken = 'access_token from http value';
await secureStorage.set(
key: 'access_token',
value: accessToken);
print('access token set');
// snipped other calls for brevity
}
} catch (error, stacktrace) {
print('Exception occurred: $error stackTrace: $stacktrace');
}
return this;
}
}
class SingleStoragePage extends StatefulWidget {
#override
_SingleStoragePageState createState() => _SingleStoragePageState();
}
class _SingleStoragePageState extends State<SingleStoragePage> {
Future<AuthenticationRepository> authRepo;
#override
void initState() {
super.initState();
authRepo = AuthenticationRepository().initStorage();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Singleton Storage'),
),
body: Center(
child: FutureBuilder<AuthenticationRepository>(
future: authRepo,
builder: (context, snapshot) {
print('FutureBuilder re/building');
if (snapshot.hasData) {
return Text('Access token: ${snapshot.data.accessToken}');
}
return Text('loading...');
},
),
),
);
}
}

Flutter Riverpod : How to Implement FutureProvider?

I using Flutter Riverpod package to handling http request. I have simple Http get request to show all user from server, and i using manage it using FutureProvider from Flutter Riverpod package.
API
class UserGoogleApi {
Future<List<UserGoogleModel>> getAllUser() async {
final result = await reusableRequestServer.requestServer(() async {
final response =
await http.get('${appConfig.baseApiUrl}/${appConfig.userGoogleController}/getAllUser');
final Map<String, dynamic> responseJson = json.decode(response.body);
if (responseJson['status'] == 'ok') {
final List list = responseJson['data'];
final listUser = list.map((e) => UserGoogleModel.fromJson(e)).toList();
return listUser;
} else {
throw responseJson['message'];
}
});
return result;
}
}
User Provider
class UserProvider extends StateNotifier<UserGoogleModel> {
UserProvider([UserGoogleModel state]) : super(UserGoogleModel());
Future<UserGoogleModel> searchUserByIdOrEmail({
String idUser,
String emailuser,
String idOrEmail = 'email_user',
}) async {
final result = await _userGoogleApi.getUserByIdOrEmail(
idUser: idUser,
emailUser: emailuser,
idOrEmail: idOrEmail,
);
UserGoogleModel temp;
for (var item in result) {
temp = item;
}
state = UserGoogleModel(
idUser: temp.idUser,
createdDate: temp.createdDate,
emailUser: temp.emailUser,
imageUser: temp.emailUser,
nameUser: temp.nameUser,
tokenFcm: temp.tokenFcm,
listUser: state.listUser,
);
return temp;
}
Future<List<UserGoogleModel>> showAllUser() async {
final result = await _userGoogleApi.getAllUser();
state.listUser = result;
return result;
}
}
final userProvider = StateNotifierProvider((ref) => UserProvider());
final showAllUser = FutureProvider.autoDispose((ref) async {
final usrProvider = ref.read(userProvider);
final result = await usrProvider.showAllUser();
return result;
});
After that setup, i simply can call showAllUser like this :
Consumer((ctx, read) {
final provider = read(showAllUser);
return provider.when(
data: (value) {
return ListView.builder(
itemCount: value.length,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
final result = value[index];
return Text(result.nameUser);
},
);
},
loading: () => const CircularProgressIndicator(),
error: (error, stackTrace) => Text('Error $error'),
);
}),
it's no problem if http request don't have required parameter, but i got problem if my http request required parameter. I don't know how to handle this.
Let's say , i have another http get to show specific user from id user or email user. Then API look like :
API
Future<List<UserGoogleModel>> getUserByIdOrEmail({
#required String idUser,
#required String emailUser,
#required String idOrEmail,
}) async {
final result = await reusableRequestServer.requestServer(() async {
final baseUrl =
'${appConfig.baseApiUrl}/${appConfig.userGoogleController}/getUserByIdOrEmail';
final chooseURL = idOrEmail == 'id_user'
? '$baseUrl?id_or_email=$idOrEmail&id_user=$idUser'
: '$baseUrl?id_or_email=$idOrEmail&email_user=$emailUser';
final response = await http.get(chooseURL);
final Map<String, dynamic> responseJson = json.decode(response.body);
if (responseJson['status'] == 'ok') {
final List list = responseJson['data'];
final listUser = list.map((e) => UserGoogleModel.fromJson(e)).toList();
return listUser;
} else {
throw responseJson['message'];
}
});
return result;
}
User Provider
final showSpecificUser = FutureProvider.autoDispose((ref) async {
final usrProvider = ref.read(userProvider);
final result = await usrProvider.searchUserByIdOrEmail(
idOrEmail: 'id_user',
idUser: usrProvider.state.idUser, // => warning on "state"
);
return result;
});
When i access idUser from userProvider using usrProvider.state.idUser , i got this warning.
The member 'state' can only be used within instance members of subclasses of 'package:state_notifier/state_notifier.dart'.
It's similiar problem with my question on this, but on that problem i already know to solved using read(userProvider.state) , but in FutureProvider i can't achieved same result using ref(userProvider).
I missed something ?
Warning: This is not a long-term solution
Assuming that your FutureProvider is being properly disposed after each use that should be a suitable workaround until the new changes to Riverpod are live. I did a quick test to see and it does work. Make sure you define a getter like this and don't override the default defined by StateNotifier.
class A extends StateNotifier<B> {
...
static final provider = StateNotifierProvider((ref) => A());
getState() => state;
...
}
final provider = FutureProvider.autoDispose((ref) async {
final a = ref.read(A.provider);
final t = a.getState();
print(t);
});
Not ideal but seems like a fine workaround. I believe the intention of state being inaccessible outside is to ensure state manipulations are handled by the StateNotifier itself, so using a getter in the meantime wouldn't be the end of the world.