Cannot call Flutter Singleton from another package - flutter

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

Related

Flutter autoDispose riverpod StateNotifierProvider

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(
//...
);
}
}

Can't initialized GraphQl Client in flutter using Get_it

I want to implement GraphQL client in my flutter app. For Dependency injection, I use GetIt library. But when I run the app, it says
'Invalid argument (Object of type HomeGraphQLService is not
registered inside GetIt. Did you forget to pass an instance name?
(Did you accidentally do GetIt sl=GetIt.instance(); instead of GetIt
sl=GetIt.instance;)): HomeGraphQLService'
.
It means GraphQL client did not instantiate somehow, although I registered it in my service locator
Session.dart
abstract class Session {
String getAccessToken();
}
SessionImpl.dart
class SessionImpl extends Session {
SharedPreferences sharedPref;
SessionImpl(SharedPreferences sharedPref) {
this.sharedPref = sharedPref;
}
#override
String getAccessToken() {
return sharedPref.getString('access_token') ?? "";
}
}
GraphQLClientGenerator.dart
class GraphQLClientGenerator {
Session session;
GraphQLClientGenerator(Session session) {
this.session = session;
}
GraphQLClient getClient() {
final HttpLink httpLink = HttpLink('https://xxx/graphql');
final AuthLink authLink = AuthLink(getToken: () async => 'Bearer ${_getAccessToken()}');
final Link link = authLink.concat(httpLink);
return GraphQLClient(link: link, cache: GraphQLCache(store: InMemoryStore()));
}
String _getAccessToken() {
return session.getAccessToken();
}
}
HomeRepository.dart
abstract class HomeRepository {
Future<List<Course>> getAllCourseOf(String className, String groupName);
}
HomeRepositoryImpl.dart
class HomeRepositoryImpl extends HomeRepository {
HomeGraphQLService homeGraphQLService;
HomeMapper homeMapper;
HomeRepositoryImpl(HomeGraphQLService homeGraphQLService, HomeMapper homeMapper) {
this.homeGraphQLService = homeGraphQLService;
this.homeMapper = homeMapper;
}
#override
Future<List<Course>> getAllCourseOf(String className, String groupName) async {
final response = await homeGraphQLService.getAllCourseOf(className, groupName);
return homeMapper.toCourses(response).where((course) => course.isAvailable);
}
}
HomeGraphQLService.dart
class HomeGraphQLService {
GraphQLClient graphQLClient;
HomeGraphQLService(GraphQLClient graphQLClient) {
this.graphQLClient = graphQLClient;
}
Future<SubjectResponse> getAllCourseOf(String className, String groupName) async {
try {
final response = await graphQLClient.query(getAllCourseQuery(className, groupName));
return SubjectResponse.fromJson((response.data));
} catch (e) {
return Future.error(e);
}
}
}
GraphQuery.dart
QueryOptions getAllCourseQuery(String className, String groupName) {
String query = """
query GetSubject($className: String, $groupName: String) {
subjects(class: $className, group: $groupName) {
code
display
insights {
coming_soon
purchased
}
}
}
""";
return QueryOptions(
document: gql(query),
variables: <String, dynamic>{
'className': className,
'groupName': groupName,
},
);
}
ServiceLocator.dart
final serviceLocator = GetIt.instance;
Future<void> initDependencies() async {
await _initSharedPref();
_initSession();
_initGraphQLClient();
_initGraphQLService();
_initMapper();
_initRepository();
}
Future<void> _initSharedPref() async {
SharedPreferences sharedPref = await SharedPreferences.getInstance();
serviceLocator.registerSingleton<SharedPreferences>(sharedPref);
}
void _initSession() {
serviceLocator.registerLazySingleton<Session>(()=>SessionImpl(serviceLocator()));
}
void _initGraphQLClient() {
serviceLocator.registerLazySingleton<GraphQLClient>(() => GraphQLClientGenerator(serviceLocator()).getClient());
}
void _initGraphQLService() {
serviceLocator.registerLazySingleton<HomeGraphQLService>(() => HomeGraphQLService(serviceLocator()));
}
void _initMapper() {
serviceLocator.registerLazySingleton<HomeMapper>(() => HomeMapper());
}
void _initRepository() {
serviceLocator.registerLazySingleton<HomeRepository>(() => HomeRepositoryImpl(serviceLocator(), serviceLocator()));
}
main.dart
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown],
);
await initDependencies();
runApp(MyApp());
}
I cannot say where exactly it is happening because it is elsewhere in your code where you are accessing the GraphQLService, but the problem is definitely due to the lazy loading. The object has not been created and loaded by the locator before it is being accessed. Try updating ServiceLocator.dart to instantiate the classes during registration, like so:
void _initSession() {
serviceLocator.registerSingleton<Session>.(SessionImpl(serviceLocator()));
}
void _initGraphQLClient() {
serviceLocator.registerSingleton<GraphQLClient>(
GraphQLClientGenerator(serviceLocator()).getClient());
}
void _initGraphQLService() {
serviceLocator.registerSingleton<HomeGraphQLService>(
HomeGraphQLService(serviceLocator()));
}
void _initMapper() {
serviceLocator.registerSingleton<HomeMapper>(HomeMapper());
}
void _initRepository() {
serviceLocator.registerSingleton<HomeRepository>(
HomeRepositoryImpl(serviceLocator(), serviceLocator()));
}

How to mock Riverpod's Reader?

I have the following repository and I'd like to test it. I know this may be a silly question but I'm still learning.
class AuthRepository implements AuthBaseRepository {
final Reader _read;
const AuthRepository(this._read);
#override
Future<User> login({String email, String password}) async {
try {
final response = await _read(dioProvider).post(
'/sign_in',
data: {
"user": {
"email": email,
"password": password,
},
},
);
return _mapUserFromResponse(response);
} on DioError catch (_) {
throw const CustomException(message: 'Invalid login credentials.');
} on SocketException catch (_) {
const message = 'Please check your connection.';
throw const CustomException(message: message);
}
}
And this is what I've done so far:
void main() {
test('loadUser', () async {
Dio dio;
DioAdapterMockito dioAdapterMockito;
AuthRepository repository;
setUpAll(() {
dio = Dio();
dioAdapterMockito = DioAdapterMockito();
dio.httpClientAdapter = dioAdapterMockito;
repository = AuthRepository(_reader_here_);
});
test('mocks any request/response via fetch method', () async {
final responsePayload =
await parseJsonFromAssets("assets/api-response.json");
final responseBody = ResponseBody.fromString(
responsePayload,
200,
headers: {
Headers.contentTypeHeader: [Headers.jsonContentType],
},
);
when(dioAdapterMockito.fetch(any, any, any))
.thenAnswer((_) async => responseBody);
});
});
}
I have no idea of how to mock Reader. Basically, I've seen something like class MyMock extends Mock implements Something but Reader is not a class, it's a function so I'm completely lost.
Any help/tips/examples will be appreciated.
Thanks in advance!
Instead of trying to mock a Reader, create a provider for your repository and use ProviderContainer to read it.
class AuthRepository implements AuthBaseRepository {
const AuthRepository(this._read);
static final provider = Provider<AuthRepository>((ref) => AuthRepository(ref.read));
final Reader _read;
#override
Future<User> login({String email, String password}) async {
...
}
Example usage:
final user = createTestUser();
final container = ProviderContainer(
overrides: [
// Example of how you can mock providers
dio.overrideWithProvider(mockDio),
],
);
final repo = container.read(AuthRepository.provider);
expectLater(
await repo.login(email: 'AzureDiamond', password: 'hunter2'),
user,
);
You could also consider using the overrides in ProviderContainer to mock Dio instead of involving a mocking framework to simplify your tests further.
More on testing here.

How to get CONTEXT for the provider to work? Flutter

In the Future fetchStudentInfo() function, i would like to use the userId from my Auth class to do filtering. The userId is embedded in the URL and it will retrieve data from database. But, the issue is that the context is lacking in the function itself. However, I couldn't figure out a way to pass in the context. It would be great if any legend could help me. The solution which retrieve data from internet is found on the flutter documentation. And i wouldn't like to hard code the userId.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
import '../model/student.dart';
import '../provider/auth.dart';
Future<Student> fetchStudentInfo() async {
final auth = Provider.of<Auth>(context);
final response = await http.post(
'https://intermediary-sharpe.000webhostapp.com/Student/read_one.php?userId=$auth.userId');
if (response.statusCode == 200) {
return Student.fromJson(json.decode(response.body));
} else {
throw Exception('Failed');
}
}
class ProfileScreen extends StatefulWidget {
#override
_ProfileScreenState createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
Future<Student> student;
#override
void initState() {
// TODO: implement initState
super.initState();
student = fetchStudentInfo();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<Student>(
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data.studentId);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
return CircularProgressIndicator();
},
future: student,
),
);
}
}
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
import '../model/http_exception.dart';
class Auth with ChangeNotifier {
String _token;
DateTime _expiryDate;
String userId;
Timer _authTimer;
bool get isAuthenticated {
return token != null;
}
String get token {
if (_expiryDate != null &&
_expiryDate.isAfter(DateTime.now()) &&
_token != null) {
return _token;
}
return null;
}
Future<void> _authenticate(
String email, String password, String urlSegment) async {
final url =
'https://identitytoolkit.googleapis.com/v1/accounts:$urlSegment?key=AIzaSyCkNZysDY4PGpScw2jUlBpd0mvpGjgSEag';
try {
final response = await http.post(
url,
body: json.encode(
{
'email': email,
'password': password,
'returnSecureToken': true,
},
),
);
final responseData = json.decode(response.body);
if (responseData['error'] != null) {
throw HttpException(responseData['error']['message']);
}
_token = responseData['idToken'];
userId = responseData['localId'];
_expiryDate = DateTime.now().add(
Duration(
seconds: int.parse(
responseData['expiresIn'],
),
),
);
_autoLogout();
notifyListeners();
final prefs = await SharedPreferences.getInstance();
final userData = json.encode({
'token': _token,
'userId': userId,
'expiryDate': _expiryDate.toIso8601String(),
});
prefs.setString('userData', userData);
} catch (error) {
throw error;
}
}
//Auto Login Function
Future<bool> tryAutoLogin() async {
final prefs = await SharedPreferences.getInstance();
if (!prefs.containsKey('userData')) {
return false;
}
final extractedUserData =
json.decode(prefs.getString('userData')) as Map<String, Object>;
final expiryDate = DateTime.parse(extractedUserData['expiryDate']);
if (expiryDate.isBefore(DateTime.now())) {
return false;
}
_token = extractedUserData['token'];
userId = extractedUserData['userId'];
_expiryDate = expiryDate;
notifyListeners();
_autoLogout();
return true;
}
//SignUp function
Future<void> signUp(String email, String password) async {
return _authenticate(email, password, 'signUp');
}
//Login Function
Future<void> login(String email, String password) async {
return _authenticate(email, password, 'signInWithPassword');
}
//Logout Function
Future<void> logout() async {
_token = null;
userId = null;
_expiryDate = null;
if (_authTimer != null) {
_authTimer.cancel();
_authTimer = null;
}
notifyListeners();
final prefs = await SharedPreferences.getInstance();
prefs.clear();
}
//Auto Logout function
void _autoLogout() {
if (_authTimer != null) {
_authTimer.cancel();
}
final timeToExpiry = _expiryDate.difference(DateTime.now()).inSeconds;
_authTimer = Timer(Duration(seconds: timeToExpiry), logout);
}
//PHP related functions
}
Thank you in advance.
I agree with #lyio, you need to modify the function to pass the context, however after passing context, you cannot call it from initState as stated in documentation of initState
BuildContext.dependOnInheritedWidgetOfExactType from this method. However, didChangeDependencies will be called immediately following this method, and BuildContext.dependOnInheritedWidgetOfExactType can be used there.
Getting provider with Provider.of(context) under the hood is using the inherited widget, so cannot be called using context from initState
So implement instead of initState use didChangeDependencies to call your fetchStudentsInfo(context) method
Wouldn't the easiest solution be to pass the context into fetchStudentInfo?
You would change fetchStudentInfo() to fetchStudentInfo(BuildContext context). And then, when you call the method you pass in the required context. That way, you have the appropriate context available.
If you are not using the `fetchStudentInfo()` outside of the state class, then just move that method into the state class and the issue will be resolved.
Since Any state class has a context getter defined by default./
I just realized how improper this answer was.
Update:
According to the answer by #dlohani, didChangeDependencies should be used in stead of initState.
So what you can do is following:
Pass BuildContext as parameter in the fetchStudentInfo method
Override didChangeDependencies in state class & call fetchStudentInfo from here instead of initState

Flutter: How to use SharedPreferences synchronously?

I am using Shared Preferences in my Flutter app and what I would like to do is store SharedPreferences as a field on startup and then use it synchronously in the app. However I'm not sure if I'm not missing anything.
What I want to achieve is instead of:
method1() async {
SharedPreferences sp = await SharedPreferences.getInstance();
return sp.getString('someKey');
}
to
SharedPreferences sp;
//I would probably pass SharedPreferences in constructor, but the idea is the same
someInitMethod() async {
sp = await SharedPreferences.getInstance();
}
method1() {
return sp.getString('someKey');
}
method2() {
return sp.getString('someKey2');
}
method3() {
return sp.getString('someKey3');
}
In that way I would achieve synchronous access to sharedPrefs. Is it bad solution?
EDIT:
What is worth mentioning is that getInstance method will only check for instance and if there is any than it returns it, so as I see it, is that async is only needed to initialize instance. And both set and get methods are sync anyway.
static Future<SharedPreferences> getInstance() async {
if (_instance == null) {
final Map<String, Object> fromSystem =
await _kChannel.invokeMethod('getAll');
assert(fromSystem != null);
// Strip the flutter. prefix from the returned preferences.
final Map<String, Object> preferencesMap = <String, Object>{};
for (String key in fromSystem.keys) {
assert(key.startsWith(_prefix));
preferencesMap[key.substring(_prefix.length)] = fromSystem[key];
}
_instance = new SharedPreferences._(preferencesMap);
}
return _instance;
}
I use the same approach as the original poster suggests i.e. I have a global variable (actually a static field in a class that I use for all such variables) which I initialise to the shared preferences something like this:
in globals.dart:
class App {
static SharedPreferences localStorage;
static Future init() async {
localStorage = await SharedPreferences.getInstance();
}
}
in main.dart:
void main() {
start();
}
Async.Future start() async {
await App.init();
localStorage.set('userName','Bob');
print('User name is: ${localStorage.get('userName)'}'); //prints 'Bob'
}
The above worked fine but I found that if I tried to use App.localStorage from another dart file e.g. settings.dart it would not work because App.localStorage was null but I could not understand how it had become null.
Turns out the problem was that the import statement in settings.dart was import 'package:<packagename>/src/globals.dart'; when it should have been import 'globals.dart;.
#iBob101 's answer is good, but still, you have to wait before you use the SharedPreferences for the first time.
The whole point is NOT to await for your SharedPreferences and be sure that it will always be NOT NULL.
Since you'll have to wait anyway let's do it in the main() method:
class App {
static SharedPreferences localStorage;
static Future init() async {
localStorage = await SharedPreferences.getInstance();
}
}
And the main method:
void main() async{
await SharedPref.initSharedPref();
runApp(MyApp());
}
the line await SharedPref.initSharedPref(); takes ~100ms to execute. This is the only drawback as far as I can see.
But you definitely know that in every place in the app your sharedPreferenes instance in NOT NULL and ready for accessing it:
String s = App.localStorage.getString(PREF_MY_STRING_VALUE);
I think it's worthwhile
The cleanest way is to retrieve SharedPreferences in main method and pass it to MyApp as a dependency:
void main() async {
// Takes ~50ms to get in iOS Simulator.
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
runApp(MyApp(sharedPreferences: sharedPreferences));
}
class MyApp extends StatefulWidget {
final SharedPreferences sharedPreferences;
const MyApp({Key key, this.sharedPreferences})
: assert(sharedPreferences != null),
super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
// You can access shared preferences via widget.sharedPreferences
return ...
}
I made a simple way to using this PrefUtil class:
import 'package:shared_preferences/shared_preferences.dart';
class PrefUtil {
static late final SharedPreferences preferences;
static bool _init = false;
static Future init() async {
if (_init) return;
preferences = await SharedPreferences.getInstance();
_init = true;
return preferences;
}
static setValue(String key, Object value) {
switch (value.runtimeType) {
case String:
preferences.setString(key, value as String);
break;
case bool:
preferences.setBool(key, value as bool);
break;
case int:
preferences.setInt(key, value as int);
break;
default:
}
}
static Object getValue(String key, Object defaultValue) {
switch (defaultValue.runtimeType) {
case String:
return preferences.getString(key) ?? "";
case bool:
return preferences.getBool(key) ?? false;
case int:
return preferences.getInt(key) ?? 0;
default:
return defaultValue;
}
}
}
In main.dart:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
PrefUtil.init();
.....
Save it like:
PrefUtil.setValue("isLogin", true);
Get the value like:
PrefUtil.getValue("isLogin", false) as bool
By this, it will initialize only once and get it where ever you need.
You can use FutureBuilder to render the loading screen while waiting for SharedPreferences to be intialized for the first time in a singleton-like class. After that, you can access it synchronously inside the children.
local_storage.dart
class LocalStorage {
static late final SharedPreferences instance;
static bool _init = false;
static Future init() async {
if (_init) return;
instance = await SharedPreferences.getInstance();
_init = true;
return instance;
}
}
app_page.dart
final Future _storageFuture = LocalStorage.init();
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _storageFuture,
builder: (context, snapshot) {
Widget child;
if (snapshot.connectionState == ConnectionState.done) {
child = MyPage();
} else if (snapshot.hasError) {
child = Text('Error: ${snapshot.error}');
} else {
child = Text('Loading...');
}
return Scaffold(
body: Center(child: child),
);
},
);
}
my_page.dart
return Text(LocalStorage.instance.getString(kUserToken) ?? 'Empty');
call shared prefs on startup of a stateful main app (we call ours a initState() override of a StatefulWidget after super.initState())
after shared prefs inits, set the value to a field on main (ex: String _someKey)
inject this field into any child component
You can the call setState() on _someKey at you leisure and it will persist to children injected with your field