Flutter: what's the most performant, seamless way to access SharedPreferences in multiple classes? - flutter

So I call
SharedPReference prefs = await SharedPreferences.getInstance();
In my parent Main.dart. Then when the user presses a button in the ButtonBar it gets sent away using
Navigator.push(context, MaterialPageRoute(
builder: (BuildContext context) => Agenda(prefs: prefs,)));
I'm concerned this is not the most robust or performant way to use shared Preferences on child screens. For instance say the user navigated away from Main.dart before I have an instance of SharedPreferences, then the child screen - Agenda.dart - will have null passed into it as the value of prefs.
What happens then? Is the child screen stuck trying to access fields which are null? When Main.dart async process gets an instance of SharedPreferences does it feed it through to the child screen/class?
Would it be more robust to summon SharedPreferences in every class that uses it? Using:
SharedPReference prefs = await SharedPreferences.getInstance();
I feel passing prefs around is better because it reduces bottlenecks, but I don't fully understand.
When I modify values in SharedPreferences in child classes, then BACK button to it's parent screen, will the parent screen be accessing an updated SharedPreference, automagically?

getInstance() will check _completer , because _completer is static
the first time execution will keep reference in SharedPreferences._(this._preferenceCache)
the next time you call getInstance() will always directly return _completer.future; reference of cache, see picture below
no matter where you call getInstance() , even in SecondRoute
Source code of getInstance()
class SharedPreferences {
SharedPreferences._(this._preferenceCache);
static Completer<SharedPreferences> _completer;
...
static Future<SharedPreferences> getInstance() async {
if (_completer == null) {
_completer = Completer<SharedPreferences>();
try {
final Map<String, Object> preferencesMap =
await _getSharedPreferencesMap();
_completer.complete(SharedPreferences._(preferencesMap));
} on Exception catch (e) {
// If there's an error, explicitly return the future with an error.
// then set the completer to null so we can retry.
_completer.completeError(e);
final Future<SharedPreferences> sharedPrefsFuture = _completer.future;
_completer = null;
return sharedPrefsFuture;
}
}
return _completer.future;
}

What happens then? Is the child screen stuck trying to access fields which are null? When Main.dart async process gets an instance of SharedPreferences does it feed it through to the child screen/class?
This depends on your code. If you await for the SharedPreferences instance and then navigate, it's OK. But if you dont await it, then null will be passed to the next screen and no, it wont be feed through to the child screen/class.
When I modify values in SharedPreferences in child classes, then BACK button to it's parent screen, will the parent screen be accessing an updated SharedPreference, automagically?
Yes, it will, but not automagically. When you pass an object to another class or function in Dart, actually the object itself is not passed. A reference to that object is passed. A reference to an object is like an address to an object's place in memory.
And generally about your question, you can make use of the get_it and injectable packages. get_it is a simple Service Locator for Dart and Flutter projects with some additional goodies and Injectable is a convenient code generator for get_it.
Using these two packages you can register modules, which is a good place for SharedPrefrences. Take a look at this example to learn more about it.
Another way can be making use of an inherited widget. You can provide a value to the whole widget tree, and then you can access that value (that in your case would be SharedPrefrences instance) anywhere in your widgets.

Related

Access Variable form another class in top level function Flutter

I have a background notification handler in my flutter app, which is a top-level function like so:
Future<void> _onBackgroundMessage(RemoteMessage message) async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
final chatClient = StreamChatClient(STREAM_API_KEY);
print(Utils.user?.uid);
print(Utils.user?.streamToken);
chatClient.connectUser(
su.User(id: Utils.user?.uid ?? ''),
Utils.user?.streamToken ?? '',
connectWebSocket: false,
);
NotificationUtils.handleGetStreamNotification(message, chatClient);
SharedPreferences prefs = await SharedPreferences.getInstance();
int appBadgeCounter = prefs.getInt(appBadgePrefsKey) ?? 0;
FlutterAppBadger.updateBadgeCount(appBadgeCounter + 1);
}
In the notification handler, I need to connect to a separate server using a uid and a token. I have a Utils singleton class where I store an app user as a variable. I initialize this Utils app variable in my home page stateful widget class and persist it throughout the app. This way, I can minimize my number of database calls for user data.
However, in this top-level notification handler, the Utils.user is always null. This top level function exists in my home page stateful widget class, but it is a top level function still.
I want to avoid making database calls for every single notification that the app receives. Is there a way to get this Utils. user data without getting null??
you have to setUser before using it in chatClient.connectUser and along with you can check if user is null or not if null initialize it then it stops does not make extra calls.
Future<void> _onBackgroundMessage(RemoteMessage message) async {
...
await Firebase.initializeApp();
final chatClient = StreamChatClient(STREAM_API_KEY);
if(Utils.user != null) {
Utils.setUser();
}
print(Utils.user?.uid);
print(Utils.user?.streamToken);
...
}

How to attend best practice for not using UI code in the Controller with GetX flutter when I need to show a Dialog if my task complete.?

For a simple Email login with OTP code I have a structure as follows.
View
await _signUpCntrl.signUp(email, password);
Controller
_showOtpDialog(email);
_showOtpDialog func
return Get.dialog(
AlertDialog(
So the thing is _showOtpDialog function is inside a controller file. ie. /Controllers/controller_file.dart
I want do something like a blocListener, call the _showOtpDialog from a screen(view) file on signup success. (also relocate the _showOtpDialog to a view file)
Using GetX I have to use one of the builders either obs or getbuilder. Which is I think not a good approach to show a dialog box.
On internet it says Workers are the alternative to BlocListener. However Workers function resides on Controller file and with that the dialog is still being called on the controller file.
As OTP dialog will have its own state and a controller I wanted to put it inside a /view/viewfile.dart
How do I obtain this?
I tried using StateMixin but when I call Get.dialog() it throw an error.
visitChildElements() called during build
Unlike BLoC there's no BlocListener or BlocConsumer in GetX.
Instead GetX has RxWorkers. You can store your response object in a Rx variable:
class SomeController extends GetxController{
final response= Rxn<SomeResponse>();
Future<void> someMethod()async{
response.value = await someApiCall();
}
}
And then right before the return of your widget's build method:
class SomeWidget extends StatelessWidget{
final controller = Get.put(SomeController());
#override
Widget build(BuildContext context){
ever(controller.response, (SomeResponse res){
if(res.success){
return Get.dialog(SuccessDialog()); //Or snackbar, or navigate to another page
}
....
});
return UI();
}
First thing, you will need to enhance the quality of your question by making things more clearly. Add the code block and the number list, highlight those and making emphasize texts are bold. Use the code block instead of quote.
Seconds things, Depends on the state management you are using, we will have different approaches:
Bloc (As you already added to the question tag). By using this state management, you controller ( business logic handler) will act like the view model in the MVVM architecture. In terms of that, You will need to emit a state (e.g: Sent success event). Afterward, the UI will listen to the changes and update it value according to the event you have emitted. See this Bloc example
GetX (As your code and question pointed out): GetX will acts a little bit different. you have multiple ways to implement this:
Using callbacks (passed at the start when calling the send otp function)
Declare a general dialog for your application ( this is the most used when it comes to realization) and calling show Dialog from Bloc
Using Rx. You will define a Reactive Variable for e.g final success = RxBool(true). Then the view will listen and update whenever the success changes.
controller.dart
class MyController extends GetxController {
final success = RxBool(false);
void sendOtp() async {
final result = await repository.sendOTP();
success.update((val) => {true});
}
}
view.dart
class MyUI extends GetView<MyController> {
#override
Widget build(BuildContext context) {
ever(controller.success, (bool success) {
// This will update things whenever success is updated
if (success) {
Get.dialog(AlertDialog());
}
});
return Container();
}
}

Riverpod - ref.onDispose called when object is created, and not when disposed

I have this ViewModel and a Riverpod provider for it:
final signInViewModelProvider = Provider.autoDispose<SignInViewModel>((ref) {
final vm = SignInViewModel();
ref.onDispose(() {
vm.cleanUp();
});
return vm;
});
class SignInViewModel extends VpViewModelNew {
FormGroup get form => _form;
String get emailKey => _emailKey;
String get passwordKey => _passwordKey;
final String _emailKey = UserSignInFieldKeys.email;
final String _passwordKey = UserSignInFieldKeys.password;
final FormGroup _form = FormGroup({
UserSignInFieldKeys.email:
FormControl<String>(validators: [Validators.required]),
UserSignInFieldKeys.password:
FormControl<String>(validators: [Validators.required])
});
void cleanUp() {
print('cleaning up');
}
void onSubmitPressed(BuildContext context) {
// _saveRegistrationLocallyUseCase.invoke(
// form.control(_self.emailKey).value as String ?? '',
// form.control(_self.passwordKey).value as String ?? '');
}
}
abstract class VpViewModelNew {
VpViewModelNew() {
if (onCreate != null) {
onCreate();
print('creating');
}
}
void onCreate() {}
}
When I navigate to the page that has the signInViewModelProvider, it prints to the console:
flutter: signInPage building
flutter: creating
flutter: cleaning up
Then popping the page from the stack with Navigator.pop() prints nothing.
Then navigating to the page again prints the same 3 lines in the same order.
I expected onDispose to be called after Navigator.pop(), and not when navigating to the page that reads the provider. Why is onDispose being called directly after creation, and not when using Navigator.pop() (when I expected the provider to be disposed of since no other views reference it)?
Edit: I access the provider with final viewModel = context.read<SignInViewModel>(signInViewModelProvider);
I don't need to listen since I don't need to rebuild the page on
change. Is consumer less performant for this?
No, the performance is meaningless, even if it's listening it's not really affecting the performance because as a Provider there is no way to notify (which is not the case with a state notifier or change notifier)
Also if you don't care to listen after the value has been read The auto dispose understand no one is watching it and it disposes, it's better to use context.read when using tap or gestures that modify something
(I realize this is late to the party but maybe it'll help somebody)
The Riverpod docs come out pretty strongly against using read for the reason you said, i.e. performance/rebuilding concerns.
Basically you should always use watch except:
If you want your custom callback function called when it updates (use listen)
If the actual reading is happening asynchronously or in response to user action (like in an onPressed): this is the only time to use read.
If you're having issues with your widgets rebuilding too often, Riverpod has some ways to deal with that that don't involve using read.

What's the best way to implement requests with tokens clearly?

I have screen with tabs and each screen implements AutomaticKeepAliveClientMixin. When I navigate to this screen(with tabs), each tab in initState fetches data from server like that:
fetchData()async{
final token = await getToken();//refresh if it is expired.
return fetchData(token);
}
I think it'd better if I initialize data for all the tabs in one request, because I can catch only one refresh token expired and socket exception in single place.
fetchAllData()async{
final token = await getToken();//refresh if it is expired.
return fetchAllData(token);
}
How would you build logic for screen and requests like that? Is my approach is something similar to what you use?
I would recommend you to use a Provider (https://pub.dev/packages/provider). By subscribing to the same Provider, you will be able to reuse the data you've once fetched. For instance, I've used this approach to provide to my App (at different places) the current user:
class UserModel extends ChangeNotifier {
User _currentUser;
void setUser(User user) {
_currentUser = user;
notifyListeners();
}
Future<User> getUser(BuildContext context) async {
if (_currentUser == null) {
_currentUser = await getUserRequest(context, hasRedirect: false);
}
return _currentUser;
}
}
Hope it will fit your needs !
You can add your fetchAllData method to the initState of the widget that holds all of the tabbed widgets. Then, you can you can pass the relevant data to the contructors of each of the tabbed widgets. Not the best solution, but it should work.
I'd still recommend Provider. State management systems are not all inclusive, nor are exclusive. Depending on how your state is presented your could use more than one state management system. Helll, the bloc library already includes the provider library.

HTTP call on screen load in flutter

We have a Features class that we are trying to fill when a screen loads. Its an http call that returns the object. Im struggling with how to do this. All of our http calls are done on a button click:
here is the call
Future<Features> getFeatureStatus(String userID) async {
Features _features;
final response =
await http.post('http://url/api/Features',
headers: {"Content-Type": "application/json",
'Accept': 'application/json',},
body: json.encode({'userID' : userID }));
_features = Features.fromJson(json.decode(response.body));
return _features;
}
When i try to call it at the top of the class I get errors and cant get to the values.
class FlutterReduxApp extends StatelessWidget {
static final User user;
static final Features features = getFeatureStatus(user.userId);
The error I get is -- "A value of type 'Future' can't be assigned to a variable of type 'Features'.
Try changing the type of the variable, or casting the right-hand type to 'Features'.dart(invalid_assignment)"
Im sure im doing something incorrect here but I havent done a screen load call yet.
The getFeatureStatus function is returning a Future<Features> while you're trying to read it as type Features in the stateless widget.
There are different ways to read the value but since you have a button, you could convert the widget into a StatefulWidget then use the onPressed function to read the value and update the state afterwards such as.
onPressed: () async {
features = await getFeatureStatus(user.userId);
setState((){
// do something
});
}
In this case, the value features cannot be a static final so you'll have to change it to Features features.
Edit based on comment:
You could also do this inside an initState:
Features features;
#override
void initState () {
super.initState();
_asyncMethod();
}
_asyncMethod() async {
features = await getFeatureStatus(user.userId);
setState((){});
}
so in the widget build method you could do:
return (features == null)
? CircularProgressIndicator()
: MyWidget(...); // where features is used.
getFeatureStatus(user.userId).than((features)
{
// you will get the features object
//you can work on that object
}
);
calling the getFeaturesStatus method in the initState() when using the statefull.
First thing first, this line static final Features features = getFeatureStatus(user.userId); will not work as you are trying to assign a type Future to the type Features.
The solution for this is to await the future so that it resolves and returns a Feature data type that satisfies your variable named 'features'.
This goes as follows: static final Features features = await getFeatureStatus(user.userId); but this has to be in a separate function which is explicitly defined with the async parameter.
This solves the error in the respect of code that you have written, but as you stated that you want this to load after the screen loads (Or technically, when the main widget is "mounted").
The solution for this logical aspect can be the use of this.mounted.
All widgets have a bool this.mounted property. It turns true when the buildContext is assigned.
In short, suppose you want to run a function after any widget is mounted/loaded, you can test it via
if(this.mounted){
//Whatever you want to do when the widget has been mounted...
}