How to use SharedPreferences and Injectable in Flutter? - flutter

Im using the library Injectable for Dependency Injection in flutter but Im getting a error where I cannot use SharedPreferences.
Error:
Exception has occurred.
FlutterError (ServicesBinding.defaultBinaryMessenger was accessed before the binding was initialized.
If you're running an application and need to access the binary messenger before runApp() has been called (for example, during plugin initialization), then you need to explicitly call the WidgetsFlutterBinding.ensureInitialized() first.
If you're running a test, you can call the TestWidgetsFlutterBinding.ensureInitialized() as the first line in your test's main() method to initialize the binding.)
I've tryed creating a class and put #lazySingleton
Future<SharedPreferences> get prefs => SharedPreferences.getInstance();
and I tryed to put WidgetsFlutterBinding.ensureInitialized()
void main() {
WidgetsFlutterBinding.ensureInitialized();
configureInjection(Environment.prod);
runApp(MyApp());
}

you can pre-await the future in SharedPreference by annotating with #preResolve
#module
abstract class InjectionModule {
//injecting third party libraries
#preResolve
Future<SharedPreferences> get prefs => SharedPreferences.getInstance();
}
and on the configureInjection class
final GetIt getIt = GetIt.instance;
#injectableInit
Future<void> configureInjection(String env) async {
await $initGetIt(getIt, environment: env);
}
and also on the main class
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await configureInjection(Environment.prod);
runApp(MyApp());
}
To actually use:
final prefs = getIt<SharedPreferences>();
await prefs.setString('city', city);
NOT:
final module = getIt<InjectionModule>();
module.prefs.setString('test', test);
Note differences between SharedPreferences and InjectionModule.

Below is the way i got it to work, no guarantee it's the best method.
Await the configureInjection method in the main method.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await configureInjection(Env.prod);
runApp(App());
}
And wrap you app in FutureBuilder that makes use of getIt.allReady().
Widget build(context) {
return FutureBuilder(
future: getIt.allReady(),
builder: (context, snapshot) {
if (snapshot.hasData) {
// ... your app widgets
} else {
// ... some progress indicator widget
}
}
);
}
Helpfull links:
https://pub.dev/documentation/injectable/latest/#registering-asynchronous-injectables
https://pub.dev/packages/get_it#synchronizing-asynchronous-initialisations-of-singletons

Related

Getx Storage return null whenever app starts

I have initilized GetStorage() in main() and calling .read() in onReady dunction of GetX Controller but always get null!
Future<void> main() async {
await GetStorage.init();
runApp(const App());
}
class AuthenticationRepository extends GetxController {
static AuthenticationRepository get instance => Get.find();
/// Variables
GetStorage userStorage = GetStorage('User');
#override
void onReady() {
// Firebase User
firebaseUser = Rx<User?>(_auth.currentUser);
firebaseUser.bindStream(_auth.userChanges());
//Session
print('========= BEFORE -- ${userStorage.read('isFirstTime')} ===========');
userStorage.writeIfNull('isFirstTime', 'true');
print('========= AFTER -- ${userStorage.read('isFirstTime')} ============');
}
OUTPUT
================== BEFORE -- null ========================
================== AFTER -- true =========================
I have tried named values also like GetStorage('User');
nothing worked.
You will need to give the container name in init if you are using a custom container name.
So, you have two solutions
1 -> Update your init to this
Future<void> main() async {
await GetStorage.init('User'); // <- add your custom Container name 'User'
runApp(const App());
}
OR
2 Don't use a custom container name and GetStorage uses it's default container name. In this case, update your code while declaring GetStorage object to read and write data
class AuthenticationRepository extends GetxController {
static AuthenticationRepository get instance => Get.find();
/// Variables
GetStorage userStorage = GetStorage(); //<- remove custom container name 'User'
#override
void onReady() {
// Firebase User
firebaseUser = Rx<User?>(_auth.currentUser);
firebaseUser.bindStream(_auth.userChanges());
//Session
print('========= BEFORE -- ${userStorage.read('isFirstTime')} ===========');
userStorage.writeIfNull('isFirstTime', 'true');
print('========= AFTER -- ${userStorage.read('isFirstTime')} ============');
}
Need to add
WidgetsFlutterBinding.ensureInitialized();
Future<void> main() async {
await GetStorage.init();
runApp(const App());
}
Change this to :
void main() async {
await GetStorage.init();
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}

SharedPreferences.getInstance() is throwing _CastError (Null check operator used on a null value)

I am new to flutter and still learning. I am trying to get reference of SharedPreferences instance but I am getting following error
_CastError (Null check operator used on a null value)
This is how my code looks like
app_settings.dart
class AppSettings {
final SharedPreferences _pref;
AppSettings._(this._pref);
static AppSettings? _instance;
static initialize() async {
if (_instance != null) {
// already initialized
return;
}
// instance not found. creating one
var pref = await SharedPreferences.getInstance();
_instance = AppSettings._(pref);
}
}
main.dart
Future<void> main() async {
// initializing application settings
await AppSettings.initialize();
runApp(App());
}
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "hello app",
home: AppHome());
}
}
Upon further debugging I found that the exception is being thrown by invokeMapMethod in
flutter-sdk/flutter/packages/flutter/lib/src/services/platform_channel.dart
which is called by getAll method in
flutter-sdk/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences_platform_interface-2.0.0/lib/method_channel_shared_preferences.dart
Below are the screenshots of error
I was calling SharedPreferences.getInstance() before runApp(). Since platform-specific native bindings were not initialized, which are used by SharedPreferences, it was throwing a null exception. So I added WidgetsFlutterBinding.ensureInitialized() before calling await SharedPreferences.getInstance():
Future<void> main() async {
// initializing application settings
WidgetsFlutterBinding.ensureInitialized();
await AppSettingService.initialize();
runApp(WorkoutTrackerApp());
}
You can learn more from following links
SharedPreferences error in Flutter
What Does WidgetsFlutterBinding.ensureInitialized() do?
ensureInitialized method documentation
How about
static initialize async() {
_instance = _instance?AppSettings(await SharedPreferences.getInstance());
}
Try:
if (preferences == null) return {} as Map;

Flutter/Dart : How to wait for asynchronous task before app starts?

I am working on a dart application where I want to fetch the data present in cache (SharedPreferences) and then show it on UI (home screen) of the app.
Problem : Since SharedPreferences is an await call, my home page loads, tries to read the data and app crashes because data fetch has not yet happened from SharedPreferences, and app loads before that.
How can I not start the app until cache read from SharedPreferences is done?
This is required because I have to display data from SharedPreferences on home page of the app.
Various view files of my project call static function : MyService.getValue(key) which crashes as cacheResponseJson has not populated yet. I want to wait for SharedPreferences to complete before my app starts.
Class MyService {
String _cacheString;
static Map < String, dynamic > cacheResponseJson;
MyService() {
asyncInit();
}
Future < void > asyncInit() async {
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
_cacheString = sharedPreferences.getString(“ConfigCache”);
cacheResponseJson = jsonDecode(ecsCacheString);
}
static String getValue(String key) {
return cacheResponseJson[key];
}
}
void main() {
MyService s = MyService();
}
Any help would be highly appreciated!
You can run code in your main() method, before the call to runApp() that kicks off your application.
For example:
void main() async {
WidgetsFlutterBinding.ensureInitialized(); // makes sure plugins are initialized
final sharedPreferences = MySharedPreferencesService(); // however you create your service
final config = await sharedPreferences.get('config');
runApp(MyApp(config: config));
}
Can you try wrapping the function asyncInit() in initstate then in the function then setstate the values
_cacheString = sharedPreferences.getString(“ConfigCache”);
cacheResponseJson = jsonDecode(ecsCacheString);
I hope it works.
avoid using initialization etc outside the runApp() function, you can create a singleton
class MyService{
MyService._oneTime();
static final _instance = MyService._oneTime();
factory MyService(){
return _instance;
}
Future <bool> asyncInit() async {
//do stuff
return true;
}
}
and incorporate that in the UI like this
runApp(
FutureBuilder(
future: MyService().asyncInit(),
builder: (_,snap){
if(snap.hasData){
//here you can use the MyService singleton and its members
return MaterialApp();
}
return CircularProgressIndicator();
},
)
);
if you take this approach you can do any UI related feedback for the user while the data loads

how to pass Future<int> to super class

I am trying to learn Flutter and BLoC pattern. So, I created a simple counter app. Initially count value is 0 and it increases/decreases as respective buttons are pressed. The initial value(zero) is sent to the initial state as follows.
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterInitialState(0));
The Counter app worked as expected, but whenever I restart the app count starts from zero. Now I wish to start the count from where I left. I read about SharedPreferences and could successfully save the current value. But I can't load the value and send it to CounterInitialState()(The argument type 'Future' can't be assigned to the parameter type 'int'). How can I achieve this?
My counter_bloc.dart looks like this;
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:sampleOne/config/config.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'counter_event.dart';
part 'counter_state.dart';
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterInitialState(0));
#override
Stream<CounterState> mapEventToState(
CounterEvent event,
) async* {
if (event is CounterIncrementEvent) {
yield* _mapIncrementEvent(event.countVal);
}
if (event is CounterDecrementEvent) {
yield* _mapDecrementEvent(event.countVal);
}
}
}
Stream<CounterState> _mapIncrementEvent(val) async* {
await saveData(val);
yield CounterIncrementState(val + 1);
}
Stream<CounterState> _mapDecrementEvent(val) async* {
await saveData(val);
yield CounterDecrementState(val + 1);
}
Future<bool> saveData(int val) async {
SharedPreferences pref = await SharedPreferences.getInstance();
return pref.setInt('key', val + 1);
}
Future<int> loadData() async {
SharedPreferences pref = await SharedPreferences.getInstance();
return pref.getInt('key') ?? 0;
}
Please help.
you need to wrap your widget with a FutureBuilder widget.
for example
FutureBuilder<int>(
future: _bloc.loadData(),
initalData: 0,
builder: (context, snapshot) {
return Text(snapshot.data.toString());
}
)
Where do you call loadData()?
Maybe you need to to put an await before like this:
Future<int> data = loadData();
int data = await loadData();
Your function is asynchronous so it returns a Future, add await to get an integer

How to set Provider's data to data that stored in SharedPreferences in Flutter?

I store bool isDarkTheme variable in my General provider class, I can acces it whenever I want.
The thing I want to do is to save that theme preference of user and whenever user opens app again, instead of again isDarkThem = false I want it to load from preference that I stored in SharedPreferences.
Here is my code of General provider: (I guess it is readable)
import 'package:shared_preferences/shared_preferences.dart';
class General with ChangeNotifier {
bool isDarkTheme = false;
General() {
loadDefaultTheme();
}
void loadDefaultTheme() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
isDarkTheme = prefs.getBool("is_dark_theme") ?? false;
}
void reverseTheme() {
isDarkTheme = !isDarkTheme;
notifyListeners();
saveThemePreference(isDarkTheme);
}
void saveThemePreference(bool isDarkTheme) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool("is_dark_theme", isDarkTheme);
}
}
Dart does not support async constructors so I think we should take another approach here. I usually create a splash screen (or loading screen, whatever you call it) to load all basic data right after the app is opened.
But if you only want to fetch theme data, you can use the async/await pair in main method:
void main() async {
WidgetsFlutterBinding.ensureInitialized(); // this line is needed to use async/await in main()
final prefs = await SharedPreferences.getInstance();
final isDarkTheme = prefs.getBool("is_dark_theme") ?? false;
runApp(MyApp(isDarkTheme));
}
After that, we can pass that piece of theme data to the General constructor:
class MyApp extends StatelessWidget {
final bool isDarkTheme;
MyApp(this.isDarkTheme);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => General(isDarkTheme), // pass it here
child: MaterialApp(
home: YourScreen(),
),
);
}
}
We should change a bit in the General class as well, the loadDefaultTheme method is left out.
class General with ChangeNotifier {
bool isDarkTheme;
General(this.isDarkTheme);
// ...
}