SharedPreference does not read local data in Flutter - flutter

In a Flutter project, on clicking the "SignUp" button, I need 2 things,
Saving data to cloud
Saving data to local storage.
I user SharedPreferece to save the data and to retrieve it. The problem is, the data I saved into local storage is available immediately after I Sign Up, but if I hot reload the emulator, the data shows null!
The function by which I saved the data to both cloud and local storage:
Future <void> _saveDataToFirestore(User? currentUser) async{
await FirebaseFirestore.instance.collection("sellers").doc(currentUser!.uid).set({
"sellerUID": currentUser.uid,
"sellerName": _fullNameController.text.trim(),
"sellerAvatarUrl": sellerImageUrl,
"sellerEmail": currentUser.email,
"phone": _phoneNumberController.text.trim(),
"address": completeAddress,
"status": "approved",
"earnings": 0.0,
"lat": position!.latitude,
"lng": position!.longitude
});
// save 3 data locally
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
await sharedPreferences.setString("uid", currentUser.uid);
await sharedPreferences.setString("email", currentUser.email.toString()); // do not take from controllers, because it will not be null if sign up fail
await sharedPreferences.setString("name", _fullNameController.text);
await sharedPreferences.setString("image", sellerImageUrl);
print("${currentUser.uid},${currentUser.email.toString()}, ${_fullNameController.text}, ${sellerImageUrl} ");}
I initialized SharedPreference in main.dart
Future <void> main() async {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences.setMockInitialValues({});
await SharedPreferences.getInstance();
await Firebase.initializeApp();
runApp(const MyApp());
}
The home_screen where I needed the local storage data
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:food_fancy_chef/authentication/auth_screen.dart';
import 'package:food_fancy_chef/authentication/login.dart';
import 'package:food_fancy_chef/authentication/register.dart';
import 'package:food_fancy_chef/global/global.dart';
import 'package:shared_preferences/shared_preferences.dart';
class HomeScreen extends StatefulWidget {
static const routeName = "home_screen";
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
String sharedName = "Null value";
Future<void> _getIntFromSharedPref()async{
final pref = await SharedPreferences.getInstance();
final startupName = pref.getString("name");
if(startupName == null){
sharedName = "no name";
} else{
setState(() {
sharedName = startupName;
});
}
}
#override
void initState() {
_getIntFromSharedPref();
super.initState();
}
#override
void didChangeDependencies() {
_getIntFromSharedPref();
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
flexibleSpace: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.cyan,
Colors.amber
],
begin: FractionalOffset(0.0, 0.0),
end: FractionalOffset(1.0, 1.6),
stops: [0.0,1.0],
tileMode: TileMode.mirror
)
),
),
title: Text(
sharedName // I need "name" from local storage
// sharedPreferences!.getString("name")! == null? "null value":sharedPreferences!.getString("name")!
)
),
body: Column(
children: [
Center(
child: ElevatedButton(
onPressed: (){
setState(() {
firebaseAuth.signOut();
Navigator.pushReplacement(context, MaterialPageRoute(builder: (c)=> LoginScreen()));
});
},
child: Text("Log Out"),
),
),
Center(
child: ElevatedButton(
onPressed: (){
setState(() {
firebaseAuth.signOut();
Navigator.pushReplacement(context, MaterialPageRoute(builder: (c)=> AuthScreen()));
});
},
child: Text("signout"),
),
),
],
),
);
}
}
What I have tried:
I have tried to get the data directly without any init() method, it returns null:
sharedPreferences!.getString("name")! == null? "null value":sharedPreferences!.getString("name")!
I have declared the variable first and assigned the value via a function run at init(), code is below.
I tried the same process above but with didChangeDependencies() method.
I used both init() and didChangeDependencies()
I deleted the emulator and reinstalled it.
I also saved SharePreferences() in a global.dart file, so that, I can access them anywhere in the project.

Try this
Future<void> _getIntFromSharedPref() async{
SharedPreferences? pref = await SharedPreferences.getInstance();
String startupName = pref!.getString("name") ?? 'no name';
setState(() {});
}

Try removing SharedPreferences.setMockInitialValues({}); from main.dart.

When hot restart the widget tree re built it means only this
Widget build(BuildContext context) method triggered. initState is called only once for a widget and didChangeDependencies may be called multiple times per widget lifecycle. So initializing some thing on didChangeDependencies you have to be careful. Calling like this will resolve your issue whille hot restart
#override
Widget build(BuildContext context) {
_getIntFromSharedPref();
return Scaffold(
);
}

Related

Why flutter Navigator.of(context).push(MaterialPageRoute(()) not working

I have a small app here, i will check buildNumber of current app and compare to my remote api data, based on this condition i will show the user interfaces.
I have home and updateApp screen where home is the normal webview screen and UpdateApp is a screen where user is required to update the new version of my app.
But condition satisfies but update screen is not showing.
// ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, use_build_context_synchronously, unrelated_type_equality_checks, unused_element
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
import 'package:webview_test/models/app_version.dart';
import 'package:webview_test/services/remote_service.dart';
import 'package:webview_test/views/update_app.dart';
import 'package:package_info_plus/package_info_plus.dart';
void main() {
runApp(MyHomePage());
}
class MyHomePage extends StatefulWidget {
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final flutterWebViewPlugin = FlutterWebviewPlugin();
bool isLoading = true;
double webProgress = 0;
bool isLoaded = false;
List<AppVersion>? appVersions;
int buildNumber = 0;
late String packageName;
#override
#override
void initState() {
super.initState();
flutterWebViewPlugin.onProgressChanged.listen((double progress) {
setState(() {
this.webProgress = progress;
});
print("The progress is $progress");
});
getVersions();
getBuild();
}
//Fetching remote data for app versions.
getVersions() async {
appVersions = await RemoteService().getAppVersion();
if (appVersions != null) {
setState(() {
isLoaded = true;
});
}
}
//getting app information to compare remote app versions.
getBuild() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
packageName = packageInfo.packageName;
buildNumber = int.parse(packageInfo.buildNumber);
print("build number is $buildNumber");
if (buildNumber == 1) {
print("Build number is $buildNumber");
}
}
#override
Widget build(BuildContext context) {
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.white,
systemNavigationBarIconBrightness: Brightness.dark));
return MaterialApp(
home: buildNumber == 1
? proceedToUpdate(context)
: SafeArea(
child: Scaffold(
body: WillPopScope(
onWillPop: () async {
if (await flutterWebViewPlugin.canGoBack()) {
flutterWebViewPlugin.goBack();
return false;
} else {
SystemNavigator.pop();
return true;
}
},
child: Stack(
children: [
Positioned.fill(
child: Column(
children: [
webProgress < 1
? SizedBox(
height: 5,
child: LinearProgressIndicator(
value: webProgress,
color: Colors.blue,
backgroundColor: Colors.white,
),
)
: SizedBox(),
Expanded(
child: WebviewScaffold(
url: "https://google.com",
mediaPlaybackRequiresUserGesture: false,
withLocalStorage: true,
),
),
// isLoading
// ? Center(
// child: CircularProgressIndicator(),
// )
// : Stack(),
],
),
),
],
)),
),
),
);
}
proceedToUpdate(context) {
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => UpdateApp()));
}
}
Your variable context in Navigator.of(context).push(...) isn't correct.
You're trying to navigate outside build(BuildContext context), so it won't work. Function build(BuildContext context) is the place where it build your mobile interface - UI screen.
Now in your StatefulWidget MyHomePage -> initState() -> getBuild() -> _proceedToUpdate() -> Navigator.of(context).push(...). The variable context in your Navigator command is not context of your screen UI. Even though function _proceedToUpdate() can run, it cannot navigate.
You may try to show an dialog. Each dialog also has a context. You can show an dialog and then navigate to somewhere when press "OK" button. It'll success.
Good luck!
Update:
Seems like you don't want to show any dialog, therefore we need another approach. You could check the build version in main() async {}. Then pass value buildNumber to somewhere (directly pass to MyApp() or use singleton to make it more professional :D). Then you can make it like: home: _getFirstScreen()
_getFirstScreen() {
if (buildNumber == 1) return UpdateScreen();
else return MyHomePage();
}

While using Navigator in an async function in flutter, I get the warning "Do not use BuildContexts across async gaps." Please tell me the workaround [duplicate]

This question already has answers here:
Do not use BuildContexts across async gaps
(10 answers)
Closed 6 months ago.
I am trying to get the weather of a location as a result of api call to the OpenWeatherApi. The async function getLocationAndWeatherData() is used to get the data. Now after getting the data, I need to send this data to a new screen. So I've used the arguments parameter. Now, when I use the Navigator.pushNamed() after getting the weather data, I'm getting the warning as mentioned in the question. So what's the workaround?
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import '../services/location.dart';
import '../services/networking.dart';
class LoadingScreen extends StatefulWidget {
#override
_LoadingScreenState createState() => _LoadingScreenState();
}
class _LoadingScreenState extends State<LoadingScreen> {
#override
void initState() {
super.initState();
}
bool pressed = false;
Widget loadingAndNext() {
setState(() {
pressed = false;
});
return Center(
child: SpinKitDoubleBounce(
color: Colors.white,
size: 50.0,
),
);
}
Widget mainScreen() {
return Center(
child: TextButton(
onPressed: () {
setState(() {
pressed = true;
getLocationAndWeatherData();
});
},
child: Container(
padding: EdgeInsets.all(18.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
color: Colors.white,
),
child: Text("Get Location"),
),
),
);
}
Future<dynamic> getLocationAndWeatherData() async {
Location location = Location();
await location.getCurrentLocation();
double lat = location.getLatitude();
double lon = location.getLongitude();
NetworkHelper networkHelper = NetworkHelper(lat: lat, lon: lon);
var x = await networkHelper.getData();
Navigator.pushNamed(context, "/location", arguments: {'weatherData': x});
pressed = false;
}
#override
Widget build(BuildContext context) {
return Scaffold(body: !pressed ? mainScreen() : loadingAndNext());
}
}
After awaiting for the result in getLocationAndWeatherData(),when I use the Navigator.pushNamed(),I get a warning that I shouldn't use BuildContexts here. If I use this method in setState, I need it to be asynchronous. Can I use async and await in setState? If not, how do I get rid of this warning?
try
if (!mounted) return ;
right before Navigation

Holding a state within StatefulWidget

I have a settings page where I'm holding a path for a keyfile in SavedPreferences. It is also possible to redefine the path for keyfile in this settings page.
class Settings extends StatefulWidget {
const Settings({Key? key}) : super(key: key);
#override
_SettingsState createState() => _SettingsState();
}
class _SettingsState extends State<Settings> {
void initState() {
getSettings();
super.initState();
}
void getSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
_keyPath = prefs.getString('keyPath')!;
_keyFile = _keyPath.split('/').last;
}
String _keyPath = '';
String _keyFile = '';
Future<void> openFile() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
_keyPath = (await FlutterFileDialog.pickFile())!;
setState(() {
print(_keyPath);
_keyFile = _keyPath.split('/').last;
prefs.setString('keyPath', _keyPath);
});
}
#override
Widget build(BuildContext context) {
getSettings();
return Scaffold(
appBar: AppBar(
title: const Text('Settings'),
),
body: Column(
children: [
Row(
children: [
Expanded(
child: Column(
children: [
Text('Key File: '),
Text(_keyFile),
],
),
),
Expanded(
child: ElevatedButton(onPressed: openFile, child: Text('Open')),
)
],
)
],
),
);
}
}
This works fine when initializing for first time but when the Widget is already initialized and the navigated back second time I'm having trouble to use the saved key in SharedPreferences when navigating back to this page.
I know I'm getting the value for _keyFile and _keyPath when renavigating in
String _keyPath = '';
String _keyFile = '';
Cant figure out how to call async function when renavigating to widget without initState to use SharedPreferences
I guess this should be done via state and not to query the items from SharedPreferences but I'm little clueless how to do this exactly.
I would suggest you to use a FutureBuilder instead of getting SharedPreferences in InitState:
FutureBuilder(
future: SharedPreferences.getInstance(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
// set keys and show your Column widget
} else if (snapshot.hasError) {
// Show error widget
} else {
// Show loading Widget
}
},
),
);
Like this you will get the saved value in your SharedPreferences everytime you navigate to this widget. For more information, you can check the doc link.
For example you can use GlobalKey to store the scaffold state in use to show snackbar and etc...

OnSharedPreferenceChangeListener for Flutter

In Android, you can do the following to listen to shared preference change
SharedPreferences.OnSharedPreferenceChangeListener spChanged = new
SharedPreferences.OnSharedPreferenceChangeListener() {
#Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
// your stuff here
}
};
Is it possible to do this using flutter? I have read through the official flutter shared_preference and this features seems not yet implemented.
Is there any other library or ways to achieve the above without diving into native code. Thanks.
You can easily "listen" to SharedPreferences using a package like flutter_riverpod.
Initialize sharedPreferences
SharedPreferences? sharedPreferences;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
sharedPreferences = await SharedPreferences.getInstance();
runApp(const ProviderScope(child: MyApp()));
}
Create the stateProvider
import 'package:hooks_riverpod/hooks_riverpod.dart';
final keepOnTopProvider = StateProvider<bool>((ref) {
return sharedPreferences?.getBool('on_top') ?? true;
});
Update your UI when something changes
class SettingsView extends ConsumerWidget {
const SettingsView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
bool onTop = ref.watch(keepOnTopProvider);
return Scaffold(
appBar: AppBar(title: const Text('Settings'), centerTitle: false),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 12),
children: [
SwitchListTile(
title: const Text('Keep on top'),
value: onTop,
onChanged: (value) async {
sharedPreferences?.setBool('on_top', value);
ref.read(keepOnTopProvider.notifier).state = value;
await windowManager.setAlwaysOnTop(value);
},
),
],
),
);
}
}
As a work around, add the following codes to your main():
void funTimerMain() async {
// here check any changes to SharedPreferences, sqflite, Global Variables etc...
if (bolAnythingChanged) {
// do something
// 'refresh' any page you want (below line using Redux as example)
GlobalVariables.storeHome.dispatch(Actions.Increment);
}
// recall this timer every x milliseconds
new Future.delayed(new Duration(milliseconds: 1000), () async {
funTimerMain();
});
}
// call the timer for the first time
funTimerMain();

How to use shared preferences to keep user logged in flutter?

I want to keep the user logged in after the user successfully logsin in flutter.
I am using a REST API to retrieve the user name and password of the user. But I want to save those details so that the user can stay logged in. My current situation is i can successfully log the user in but when i restart the app i have to login again so i need to save the details of the user in a shared preference so that the user can stay logged for the entire session until logout.But i am unable to do that so please help me with it.
Thanks in advance
This is the code i have for my login page.
I have removed the UI contents which should be inside the listview as those are not that relevant.
Login.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:restaurant_app/globalVar.dart';
import 'package:restaurant_app/homescreen.dart';
import 'package:restaurant_app/models/auth.dart';
import 'package:restaurant_app/signup.dart';
import 'package:http/http.dart' as http;
import 'package:restaurant_app/utils/authutils.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SignIn extends StatefulWidget {
SignIn({ Key key, this.post }): super(key: key);
#override
_SignInState createState() => _SignInState();
}
class _SignInState extends State<SignIn> with SingleTickerProviderStateMixin
{
TabController controller;
TextEditingController _email = new TextEditingController();
TextEditingController _password = new TextEditingController();
bool loading;
final GlobalKey < ScaffoldState > _scaffoldKey = new GlobalKey<ScaffoldState>
();
#override
void initState() {
// TODO: implement initState
super.initState();
_fetchSessionAndNavigate();
controller = new TabController(length: 2, vsync: this);
loading = false;
}
#override
void dispose() {
// TODO: implement dispose
super.dispose();
controller.dispose();
setState(() {
loading = false;
});
_email.dispose();
_password.dispose();
}
final GlobalKey < FormState > _formKey = GlobalKey<FormState>();
bool _autoValidate = false;
_login(username, password) async {
setState(() {
loading = true;
});
var body = json.encode({
"username": username,
"password": password,
});
Map < String, String > headers = {
'Content-type': 'application/json',
'Accept': 'application/json',
};
await http
.post("${GlobalVar.Ip}/wp-json/jwt-auth/v1/token",
body: body, headers: headers)
.then((response) {
var body = json.decode(response.body);
//var response1;
if (response.statusCode == 200) {
// TODO: you need to store body['token'] to use in some authentication
loading = false;
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (BuildContext ctx) => HomePage()));
} else {
// TODO: alert message
final snackBar = SnackBar(
content: Text(body['message'].toString().trim()),
);
_scaffoldKey.currentState.showSnackBar(snackBar);
}
setState(() {
loading = false;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
resizeToAvoidBottomPadding: false,
body: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('images/art.png'),
fit: BoxFit.fill,
colorFilter: ColorFilter.mode(
Colors.white12.withOpacity(0.2), BlendMode.dstATop),
),
),
child: ListView();
}
You can navigate to the Login page if the user details are saved in the storage else to the Home page with the below code
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences prefs = await SharedPreferences.getInstance();
var email = prefs.getString('email');
print(email);
runApp(MaterialApp(home: email == null ? Login() : Home()));
}
Save the required user details after the successful login
class Login extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
onPressed: () async {
//after the login REST api call && response code ==200
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString('email', 'useremail#gmail.com');
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (BuildContext ctx) => Home()));
},
child: Text('Login'),
),
),
);
}
}
clear the details on logout
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: Center(
child: RaisedButton(
onPressed: () async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.remove('email');
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (BuildContext ctx) => Login()));
},
child: Text('Logout'),
),
),
);
}
}
Hope it helps!
Make sure WidgetFlutterBinding.ensureInitialized() is the first line of main()
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/material.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences prefs = await SharedPreferences.getInstance();
bool login = prefs.getBool("login");
print("login:" + login.toString());
runApp(MaterialApp(home: login == null ? LoginPage(title: 'My App') : HomePage()));
}
class LoginPage extends StatelessWidget { ...
The above answers using SharedPreferences works (make sure you have WidgetsFlutterBinding.ensureInitiazed(); as your first line of main), but it will give you a null on re-start, ie, if you remove the app from recent and re-open it again, it will not re-direct you to the Home or Profile Page. I solved this issue by giving write external storage permission to your app because the shared preferences need to write the data somewhere in your device or emulator.
Just add the write and read external storage permissions in your Android Manifest file and you can use permission_handler plugin for flutter from pub.dev to get the required permissions from user at runtime when the app is opened for the first time and then Shared Preferences won't give you null.
Use user sessions instead. Check out Consession. The package adds user session support in Flutter and is easy to use.
// Store value to session
await Consession().set("token", myJWTToken);
// Retrieve item from session
dynamic token = await Consession().get("token");
Concession is now deprecated in favour of flutter_session. We can now use flutter_session to manage user sessions seamlessly.
//Write values to the session:
await FlutterSession().set("token", myJWTToken);
//Read values from the session:
dynamic token = await FlutterSession().get("token");