Flutter - FutureBuilder not rebuilding when user logs into Firebase? - firebase

I am trying to fetch my currentUser using FutureBuilder in my main.dart file:
//my auth.dart file
class Auth {
final FirebaseAuth _auth = FirebaseAuth.instance;
Future getUser() {
return _auth.currentUser();
}
}
class MyApp extends StatelessWidget {
Auth auth = Auth();
#override
Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder(
future: auth.getUser(),
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.error != null) {
print("$snapshot.error.toString()");
return Container();
}
if (snapshot.hasData) {
return BottomBar(
firebaseUser: snapshot.data, visibleLogin: false);
} else if (snapshot.data == null) {
return BottomBar(
firebaseUser: null,
visibleLogin: true,
);
}
}
return CircularProgressIndicator();
}),
);
}
}
//Update, tried using StreamBuilder
StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data.uid!=null) {
return BottomBar(
firebaseUser: snapshot.data, visibleLogin: false);
} else {
return BottomBar(
firebaseUser: null,
visibleLogin: true,
);
}
}
return CircularProgressIndicator();
}),
Whenever my user logs in, I pass the user in my bottom navigation bar, which then passes the data to all my pages.
My login is in the same page as my home page, so whenever my user logs in, I want to hide the login part in my home page.
Widget login(bool visibleLogin) {
return Visibility(
visible: visibleLogin,
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 78.0),
child: Container(
height: 90,
color: Colors.transparent,
margin: EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0),
child: Container(
child: Container(
margin: EdgeInsets.only(top: 20.0),
child: Column(
children: [
Padding(
padding: EdgeInsets.only(top: 5.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GestureDetector(
onTap: () => auth.signInWithGoogle(),
child: Container(
padding: EdgeInsets.all(10.0),
child: Icon(
FontAwesome.google,
color: kOrange,
),
),
),
],
),
),
],
),
),
),
),
),
),
);
}
When my user logs in, the UI of my app doesn't get updated automatically by my FutureBuilder, hence I call the onAuthStateChanged listener in my initState() of my homepage:
void isSignedIn() {
_auth.onAuthStateChanged.listen((user) {
if (user != null) {
setState(() {});
} else {
print('no user');
}
});
}
}
After checking, this method is getting triggered, but my UI only gets updated when I shut and relaunch my app. How do I update my UI as soon as my user logs in?

I think this is not possible with FutureBuilder, you have to use StreamBuilder for this Functionality.
As once Future is complete it will not look for any change, where as if you use stream then whenever new data arrives it will change data.
Update:
You even don't need to change any variable or call any method, now just check snap and show screen accordingly. make to check connection status is done because until that time it will return null even user is login.
StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (_, snap) {
if(snap.connectionState == ConnectionState.active){
if(snap.data == null){
// return with out login
}else{
// return login
}
}else{
return CircularProgressIndicator();
}
},
),
Update:
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (_, snap) {
print(snap.connectionState);
if (snap.connectionState == ConnectionState.active) {
print(snap.data.toString());
if (snap.data == null) {
return Text("not login");
} else {
return Text("login");
}
} else {
return CircularProgressIndicator();
}
},
),
RaisedButton(
child: Text("hello"),
onPressed: () {
var a = FirebaseAuth.instance.signInAnonymously();
print(a);
},
),
RaisedButton(
child: Text("Log out"),
onPressed: () {
var a = FirebaseAuth.instance.signOut();
print(a);
},
)
],
),

Related

How to get matched user list show in search result [Flutter]

I'm using package search_page
I want to show user name when searching their name like this example
Here is my Firestore, I have user named Test User One and Test User Two
Here is my code for FloatingActionButton( )
FloatingActionButton(
onPressed: () => showSearch(
context: context,
delegate: ChatSearch(),
),
child: Icon(Icons.chat),
),
Here is my code for ChatSearch( )
class ChatSearch extends SearchDelegate {
FirebaseFirestore _fires = FirebaseFirestore.instance;
Future<QuerySnapshot> getUserInfo() async {
return await _fires.collection("Students").where("displayName", isEqualTo: query).get();
}
...
#override
Widget buildResults(BuildContext context) {
return Column(
children: [
FutureBuilder(
future: getUserInfo(),
builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('No user found.');
}
else if (snapshot.connectionState == ConnectionState.waiting) {
return Text('Loading ...');
}
return ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data.docs.length,
itemBuilder: (BuildContext context, int index) {
var result = snapshot.data.docs[index];
return ListTile(
title: Text(result["displayName"]),
subtitle: Text(result["society"]),
leading: CircleAvatar(backgroundImage: NetworkImage(result["photoUrl"]),),
);
},
);
}),
],
);
}
}
Here is my screenshot
How can I get the query (all user name matched with search) instead of isEqualTo?
For example, when I search Test User, it will appear Test User One and Test User Two
Really appreciate if anyone can help me or provide me some direction. Thanks.
It's easy no need to use the plugin here is the code
//it's the code to search for user for name
static Future<QuerySnapshot> getUserInfo(String name) {
Future<QuerySnapshot> users =
usersRef.where('name', isGreaterThanOrEqualTo: name).getDocuments();
return users;
}
and this are some values
TextEditingController _searchController = TextEditingController();
Future<QuerySnapshot> _users;
String _searchText = '';
and it's the search ui code
#override
Widget build(BuildContext context) {
String _currentUserId = Provider.of<UserData>(context).currentUserId;
void _clearSearch() {
WidgetsBinding.instance
.addPostFrameCallback((_) => _searchController.clear());
setState(() {
_users = null;
_searchText = '';
});
}
return Scaffold(
appBar: AppBar(
title: TextField(
controller: _searchController,
decoration: InputDecoration(
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 15.0),
border: InputBorder.none,
hintText: 'Search for a user...',
prefixIcon: Icon(
Icons.search,
color: Theme.of(context).accentColor.withOpacity(0.6),
size: 30.0,
),
suffixIcon: _searchText.trim().isEmpty
? null
: IconButton(
color: Theme.of(context).accentColor.withOpacity(0.6),
icon: Icon(Icons.clear),
onPressed: _clearSearch,
),
// filled: true,
),
onChanged: (value) {
setState(() {
_searchText = value;
});
},
onSubmitted: (input) {
if (input.trim().isNotEmpty) {
setState(() {
_users = DatabaseService.searchUsers(input);
});
}
},
),
),
body: _users == null
? Center(child: Text('Search for users'))
: FutureBuilder(
future: _users,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.data.documents.length == 0) {
return Center(
child: Text('No Users found! Please try again.'),
);
}
return ListView.builder(
itemCount: snapshot.data.documents.length,
itemBuilder: (BuildContext context, int index) {
//User user = User.fromDoc(snapshot.data.documents[index]);
// Prevent current user to send messages to himself
return null;
// (widget.searchFrom != SearchFrom.homeScreen &&
// user.id == _currentUserId)
// ? SizedBox.shrink()
// : _buildUserTile(user);
});
},
),
);
}
and this is the user tile
ListTile _buildUserTile(User user) {
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.grey,
radius: 20.0,
backgroundImage: user.profileImageUrl.isEmpty
? AssetImage(placeHolderImageRef)
: CachedNetworkImageProvider(user.profileImageUrl),
),
title: Row(
children: [Text(user.name)],
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ProfileScreen()
),
)
);
}
Hope it works, if something is missing then tell me.

Error message - Alert Message Not Appearing

I placing on the interface a message to provide information to users on errors in logging or signing up.
The error does not provide a bug and the application continues to run.
However, the message of error or the status is not passed on to the widget _showAlert which does not appear.
_signup() async {
AuthNotifier authNotifier = Provider.of<AuthNotifier>(context, listen: false);
{
setState(() {
});
final status =
await signup(_user, authNotifier);
if (status == AuthResultStatus.successful) {
// Navigate to success screen
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => SecondPage()),
(r) => false);
} else {
final errorMsg = AuthExceptionHandler.generateExceptionMessage(status);
_showAlert(errorMsg);
}
}
}
_showAlert(errorMsg) {
if (errorMsg != null) {
return Container(
color: Colors.amberAccent,
width: double.infinity,
padding: EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Icon(Icons.error_outline),
),
Expanded(
child: AutoSizeText(
errorMsg,
maxLines: 3,
),
),
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: IconButton(
icon: Icon(Icons.close),
onPressed: () {
setState(() {
errorMsg = null;
});
},
),
)
],
),
);
}
return SizedBox(
height: 0,
);
}
You are returning a Widget not showing a Dialog. To show a Dialog try this
_showAlert(errorMsg, BuildContext context) {
if (errorMsg != null) {
showDialog(
context: context,
builder: (BuildContext context) =>
Dialog(
child: Container(
// ...
),
),
);
}
}

Trying to implement loading spinner while loading data from Firestore with Flutter

I'm working on an app that display spinner when backend loading data from Firestore but it's not worked as intended and I'm having a struggle to find the flaw.
My Orders Page code
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:table_service/providers/order.dart';
import '../providers/session.dart';
class OrdersPage extends StatefulWidget {
bool isLoading = true;
#override
_OrdersPageState createState() => _OrdersPageState();
}
class _OrdersPageState extends State<OrdersPage> {
List<Order> _orders = [];
#override
Widget build(BuildContext context) {
final session = Provider.of<Session>(context, listen: false);
return Scaffold(
floatingActionButton: session.privilege == 'Administrator' ||
session.privilege == 'Waiter' ||
session.privilege == 'Customer'
? FloatingActionButton(
heroTag: 'OrdersPageFAB',
onPressed: () {},
child: Icon(Icons.add, color: Colors.white),
)
: null,
body: FutureBuilder(
future: session.fetchOrdersData(),
builder: (ctx, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else {
print(snapshot.data);
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 2 / 2,
),
itemCount: _orders.length,
itemBuilder: (_, i) {
return Padding(
padding: const EdgeInsets.all(5.0),
child: Card(
child: GridTile(
child: Icon(
Icons.library_books,
size: 100.0,
color: Colors.grey,
),
footer: GridTileBar(
backgroundColor: Colors.black54,
title: Text('Order by: ${_orders[i].name}'),
),
),
),
);
},
);
}
},
),
);
}
}
The fetchOrdersData() handler
final Auth auth = Auth();
final Firestore database = Firestore.instance;
String user_name;
String privilege;
List<Food> _foods = [];
List<Order> _orders = [];
List<TransactionModel.Transaction> _transactions = [];
...
...
Future fetchOrdersData() async {
_orders.clear();
return await database.collection('orders').getDocuments().then((documents) {
documents.documents.forEach((order) {
database
.collection('users')
.document(order.data['uid'])
.get()
.then((user) {
_orders.add(Order(
id: order.documentID,
tableNumber: order.data['tablenumber'],
orderDate: (order.data['orderdate'] as Timestamp).toDate(),
status: order.data['status'],
note: order.data['note'],
uid: order.data['uid'],
name: user.data['user_name'],
));
});
});
return _orders;
});
notifyListeners();
}
get getOrders {
return [..._orders];
}
I have tried many methods including StreamBuilder, setState() and recently FutureBuilder.
Did i just missing an important code?
Or did i use the wrong method?
The problem was Orders Page showing 0 data even though List on _fetchOrdersData() have 1 element.
for full source code
here on github
The other answers look reasonable. They are just missing data validation checks, which I find is required in all my apps. Because if I have a good connection, and hasData is true and hasError is false, there might be no documents at all though. This should be checked. Here is a snippet from my projects.
Checking connection state is the same as just checking snapshot.hasError.
Widget _getMyFriends() {
return StreamBuilder<QuerySnapshot>(
stream: Database.getFriendsByUserId(widget.loggedInUserId),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError)
return Center(child: Text("Error"));
else if (!snapshot.hasData)
return Center(child: Text("Loading..."));
else if (snapshot.data.documents.isEmpty) //also check if empty! show loader?
return Center(child: Text("No friends added yet."));
else
return ListView(
children: snapshot.data.documents.map((DocumentSnapshot document) {
return SimpleUserPanel(userId: document['friendid']);
}).toList(),
);
}
);
}
You should do the following:
else {
if(snapshot.hasData){
print(snapshot.data);
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 2 / 2,
),
itemCount: _orders.length,
itemBuilder: (_, i) {
return Padding(
padding: const EdgeInsets.all(5.0),
child: Card(
child: GridTile(
child: Icon(
Icons.library_books,
size: 100.0,
color: Colors.grey,
),
footer: GridTileBar(
backgroundColor: Colors.black54,
title: Text('Order by: ${_orders[i].name}'),
),
),
),
);
},
// By default, show a loading spinner.
return CircularProgressIndicator();
},
So first check if snapshot has data using the property hasData and since this is asynchronous it will first execute the return CircularProgressIndicator(); and then execute the if block.
Loking at your code, you have to check for ConnectionState.active also with your snapshot.connectionState == ConnectionState.waiting.
Actually you have more power and controll when using FutureBuilder or StreamBuilder. Below is a sample code snippet:
switch (snapshot.connectionState) {
case ConnectionState.none:
return Center(child: Text("Check Connection"));
case ConnectionState.active:
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator(backgroundColor: Theme.of(context).primaryColorLight,));
case ConnectionState.done:
if (snapshot.hasError) {
return Center(child: Text("Error occured!"));
} else if (snapshot.hasData) {
return YourWidgetWithData();
} else {
debugPrint("What went wrong");
return SizedBox();
}
break;
default:
return SizedBox();
}

Flutter FutureBuilder show Progress indicator

I have a button which onpressed inserts something in DB and redirects user to another page. I am trying to implement FutureBuilder which should show CircularProgressIndicator until everything is done.
This is my function:
Future<bool> insertPhoneNumber(String phoneNumber) async {
String token = await getToken();
if (token.isNotEmpty) {
var body = jsonEncode({'token': token, 'userID': user.getUserID(), 'phoneNumber': phoneNumber});
print(body.toString());
var res = await http.post((baseUrl + "/insertPhoneNumber/" + user.getUserID()),
body: body,
headers: {
"Accept": "application/json",
"content-type": "application/json"
});
if (res.statusCode == 200) {
print("Insert Phone Number is OK");
notifyListeners();
return true;
} else {
print("Insert Phone Number not OK");
notifyListeners();
return false;
}
} else {
print("Insert Phone Number failed due to unexisting token");
}
}
and this is a button which triggers DB interaction:
RaisedButton(
color: Color.fromRGBO(105, 79, 150, 1),
onPressed: () {
var user = Provider.of<UserRepository>(context);
String completePhoneNumber = _selectedDialogCountry.phoneCode + phoneNumberController.text;
FutureBuilder(
future: user.insertPhoneNumber(completePhoneNumber),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return CircularProgressIndicator(
backgroundColor: Colors.blue);
} else {
return Dashboard();
}
},
);
},
textColor: Colors.white,
child: Text("SAVE"),
)
It updates DB but nothing else happens. There is no progress indicator nor redirection to Dashboard.
EDIT
This is my full build method:
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
padding: EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
AutoSizeText(
'What is your phone number?',
style: TextStyle(fontSize: 30),
maxLines: 1,
),
Row(
children: <Widget>[
SizedBox(
width: 9.0,
child: Icon(Icons.arrow_downward),
),
SizedBox(width: 8.0),
SizedBox(
width: 120.0,
height: 65.0,
child: Card(
child: ListTile(
onTap: _openCountryPickerDialog,
title: _buildDialogItem(_selectedDialogCountry),
),
)),
Expanded(
child: TextField(
autofocus: true,
keyboardType: TextInputType.number,
decoration:
InputDecoration(border: OutlineInputBorder())),
),
],
),
SizedBox(
width: 100,
child: RaisedButton(
color: Color.fromRGBO(105, 79, 150, 1),
onPressed: () {
print("Test");
},
textColor: Colors.white,
child: Text("SAVE"),
),
)
],
)),
);
}
It is not working because the FutureBuilder isn't attached to the Widget tree.
The once the future is not in done state, it is just creating an instance Dashboard.You shouldn't be using FutureBuilder here instead you can set the child widget based on some variable and when future complete, you call setState on the that variable to update the state which will in-turn rebuild the widget with the new state value
Something like this
var isLoading = false;
void insertNumber(){
var user = Provider.of<UserRepository>(context);
String completePhoneNumber = _selectedDialogCountry.phoneCode + phoneNumberController.text;
setState(() => isLoading=true);
user.insertPhoneNumber(completePhoneNumber).then((result){
setState(() => isLoading=false);
})
}
Widget build(BuildContext context){
return RaisedButton(
color: Color.fromRGBO(105, 79, 150, 1),
onPressed: () {
var user = Provider.of<UserRepository>(context);
String completePhoneNumber = _selectedDialogCountry.phoneCode + phoneNumberController.text;
FutureBuilder(
future: user.insertPhoneNumber(completePhoneNumber),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return CircularProgressIndicator(
backgroundColor: Colors.blue);
} else {
return Dashboard();
}
},
);
},
textColor: Colors.white,
child: isLoading? CircularProgressIndicator(): Text("Save"),
);
}
For your use case, you will need more that two state to achieve the UI you are looking for .Example DEFAULT,LOADING,ERROR,SUCCESS

Using TextField inside a Streambuilder

How do we add a TextField inside a StreamBuilder?
I have a TextField / TextFormField as one of the widgets inside the builder function of either a StreamBuilder or FutureBuilder, whenever we try to interact with the textfield it just refreshes the entire builder widget and calls the stream/future again.
body: StreamBuilder(
stream: getClientProfile().snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
print(snapshot.data.data);
Client tempClient = Client.from(snapshot.data);
print('details = ${tempClient.representative.email} ${tempClient
.address.location} ${tempClient.businessDescription}');
return Container(
child: Column(
children: <Widget>[
TextFormField(
)
],
),
);
} else if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else {
return Center(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(Icons.error),
),
Text('Error loading data')
],
),
);
}
}),
and firestore function
DocumentReference getClientProfile() {
return _firestore.collection(SELLERS_COLLECTION).document(_uid);
}
What I want to achieve, is to have a form with pre-filled data from firestore document, basically an edit form. Is there any other way I could achieve the same or am I doing something wrong structurally ?
EDIT:
code after suggested edits.
import 'package:flutter/material.dart';
import 'Utils/globalStore.dart';
import 'models/client_model.dart';
import 'dart:async';
class EditProfileInformation extends StatefulWidget {
#override
EditProfileInformationState createState() {
return new EditProfileInformationState();
}
}
class EditProfileInformationState extends State<EditProfileInformation> {
Stream dbCall;
final myController = TextEditingController();
#override
void initState() {
// TODO: implement initState
super.initState();
dbCall = getClientProfile().snapshots();
myController.addListener(_printLatestValue);
}
_printLatestValue() {
print("Second text field: ${myController.text}");
}
#override
void dispose() {
myController.removeListener(_printLatestValue);
myController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
// key: _scaffoldKey,
appBar: AppBar(
title: Text(
'Edit profile',
style: TextStyle(),
),
),
body: StreamBuilder(
stream: dbCall,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
print(snapshot.data.data);
Client tempClient = Client.from(snapshot.data);
print('details = ${tempClient.representative.email} ${tempClient
.address.location} ${tempClient.businessDescription}');
return Container(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: myController,
),
)
],
),
);
} else if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else {
return Center(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(Icons.error),
),
Text('Error loading data')
],
),
);
}
}),
floatingActionButton: FloatingActionButton(
onPressed: () {
},
child: Icon(Icons.done),
),
);
}
}
In order to use a StreamBuilder correctly you must ensure that the stream you are using is cached on a State object. While StreamBuilder can correctly handle getting new events from a stream, receiving an entirely new Stream will force it to completely rebuild. In your case, getClientProfile().snapshots() will create an entirely new Stream when it is called, destroying all of the state of your text fields.
class Example extends StatefulWidget {
#override
State createState() => new ExampleState();
}
class ExampleState extends State<Example> {
Stream<SomeType> _stream;
#override
void initState() {
// Only create the stream once
_stream = _firestore.collection(collection).document(id);
super.initState();
}
#override
Widget build(BuildContext context) {
return new StreamBuilder(
stream: _stream,
builder: (context, snapshot) {
...
},
);
}
}
EDIT: it sounds like there are other problems which I cannot diagnose from the code snippet you provided.

Resources