Using SharedPreferences to set login state and retrieving it at App launch - Flutter - dart

I have an flutter app in which I have to check the login status when the app is launched and call the relevant screen accordingly.
The code used to launch the app:
class MyApp extends StatefulWidget {
#override
MyAppState createState() {
return new MyAppState();
}
}
class MyAppState extends State<MyApp> {
bool isLoggedIn;
Future<bool> getLoginState() async{
SharedPreferences pf = await SharedPreferences.getInstance();
bool loginState = pf.getBool('loginState');
return loginState;
// return pf.commit();
}
#override
Widget build(BuildContext context) {
getLoginState().then((isAuth){
this.isLoggedIn = isAuth;
});
if(this.isLoggedIn) {return Container(child: Text('Logged In'));}
else {return Container(child: Text('Not Logged In));}
}
}
I am able to save the SharedPreference and retrieve it here, the issue is that as getLoginState() is an async function, this.isLoggedIn is null by the time the if condition is executed. The boolean assertion fails in the if statement and the app crashes.
How do I ensure that the bool variable isLoggedIn used in the if condition has a value when the if statement is executed?
Any help would be appreciated.

You can use FutureBuilder to solve this problem.
#override
Widget build(BuildContext context) {
new FutureBuilder<String>(
future: getLoginState(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.active:
case ConnectionState.waiting:
return new Text('Loading...');
case ConnectionState.done:
if (snapshot.hasData) {
loginState = snapshot.data;
if(loginState) {
return Container(child: Text('Logged In'));
}
else {
return Container(child: Text('Not Logged In));
}
} else {
return Container(child: Text('Error..));
}
}
},
)
}
Note: we don't need isLoggedIn state variable.

Related

Flutter set startup page based on Shared Preference

I've been trying without success to load different pages according to my Shared Preference settings.
Based on several posts found in stackoverflow, i end up with the following solution:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:testing/screens/login.dart';
import 'package:testing/screens/home.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Widget page = Login();
Future getSharedPrefs() async {
String user = Preferences.local.getString('user');
if (user != null) {
print(user);
this.page = Home();
}
}
#override
void initState() {
super.initState();
this.getSharedPrefs();
}
#override
Widget build(BuildContext context) {
return MaterialApp(home: this.page);
}
}
class Preferences {
static SharedPreferences local;
/// Initializes the Shared Preferences and sets the info towards a global variable
static Future init() async {
local = await SharedPreferences.getInstance();
}
}
The variable user is not null because the print(user) returns a value as expected, but the login screen is always being opened.
Your problem is that your build method returns before your getSharedPrefs future is complete. The getSharedPrefs returns instantly as soon as it's called because it's async and you're treating it as a "Fire and Forget" by not awaiting. Seeing that you can't await in your initState function that makes sense.
This is where you want to use the FutureBuilder widget. Create a Future that returns a boolean (or enum if you want more states) and use a future builder as your home child to return the correct widget.
Create your future
Future<bool> showLoginPage() async {
var sharedPreferences = await SharedPreferences.getInstance();
// sharedPreferences.setString('user', 'hasuser');
String user = sharedPreferences.getString('user');
return user == null;
}
When user is null this will return true. Use this future in a Future builder to listen to the value changes and respond accordingly.
#override
Widget build(BuildContext context) {
return MaterialApp(home: FutureBuilder<bool>(
future: showLoginPage(),
builder: (buildContext, snapshot) {
if(snapshot.hasData) {
if(snapshot.data){
// Return your login here
return Container(color: Colors.blue);
}
// Return your home here
return Container(color: Colors.red);
} else {
// Return loading screen while reading preferences
return Center(child: CircularProgressIndicator());
}
},
));
}
I ran this code and it works fine. You should see a blue screen when login is required and a red screen when there's a user present. Uncomment the line in showLoginPage to test.
There is a much pretty way of doing this.
Assuming that you have some routes and a boolean SharedPreference key called initialized.
You need to use the WidgetsFlutterBinding.ensureInitialized() function before calling runApp() method.
void main() async {
var mapp;
var routes = <String, WidgetBuilder>{
'/initialize': (BuildContext context) => Initialize(),
'/register': (BuildContext context) => Register(),
'/home': (BuildContext context) => Home(),
};
print("Initializing.");
WidgetsFlutterBinding.ensureInitialized();
await SharedPreferencesClass.restore("initialized").then((value) {
if (value) {
mapp = MaterialApp(
debugShowCheckedModeBanner: false,
title: 'AppName',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routes: routes,
home: Home(),
);
} else {
mapp = MaterialApp(
debugShowCheckedModeBanner: false,
title: 'AppName',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routes: routes,
home: Initialize(),
);
}
});
print("Done.");
runApp(mapp);
}
The SharedPreference Class Code :
class SharedPreferencesClass {
static Future restore(String key) async {
final SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
return (sharedPrefs.get(key) ?? false);
}
static save(String key, dynamic value) async {
final SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
if (value is bool) {
sharedPrefs.setBool(key, value);
} else if (value is String) {
sharedPrefs.setString(key, value);
} else if (value is int) {
sharedPrefs.setInt(key, value);
} else if (value is double) {
sharedPrefs.setDouble(key, value);
} else if (value is List<String>) {
sharedPrefs.setStringList(key, value);
}
}
}

Pass parameter to initState

Look at this code - widget to fetch data and display on list:
class _MyEventsFragmentState extends State <MyEventsFragment>{
var events;
#override
initState(){
super.initState();
events = fetchEvents(true);
}
#override
Widget build(BuildContext context) {
return new Center(
child: FutureBuilder<EventsResponse>(
future: events,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
helpers.logout(context, Strings.msg_session_expired);
return CircularProgressIndicator();
}
return new Container(color: Colors.white,
child: new ListControl().build(snapshot));
}
return CircularProgressIndicator();
},
)
);
}
}
fetchEvent method has parameter to indicate which events I need to fetch. If set to true, - my events, if set to false - all events returned. Above code loads my events and fetchEvents is called inside initState override to avoid unnecesary data reloading.
To fetch all events I defined another class:
class EventsFragment extends StatefulWidget {
#override
_EventsFragmentState createState() => new _EventsFragmentState();
}
class _EventsFragmentState extends State <EventsFragment>{
var events;
#override
initState(){
super.initState();
events = fetchEvents(false);
}
#override
Widget build(BuildContext context) {
return new Center(
child: FutureBuilder<EventsResponse>(
future: events,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
helpers.logout(context, Strings.msg_session_expired);
return CircularProgressIndicator();
}
return new Container(color: Colors.white,
child: new ListControl().build(snapshot));
}
return CircularProgressIndicator();
},
)
);
}
}
But this is very dumb solution, because code is almost the same. So I tried to pass boolean value to indicate which events to load, something like that:
#override
initState(){
super.initState();
events = fetchEvents(isMyEvents);
}
isMyEvents should be got from EventsFragment constructor. However, it won't be accesible inside initState. Ho to pass it properly? I could access it inside build override, but not inside initState. How to pass it properly and make sure it will be refreshed every time widget instance is created?
[edit]
So this how I solved my problem (it seems to be fine):
class EventsFragment extends StatefulWidget {
const EventsFragment({Key key, this.isMyEvent}) : super(key: key);
final bool isMyEvent;
#override
_EventsFragmentState createState() => new _EventsFragmentState();
}
class _EventsFragmentState extends State <EventsFragment>{
var events;
#override
initState(){
super.initState();
events = fetchEvents(widget.isMyEvent);
}
#override
void didUpdateWidget(EventsFragment oldWidget) {
if(oldWidget.isMyEvent != widget.isMyEvent)
events = fetchEvents(widget.isMyEvent);
super.didUpdateWidget(oldWidget);
}
#override
Widget build(BuildContext context) {
return new Center(
child: FutureBuilder<EventsResponse>(
future: events,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
helpers.logout(context, Strings.msg_session_expired);
return CircularProgressIndicator();
}
return new Container(color: Colors.white,
child: new ListControl().build(snapshot));
}
return CircularProgressIndicator();
},
)
);
}
}
Pass such parameter to the StatefulWidget subclass, and use that field instead
class Foo extends StatefulWidget {
const Foo({Key key, this.isMyEvent}) : super(key: key);
final bool isMyEvent;
#override
_FooState createState() => _FooState();
}
class _FooState extends State<Foo> {
#override
void initState() {
super.initState();
print(widget.isMyEvent);
}
#override
Widget build(BuildContext context) {
return Container(
);
}
}

How to use UID to acces database in Flutter?

i am using the following function to retrive the UID:
FirebaseAuth auth = FirebaseAuth.instance;
getUID() async {
final FirebaseUser user = await auth.currentUser();
final uid = user.uid;
return uid;
}
After that, i would like to use the UID to acces the right database-doc:
return new StreamBuilder(
stream: Firestore.instance.collection('users').document(getUID()).snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return new Text("Loading");
}
var userDocument = snapshot.data;
But i get the error
Future<dynamic> is not a subtype of type "string"
What is the right way to do it?
FULL CODE
class Settings extends StatelessWidget{
FirebaseAuth auth = FirebaseAuth.instance;
getUID() async {
final FirebaseUser user = await auth.currentUser();
final uid = user.uid;
return uid;
}
static GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Indstillinger"),
),
body: _buildSetting(context),
);
}
Widget _buildSetting(BuildContext context) {
return new StreamBuilder(
stream: Firestore.instance.collection('users').document(getUID()).snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return new Text("Loading");
}
var userDocument = snapshot.data;
You need to use stateful widget, like
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class Settings extends StatefulWidget {
#override
_SettingsState createState() {
return _SettingsState();
}
}
class _SettingsState extends State<Settings> {
FirebaseUser user;
String error;
void setUser(FirebaseUser user) {
setState(() {
this.user = user;
this.error = null;
});
}
void setError(e) {
setState(() {
this.user = null;
this.error = e.toString();
});
}
#override
void initState() {
super.initState();
FirebaseAuth.instance.currentUser().then(setUser).catchError(setError);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Indstillinger"),
),
body: user != null ? _buildSetting(context) : Text("Error: $error"),
);
}
Widget _buildSetting(BuildContext context) {
return new StreamBuilder(
stream: Firestore.instance.collection('users').document(user.uid).snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return new Text("Loading");
}
var userDocument = snapshot.data;
// User userDocument here ..........
return new Text(userDocument.toString());
});
}
}
You're passing the function without actually awaiting for it to return. You could store the returned value in a variable before calling StreamBuilder.
final documentId = await getUID();
return new StreamBuilder(
stream: Firestore.instance.collection('users').document(documentId).snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return new Text("Loading");
}
var userDocument = snapshot.data;

Usage of FutureBuilder with setState

How to use the FutureBuilder with setState properly? For example, when i create a stateful widget its starting to load data (FutureBuilder) and then i should update the list with new data, so i use setState, but its starting to loop for infinity (because i rebuild the widget again), any solutions?
class FeedListState extends State<FeedList> {
Future<Null> updateList() async {
await widget.feeds.update();
setState(() {
widget.items = widget.feeds.getList();
});
//widget.items = widget.feeds.getList();
}
#override
Widget build(BuildContext context) {
return new FutureBuilder<Null>(
future: updateList(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return new Center(
child: new CircularProgressIndicator(),
);
default:
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
else
return new Scrollbar(
child: new RefreshIndicator(
child: ListView.builder(
physics:
const AlwaysScrollableScrollPhysics(), //Even if zero elements to update scroll
itemCount: widget.items.length,
itemBuilder: (context, index) {
return FeedListItem(widget.items[index]);
},
),
onRefresh: updateList,
),
);
}
},
);
}
}
Indeed, it will loop into infinity because whenever build is called, updateList is also called and returns a brand new future.
You have to keep your build pure. It should just read and combine variables and properties, but never cause any side effects!
Another note: All fields of your StatefulWidget subclass must be final (widget.items = ... is bad). The state that changes must be stored in the State object.
In this case you can store the result (the data for the list) in the future itself, there is no need for a separate field. It's even dangerous to call setState from a future, because the future might complete after the disposal of the state, and it will throw an error.
Here is some update code that takes into account all of these things:
class FeedListState extends State<FeedList> {
// no idea how you named your data class...
Future<List<ItemData>> _listFuture;
#override
void initState() {
super.initState();
// initial load
_listFuture = updateAndGetList();
}
void refreshList() {
// reload
setState(() {
_listFuture = updateAndGetList();
});
}
Future<List<ItemData>> updateAndGetList() async {
await widget.feeds.update();
// return the list here
return widget.feeds.getList();
}
#override
Widget build(BuildContext context) {
return new FutureBuilder<List<ItemData>>(
future: _listFuture,
builder: (BuildContext context, AsyncSnapshot<List<ItemData>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return new Center(
child: new CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return new Text('Error: ${snapshot.error}');
} else {
final items = snapshot.data ?? <ItemData>[]; // handle the case that data is null
return new Scrollbar(
child: new RefreshIndicator(
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(), //Even if zero elements to update scroll
itemCount: items.length,
itemBuilder: (context, index) {
return FeedListItem(items[index]);
},
),
onRefresh: refreshList,
),
);
}
},
);
}
}
Use can SchedulerBinding for using setState() inside Future Builders or Stream Builder,
SchedulerBinding.instance
.addPostFrameCallback((_) => setState(() {
isServiceError = false;
isDataFetched = true;
}));
Screenshot (Null Safe):
Code:
You don't need setState while using FutureBuilder.
class MyPage extends StatefulWidget {
#override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
// Declare a variable.
late final Future<int> _future;
#override
void initState() {
super.initState();
_future = _calculate(); // Assign your Future to it.
}
// This is your actual Future.
Future<int> _calculate() => Future.delayed(Duration(seconds: 3), () => 42);
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<int>(
future: _future, // Use your variable here (not the actual Future)
builder: (_, snapshot) {
if (snapshot.hasData) return Text('Value = ${snapshot.data!}');
return Text('Loading...');
},
),
);
}
}

Future.wait function

class Tickets extends StatefulWidget {
int groupid;
int event_id;
List returnTickets = [];
Map<String, dynamic> hey;
Tickets([this.groupid,this.event_id,this.hey,this.returnTickets]);
#override
_TicketsState createState() => new _TicketsState();
}
class _TicketsState extends State<Tickets> {
List returnTickets = [];
List ticketType = [];
Map<String, dynamic> hey;
int i = 0;
#override
void initState() {
super.initState();
// _getTicketType();
}
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
widget.groupid;
widget.event_id;
var futureBuilder = new FutureBuilder(
future: Future.wait([hey, returnTickets]).then(
(response) => new _TicketsState(hey: response[0], returnTickets: response[1]),
),
builder: (BuildContext context, AsyncSnapshot snapshot) {
print(snapshot.connectionState);
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return new Text('...');
default:
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
else
return createListTickets(context, snapshot);
}
},
);
Hi everyone, I am trying to implement the Future.wait function so then I can hopefully use more than a method within a building widget, but as I am trying to do so, I get this long error, any help please??
Thank you in advance, much appreciated.
It's not recommended to use what is called "raw" types in Dart2 (List). In this particular case, Future.wait expects a List<Future>, but you've (implicitly) created a List<Object> here:
List returnTickets = [];
This worked in Dart1, because List<dynamic> could pretend to be List<T>.
Try writing:
List<Future> returnTickets = [];
Or, better yet:
final returnTickets = <Future>[];

Resources