Riverpod, reading state in outside BuildContext and Provider - flutter

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?

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

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

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.

Related

How to write and read data anywhere by shared_preferences on Flutter 3.7 background isolates?

On Flutter 3.7 platform channels can run on any isolate. So I tried this sample,
import ‘package:flutter/services.dart’;
import ‘package:shared_preferences/shared_preferences.dart’;
void main() {
// Identify the root isolate to pass to the background isolate.
// (API introduced in Flutter 3.7)
RootIsolateToken rootIsolateToken = RootIsolateToken.instance!;
Isolate.spawn(_isolateMain, rootIsolateToken);
}
void _isolateMain(RootIsolateToken rootIsolateToken) async {
// Register the background isolate with the root isolate.
BackgroundIsolateBinaryMessenger
.ensureInitialized(rootIsolateToken);
// You can now use the shared_preferences plugin.
SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
print(sharedPreferences.getBool(‘isDebug’));
}
I can read from data on shared_preferences in this sample okey. But how can I use this feature anywhere in my app? How can I set or read data using this isolate on initState for example?
Basically you need to implement communication between isolates. You can read more about it here
Here is an example, you can change flutter_secure_storage that i used with shared_preferences package
import 'dart:async';
import 'dart:isolate';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class CreationEvent {
final RootIsolateToken isolateToken;
final SendPort sendPort;
CreationEvent(this.isolateToken, this.sendPort);
}
class DeletetionEvent {}
class ReadEvent {
final String key;
const ReadEvent(this.key);
}
class ReadResult {
final String key;
final String? content;
const ReadResult(this.key, this.content);
}
class IsolateIO {
IsolateIO._();
final _toBgPort = Completer();
final Map<Object, Completer> _completerMap = {};
Isolate? _isolate;
StreamSubscription? _fromBgListener;
void start() async {
RootIsolateToken rootIsolateToken = RootIsolateToken.instance!;
ReceivePort fromBG = ReceivePort();
_fromBgListener = fromBG.listen((message) {
// setup process
if (message is SendPort) {
_toBgPort.complete(message);
return;
}
if (message is ReadResult) {
_completerMap['read:${message.key}']?.complete(message.content);
_completerMap.remove('read:${message.key}');
}
});
_isolate = await Isolate.spawn(
(CreationEvent data) {
final worker = IsolateWorker(data.isolateToken, data.sendPort);
worker.listen();
},
CreationEvent(rootIsolateToken, fromBG.sendPort),
);
}
Future<String?> readFromStorage(String key) async {
// make sure isolate created with ports
final port = await _toBgPort.future;
// store completer
final completer = Completer<String?>();
_completerMap['read:$key'] = completer;
// send key to be read
port.send(ReadEvent(key));
// return result
return completer.future;
}
void stop() async {
if (_toBgPort.isCompleted) {
final port = await _toBgPort.future;
port.send(DeletetionEvent());
}
_fromBgListener?.cancel();
_isolate?.kill(priority: Isolate.immediate);
}
static final i = IsolateIO._();
}
class IsolateWorker {
final RootIsolateToken rootIsolateToken;
final SendPort toMain;
final FlutterSecureStorage storage;
StreamSubscription? subs;
IsolateWorker(
this.rootIsolateToken,
this.toMain, {
this.storage = const FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
),
}) {
// Register the background isolate with the root isolate.
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
}
void listen() {
ReceivePort fromMain = ReceivePort();
toMain.send(fromMain.sendPort);
subs = fromMain.listen((message) => onMessage(message));
}
void onMessage(dynamic message) async {
if (message is DeletetionEvent) {
subs?.cancel();
return;
}
if (message is ReadEvent) {
final rawJson = await storage.read(key: message.key);
toMain.send(ReadResult(message.key, rawJson));
}
}
}
class View extends StatefulWidget {
const View({super.key});
#override
State<View> createState() => _ViewState();
}
class _ViewState extends State<View> {
String username = '';
#override
void initState() {
super.initState();
IsolateIO.i.start();
WidgetsBinding.instance.addPostFrameCallback((_) async {
final name = await IsolateIO.i.readFromStorage('username');
setState(() {
username = name ?? '';
});
});
}
#override
void dispose() {
IsolateIO.i.stop();
super.dispose();
}
#override
Widget build(BuildContext context) {
return SizedBox(
child: Text(username),
);
}
}

Riverpod as global variable

I'm using riverpod in my project and I want to use is it provide instance of User class between widgets.
My UserState class:
import 'package:caralgo_mobile_app/models/user.dart';
import 'package:flutter/cupertino.dart';
class UserState extends ChangeNotifier{
User? _user;
User? getUser(){
return _user;
}
void setUser(User user){
_user = user;
}
}
Function where I'm writing to _user:
void logIn(){
if(loginFormKey.currentState!.validate()){
final _user = UserDraft(email: emailTEC.text, password: passwordTEC.text);
_apiService.post('/signin', _user.toJson()).then((value) {
scaffoldMessengerKey.currentState!.showSnackBar(SnackBar(content: Text(value!.message)));
ProviderContainer _container = ProviderContainer();
_container.read(userProvider).setUser(User.fromJson(value.data['user']));
_navigatorService.navigateToMainPage();
}
);
}
}
Class where I want to read User data:
class ConfigurationPageViewModel {
final ApiService _apiService = GetIt.I.get<ApiService>();
static final ProviderContainer _container = ProviderContainer();
final registrationFormKey = GlobalKey<FormState>();
final validator = Validator();
final emailTEC = TextEditingController(text: _container.read(userProvider).getUser()!.email);
final nameTEC = TextEditingController(text: _container.read(userProvider).getUser()!.firstName);
final familyNameTEC = TextEditingController(text: _container.read(userProvider).getUser()!.lastName);
final passwordTEC = TextEditingController(text: _container.read(userProvider).getUser()!.password);
var isEnterpriseUser = false;
final enterpriseNameTEC = TextEditingController(text: _container.read(userProvider).getUser()!.enterprise);
}
I assumed this solution can work, instead it throws an error when I'm opening the ConfigurationPage:
Null check operator used on a null value
The problem occurs because of usage of ProviderContainer. In riverpod every provider has two objects: provider and state of provider. Providers itself are global, but states are container depending. In this case I couldn't access the User data, because each container that I used had separate state.
To solve this issue I got rid of ProviderContainers completely.
I'm passing userProvider directly from consumer widget to:
void logIn(dynamic userProvider){
if(loginFormKey.currentState!.validate()){
final _user = UserRegistrationInfo(email: emailTEC.text, password: passwordTEC.text);
_apiService.post('/signin', _user.toJson()).then((value) {
scaffoldMessengerKey.currentState!.showSnackBar(SnackBar(content: Text(value!.message)));
userProvider.setUser(User.fromJson(value.data['user']));
_navigatorService.navigateToMainPage();
}
);
}
}
And all the TextEditControllers are initialized inside the widget:
#override
void initState() {
_configurationPageViewModel.emailTEC.text = ref.read(userProvider).getUser()!.email;
_configurationPageViewModel.nameTEC.text = ref.read(userProvider).getUser()!.firstName;
_configurationPageViewModel.familyNameTEC.text = ref.read(userProvider).getUser()!.lastName;
_configurationPageViewModel.enterpriseCodeTEC.text = ref.read(userProvider).getUser()!.enterprise ?? '';
_configurationPageViewModel.phoneNumberTEC.text = ref.read(userProvider).getUser()!.phone ?? '';
super.initState();
}

How to Store API model object in Local Storage in flutter?

I fatch this issue during use Local Storage(shared_preferences: ^2.0.6) in my code....but i cant store the api model object in local storage...How can i do?
storeModelInPrefs() async {
http.Response response =
await http.get(Uri.parse('http://18.191.193.64/api/view_categories'));
String encodeData = jsonEncode(response.body);
///Write Data in local Storage
GetStorageUtility.prefs!.write('key', encodeData);
///Read Data from local Storage
String data = GetStorageUtility.prefs!.read('key');
if (data == null) {
print('no data in GetStorage');
} else {
Map<String, dynamic> map = jsonDecode(data);
print(map);
}
}
This is the sample example that i have created from the code that you have provided.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_app/utilities.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(MaterialApp(
home: MyApp(),
));
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
void initState() {
GetStorageUtility.init();
super.initState();
getRemoteData();
}
getRemoteData() async {
/// This is where the api is fetching the data
var response =
await http.get(Uri.parse('http://18.191.193.64/api/view_categories'));
/// This is where the string getting
String encodeData = jsonEncode(response.body);
GetStorageUtility.write("key", encodeData);
/// this is where you fetch the data
String data = GetStorageUtility.read("key");
if (data == null) {
print('no data in GetStorage');
} else {
Map<String, dynamic> jsonData = json.decode(data);
jsonData.forEach((key, value) {
print("$key : $value\n");
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(" Page"),
),
);
}
}
SharadPrefs Singleton,
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
class GetStorageUtility {
static Future<SharedPreferences> get _instance async =>
_prefsInstance ??= await SharedPreferences.getInstance();
static SharedPreferences _prefsInstance;
static Future<SharedPreferences> init() async {
_prefsInstance = await _instance;
return _prefsInstance;
}
static String read(String key, [String defValue]) {
return _prefsInstance.getString(key) ?? defValue ?? "";
}
static Future<bool> write(String key, String value) async {
var prefs = await _instance;
return prefs?.setString(key, value) ?? Future.value(false);
}
}
Now there is on thing that you have see that you have added in you android manifest file
<application android:usesCleartextTraffic="true" />
This one should be there and the internet permission should be there in the debug and the main folders manifestfile.
This will work but this is not the best practice to store the data as string in the sharedprefs. Shared Prefs has only the job to manage the small data like bool or string. For your use case you can use a sqlite as a local data base. where you can fetch the data based on the condititions.
Let me know if it works.
Happy Coding.

Riverpod - State notifier to get current user is always null

I have this state notifier to get the current user:
import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final userProvider = StateNotifierProvider<UserNotifier, User?>((ref) {
return UserNotifier(null);
});
class UserNotifier extends StateNotifier<User?> {
UserNotifier(User? state) : super(state) {
FirebaseAuth.instance.authStateChanges().listen((user) {
if (user == null) {
clearUser();
} else {
setUser(user);
}
});
}
void setUser(User user) {
state = user;
}
void clearUser() {
state = null;
}
}
I use it in my Http client to get the id token:
final Provider<Dio> httpClientProvider = Provider<Dio>((ref) {
final dio = Dio(BaseOptions(baseUrl: EnvironmentConstants.of().apiUrl));
final localDataSource = ref.read(localStorageDataSourceProvider);
final user = ref.read(userProvider);
String? accessToken = localDataSource.get<dynamic>(
keyToRead: 'access_token')['access_token_value'] as String?;
accessToken = accessToken ?? user?.getIdToken() as String;
The thing is the value of user is always null in the HttpProvider.
So final user = ref.read(userProvider); always returns null.
FirebaseAuth.instance.authStateChanges().listen( shows it is populated but it is also running too often:
I think this bit is always setting it back to null as it is also running too often:
final userProvider = StateNotifierProvider<UserNotifier, User?>((ref) {
return UserNotifier(null);
});
I'm not sure what I could pass into there instead of null.
Made the instance of the state notifier a global variable so it doesn't re-instantiate it when something calls the ref.read(userProvider):
var currentUser = UserNotifier();
final userProvider = StateNotifierProvider<UserNotifier, User?>((ref) {
return currentUser;
});
Very open to suggestions for improvement as I'm not strictly sure if this is meant to be done... Can I achieve the same outcome without a global variable?

Flutter api login using riverpod

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 }