How to perform async calls inside streambuilder? - flutter

I have an flutter app, which uses FirebaseAuth for authentication and Firestore for storing data. To store user profile (name, photo etc), I've created a separate collection in my firestore database.
So, once a user registers he is redirected to a screen where he can add his profile data. This data is again stored as a document.
I want to implement the same checks when the app is starting:
So, I display a splash screen and in the backend it checks,
Is the user logged in? If yes, proceed, else redirect him to the terms and conditions page.
Does the collection have user profile? If yes, proceed, else redirect him to a page where he can add his photo etc.
I was able to accomplish point 1, but I am not able to do the 2nd check.
Here's the code:
class SplashScreen extends StatelessWidget {
static final String id = 'splash_screen';
final Firestore _firestore = Firestore.instance;
#override
Widget build(BuildContext context) {
return StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
FirebaseUser user = snapshot.data;
if (user == null) {
return TermsAndConditions();
}
// Check if user profile has been created
return ChatsScreen();
} else {
return SplashScreenContent();
}
},
);
}
}
The commented line is where the code for checking user profile should go.
I tried the following:
_firestore.collection('users').where('id', isEqualTo: user.uid).snapshots().first.then( (value) {
if(value.documents.isEmpty) {
return ProfileScreen();
}
});
As I understand I cannot return the value to outer function from the callback. How can I achieve this

What you want to do is to show splash screen every time.
Then, inside splash screen, you can create the check.
call this function inside initState
void bootstrap() async {
var user = await FirebaseAuth.instance.currentUser();
if (user == null) {
Navigator.push(context,
MaterialPageRoute(builder: (context) => TermsAndConditions()));
return;
}
DocumentSnapshot userDoc =
await Firestore.instance.collection("users").document(user.uid).get();
if (!userDoc.exists) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => ProfilePage()));
return;
}
Navigator.push(
context, MaterialPageRoute(builder: (context) => ChatsScreen()));
}
You can also add initial 2-3 seconds delay, else the splash screen can be too abruptly changed.
You can find many splash screen libraries in pub.dev that allow you to do this bootstraping.

Related

Displaying multiple screens based on ternary condition in dart

I have a scenario where I am showing a spinner when a page loads and while it's loading, it fetches some data in DB and sets a bool value to either true or false based on data availability.
I then want to share either screen A or B based on the boolean result.
I have done the following in my code but the app keeps showing the spinner. Any ideas what I might be doing incorrectly?
return _isLoading?
Center(child:Loading(),):
_isPersonalInfoSubmitted?ScreenA():ScreenB();
Second Attempt (Using Future Builder)
I want to show categories if the loggedin user is an admin. Else, for the rest of the users, I want to fetch address of the user from the DB. If the address is null, show Personal Details Screen else show Categories.
return FutureBuilder (
future: userId=='ADMIN_ID'?
Provider.of<Categories>(context,listen:false).fetchAndReturnCategories():
Provider.of<Addresses>(context,listen: false).fetchAndReturnAddress(userId)!=null?
Provider.of<Categories>(context,listen:false).fetchAndReturnCategories():null,
builder: (context, snap) {
inspect(snap);
if (snap.hasData) {
var categoriesData = Provider.of<Categories>(context);
return snap.hasData?
Scaffold(...) : PersonalDetails();
What happens here is that the method fetchAndReturnCategories gets executed even if the userID is not admin id. Do I have the correct setup?
prefer to use FutureBuilder
FutureBuilder<SomeClass>(
future: fetchdatFuture,
builder: (ctx, snap) {
if (snap.hasData) {
return snap.data?ScreenA():ScreenB();
} else if (snap.connectionState == ConnectionState.waiting)
{
return Center(child:Loading());
}
return Text("Error");
},
)
I think you have missed the setState to rebuild your widget
var bool _isLoading = true;
return _isLoading?
Center(child:Loading(),):
_isPersonalInfoSubmitted?ScreenA():ScreenB();
void _apiCall() {
// After Success of API Call
setState((){
_isLoading = false;
})
}

How to use shared preferences to have a one-time login in flutter?

I searched google/stackoverflow alot and tried but nothing worked. This is the flow i want:
User launches the app. Splash screen appears.
Login screen appears after splash screen. User logs in.
User kills(closes) the app.
When user relaunches the app, it should show the splash screen followed by the homepage, as user has already logged in once before.
User will only see login page if he/she logs out.
So far 1 and 2 works. But when user kills/closes the app and relaunches it again, instead of being directed to the home page, they are directed to the login page again.
The code for the splash screen:
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
startTimer();}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
height: 150,
width: 150,
child: new SvgPicture.asset(
'assets/logo.png'
),
),
),
);
}
void startTimer() {
Timer(Duration(seconds: 3), () {
navigateUser(); //It will redirect after 3 seconds
});
}
void navigateUser() async{
SharedPreferences prefs = await SharedPreferences.getInstance();
var status = prefs.getBool('isLoggedIn');
print(status);
if (status == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (BuildContext context) => HomePage());
} else {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (BuildContext context) => LoginScreen()));
}
}}
The code for the log out button:
void logoutUser() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs?.clear();
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (BuildContext context) => SplashScreen()),
ModalRoute.withName("/login"),
);
}
Sorry for the lengthy post, and really appreciate the help if someone could point out where i've gone wrong. Or if there's any other way to achieve a one-time login in flutter. Thanks!
I know my answer is late now. But, If you are using FirebaseAuth, this will automatically cache your login, logout log. So you will not need to store it to pref. Just nee to make additional step when you lauch screen to check if user's last status was login or log out by the following. And this information can be used to rediret to the desired screen.
Code:
Future<bool> checkIfAlreadySignedIn () async {
late bool _isAlreadySignedIn;
await FirebaseAuth.instance
.authStateChanges()
.listen((event) async {
if (event == null) {
print('Currentyl signed out');
_isAlreadySignedIn = false;
} else {
_isAlreadySignedIn = true;
}
});
return _isAlreadySignedIn;
}
Where do you set 'isLoggedIn' pref to true?

I have a question about navigating to the next page conditionally in initstate

I want to implement Auto Login with Shared preferences.
What I want to implement is that as soon as 'LoginPage' starts, it goes to the next page without rendering LoginPage according to the Flag value stored in Shared preferences.
However, there is a problem in not becoming Navigate even though implementing these functions and calling them from initstate. What is the problem?
//Login Page
void autoLogIn() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
final String userId = prefs.getString('username');
print("ddddddddddddddd");
SocketProvider provider = Provider.of<SocketProvider>(context);
Future.delayed(Duration(milliseconds: 100)).then((_) {**//I tried giving Delay but it still didn't work.**
Navigator.of(context).pushNamedAndRemoveUntil("/MainPage", (route) => false);
});
}
#override
void initState() {
// TODO: implement initState
loginBloc = BlocProvider.of<LoginBloc>(context);
if(!kReleaseMode){
_idController.text = "TESTTEST";
_passwordController.text = "1234123";
}
initBadgeList();
autoLogIn();**//This is the function in question.**
super.initState();
print("1111111111111111");
}
I don't think you should show LoginPage widget if user is already logged in and then navigate to main page.
I suggest you to use FutureBuilder and show either splash screen or loader while performing await SharedPreferences.getInstance(). In this case your App widget should look like this:
class App extends MaterialApp {
App()
: super(
title: 'MyApp',
...
home: FutureBuilder(
future: SharedPreferences.getInstance(),
builder: (context, snapshot) {
if (snapshot.data != null) {
final SharedPreferences prefs = snapshot.data;
final userId = prefs.getString('username');
...
return userId == null ?? LoginPage() : MainPage();
} else {
return SplashScreenOrLoader();
}
}));
}
But if you still want to show LoginPage first, just replace SplashScreenOrLoader() with LoginPage() in code above.

How should I manage login page in flutter app

I am making an app which starts with a splash screen.
What I want is if the user signing for the first time it should redirect to overboarding screen.For that I have used shared preferences and stored a value to check if the user is new or not.And from overbearing it should go to login page after login it will check the user data exists or not. And in normal case after splash it should redirect to home screen.My code is working. But the problem is I don't know how to arrange it.
Main.dart -> (check logged in or not ) -> (if already logged in)Home.dart -> (else check first time login or not)-> (if first time login then)Onboarding Screen From there Login screen ->else Login Screen
If you need any more information just ask me
This is the code from main.dart
return SplashScreen.navigate(
name: 'assets/splash.flr',
next: (context) {
return AuthService().handleAuth();
},
startAnimation: 'Untitled',
until: () => Future.delayed(Duration(seconds: 4)),
backgroundColor: Colors.white,
);
This is AuthService().handleAuth() code
handleAuth() {
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (context, snapshot) {
if (snapshot.hasData) {
Navigator.maybePop(context);
SharedPrefFunction().saveLoginPreference();
return CheckUser();
}
else{
return LoginScreen();}
});
}
This is the onboarding code
onTap: () {
Navigator.of(context)
.pushReplacementNamed(LoginScreen.loginRoute);
},
I want to go to AuthService().handleAuth() from onboarding.
How can I reach there from Onboarding screen or suggest me something better.
In your main.dart
bool loggedIn = false;
#override
void initState() {
super.initState();
isUserLoggedIn();
}
void isUserLoggedIn() async {
_loggedIn = await SharedPrefFunction().getLoginPreference()
setState(() => loggedIn = _loggedIn);
}
Widget build(BuildContext context) {
loggedIn ? LoginScreen() : SplashScreen()
}

How to inform FutureBuilder that database was updated?

I have a group profile page, where a user can change the description of a group. He clicks on the description, gets on a new screen and saves it to Firestore. He then get's back via Navigator.pop(context) to the group profile page which lists all elements via FutureBuilder.
First, I had the database request for my FutureBuilder inside the main build method (directly inside future builder 'future: request') which was working but I learnt it is wrong. But now I have to wait for a rebuild to see changes. How do I tell FutureBuilder that there is a data update?
I am loading Firestore data as follows within the group profile page:
Future<DocumentSnapshot> _future;
#override
void initState() {
super.initState();
_getFiretoreData();
}
Future<void> _getFiretoreData() async{
setState(() {
this._future = Firestore.instance
.collection('users')
.document(globals.userId.toString())
.get();});
}
The FutureBuilder is inside the main build method and gets the 'already loaded' future like this:
FutureBuilder(future: _future, ...)
Now I would like to tell him: a change happened to _future, please rebuild ;-).
Ok, I managed it like this (which took me only a few lines of code). Leave the code as it is and get a true callback from the navigator to know that there was a change on the second page:
// check if second page callback is true
bool _changed = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ProfileUpdate(userId: globals.userId.toString())),
);
// if it's true, reload future data
_changed ? _getFiretoreData() : Container();
On the second page give the save button a Navigator.pop(context, true).
i would advice you not to use future builder in this situation and use future.then() in an async function and after you get your data update the build without using future builder..!
Future getData() async {
//here you can call the function and handle the output(return value) as result
getFiretoreData().then((result) {
// print(result);
setState(() {
//handle your result here.
//update build here.
});
});
}
How about this?
#override
Widget build(BuildContext context) {
if (_future == null) {
// show loading indicator while waiting for data
return Center(child: CircularProgressIndicator());
} else {
return YourWidget();
}
}
You do not need to set any state. You just need to return your collection of users in your GetFirestoreData method.
Future<TypeYouReturning> _getFirestoreData() async{
return Firestore.instance
.collection('users')
.document(globals.userId.toString())
.get();
}
Inside your FutureBuilder widget you can set it up something like Theo recommended, I would do something like this
return FutureBuilder(
future: _getFirestoreData(),
builder: (context, AsyncSnapshot<TypeYouReturning> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else {
if (snapshot.data.length == 0)
return Text("No available data just yet");
return Container();//This should be the desire widget you want the user to see
}
},
);
Why don't you use Stream builder instead of Future builder?
StreamBuilder(stream: _future, ...)
You can change the variable name to _stream for clarity.