I am just learning Flutter and am trying to use a StreamBuilder to display a Login / Register page if the user is logged out, or a Profile page if the user is logged in. My code is below:
Auth Service:
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class AuthUser {
AuthUser({#required this.uid, #required this.email});
final String uid;
final String email;
}
abstract class AuthBase {
Future<AuthUser> currentUser();
Future<AuthUser> signIn({String email, String pw});
Future<AuthUser> registerUser({String email, String pw});
Stream<AuthUser> get onAuthStateChanged;
Future<void> signOut();
}
class Auth implements AuthBase {
final _firebaseAuth = FirebaseAuth.instance;
AuthUser _userFromFirebase(FirebaseUser user) {
if (user != null) {
return AuthUser(uid: user.uid, email: user.email);
} else {
return null;
}
}
#override
Stream<AuthUser> get onAuthStateChanged {
return _firebaseAuth.onAuthStateChanged.map(_userFromFirebase);
}
#override
Future<AuthUser> currentUser() async {
final user = await _firebaseAuth.currentUser();
return _userFromFirebase(user);
}
#override
Future<AuthUser> signIn({String email, String pw}) async {
final authResult = await _firebaseAuth.signInWithEmailAndPassword(email: email, password: pw);
return _userFromFirebase(authResult.user);
}
#override
Future<AuthUser> registerUser({String email, String pw}) async {
final authResult = await _firebaseAuth.createUserWithEmailAndPassword(email: email, password: pw);
return _userFromFirebase(authResult.user);
}
#override
Future<void> signOut() async {
await _firebaseAuth.signOut();
}
}
StreamBuilder:
class WelcomeScreen extends StatelessWidget {
WelcomeScreen({#required this.auth});
static const String id = '/';
final AuthBase auth;
#override
Widget build(BuildContext context) {
return StreamBuilder<AuthUser>(
stream: auth.onAuthStateChanged,
builder: (context, snapshot) {
if (snapshot.hasData) {
AuthUser user = snapshot.data;
if (user == null) {
return displayLoginOrRegPage(context);
} else {
return ProjectScreen(
user: user,
auth: auth,
);
}
} else {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
},
);
}
It was my understanding the stream would begin emitting 'null' once it was initialized, and would continue doing so until it fired off an Auth state change...
But the snapshot continually reports "No Data" and thus my code is stuck on the CircularProgressIndicator.
BTW, if I display the log-in screen in place of the progress indicator, the code works. So I'm clearly not understanding the whole stream initialization process.
Can somebody kindly explain to me where I have gone wrong here? Thanks a million in advance.
As you mentioned, when stream initialises it emits null, but when the user is not logged in, it still emits null, which stream considers as no data i.e null that's the reason for the error.
You can use Streambuilder's connection state to differentiate between no user null and null after initialisation.
I hope following code helps you.
if (snapshot.connectionState == ConnectionState.active) {
if (snapshot.data == null) {
return displayLoginOrRegPage(context);
} else {
AuthUser user = snapshot.data;
return ProjectScreen(
user: user,
auth: auth,
);
}
} else {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
Per #VirenVVarasadiya, it was definitely a case of checking the ConnectionState. Here is the final working code. Thank you!
#override
Widget build(BuildContext context) {
return StreamBuilder<AuthUser>(
stream: auth.onAuthStateChanged,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
if (snapshot.data != null) {
AuthUser user = snapshot.data;
return ProjectScreen(
user: user,
auth: auth,
);
} else {
return displayLoginOrRegPage(context);
}
} else {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
},
);
}
Try to change WelcomeScreen to state full Widget.
Related
I have a simple controller like this
class UserController with ChangeNotifier {
UserData user = UserData();
UserData get userdata => user;
void setUser(UserData user) {
user = user;
print(user.sId);
notifyListeners();
}
login(data) async {
var response = await ApiService().login(data);
final databody = json.decode(response);
if (databody['success']) {
UserData authUser = UserData.fromJson(databody['data']);
setUser(authUser);
notifyListeners();
return true;
} else {
return false;
}
}
}
I am trying to just print it like this on both widget and in initstate function but values are showing null. I can see in set function value is not null.
print('id ${context.watch<UserController>().user.sId.toString()}');
print(
'id2 ${Provider.of<UserController>(context, listen: false).user.sId.toString()}');
I already have added
ChangeNotifierProvider(create: (_) => UserController()),
],
in main.dart in MultiProvider
Also on Tap of login button I am doing this
showLoader(context);
UserController auth = Provider.of<UserController>(
context,
listen: false);
var data = {
"userEmail":
emailController.text.trim().toLowerCase(),
"userPassword": passwordController.text.trim(),
};
auth.login(data).then((v) {
if (v) {
hideLoader(context);
context.go('/homeroot');
} else {
hideLoader(context);
Fluttertoast.showToast(
backgroundColor: green,
textColor: Colors.white,
msg:
'Please enter correct email and password');
}
});
Try to include this while naming is same,
void setUser(UserData user) {
this.user = user;
print(user.sId);
notifyListeners();
}
Follow this structure
class UserController with ChangeNotifier {
UserData user = UserData();
UserData get userdata => user;
void setUser(UserData user) {
this.user = user;
print(user.sId);
notifyListeners();
}
Future<bool> login(String data) async {
await Future.delayed(Duration(seconds: 1));
UserData authUser = UserData(sId: data);
setUser(authUser);
notifyListeners();
return true;
}
}
class HPTest extends StatelessWidget {
const HPTest({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: Consumer<UserController>(
builder: (context, value, child) {
return Text(value.user.sId);
},
),
floatingActionButton: FloatingActionButton(onPressed: () async {
final result = await Provider.of<UserController>(context, listen: false)
.login("new ID");
print("login $result");
;
}),
);
}
}
I have created simple home screen for login and register,
Here I have taken readymade code from a channel, and now I need to change little bit..
code is simple, so no more details to explain
just I want to pass Usercredential to my welcome screen...
here is my code
class MainPage extends StatelessWidget {
const MainPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context,snapshot){
if(snapshot.hasData)
{
print(snapshot.data);
return WelcomePage(usercredential:
//how to get usercredential,
);
}
else
{
return AuthPage();
}
},
),
);
}
}
here is my login page's login code
Future signin() async {
UserCredential? usercredential;
try {
usercredential=await FirebaseAuth.instance.signInWithEmailAndPassword(
email: txtemailcontroller.text, password: txtpasswordcontroller.text);
} on FirebaseAuthException catch (e) {
print("Error is =" + e.toString());
}
}
and register page's register code
Future signup() async {
UserCredential? usercredential;
try {
if (txtconfirmpasswordcontroller.text.trim() ==
txtpasswordcontroller.text.trim()) {
usercredential = await FirebaseAuth.instance
.createUserWithEmailAndPassword(
email: txtemailcontroller.text,
password: txtpasswordcontroller.text);
} else {
print("passwrod does not match");
}
} on FirebaseAuthException catch (e) {
print('Error while register' + e.toString());
}
if (usercredential != null) {
String userid = usercredential.user!.uid;
UserModel newuser = UserModel(
email: txtemailcontroller.text,
userid: userid,
fullname:
txtfirstnamecontroller.text + ' ' + txtlastnamecontroller.text,
profilepicture: '');
}
}
so far I know, user credential generated while createuserwithemailandpassword and signinwithemailandpassword method, but how to get it here....where I need...
Add this In your Welcome page:
User? currentUser=FirebaseAuth.instance.currentUser;
Then you can call it any where and get user details like email & id & display name.
for example:
currentUser.email
or
currentUser.uid
The FirebaseAuth was working fine for me a few hours back but now I am receiving an error as :
Undefinded name 'FirebaseAuth' .I am unable to resolve it. Don't know what is the real issue. Would be a great help for me if anybody could step in and provide me a or more solutions.
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:jiwdopani/UserLogIn/MenuUse.dart';
import 'package:jiwdopani/services/error_handler.dart';
import 'package:jiwdopani/UserLogIn/login_page.dart';
class AuthService {
handleAuth() {
return StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (BuildContext context, snapshot) {
if (snapshot.hasData) {
return UserMenu();
} else
return LoginPage();
});
}
//Sign out
signOut() async {
try {
return await FirebaseAuth.instance.signOut();
}
catch(e){
print(e.toString());
return null;
}
}
//Sign In
signIn(String email, String password, context) {
FirebaseAuth.instance
.signInWithEmailAndPassword(email: email, password: password)
.then((val) {
UserMenu();
}).catchError((e) {
ErrorHandler().errorDialog(context, e);
});
}
//Signup a new user
signUp(String email, String password) {
return FirebaseAuth.instance
.createUserWithEmailAndPassword(email: email, password: password);
}
//Reset Password
resetPasswordLink(String email) {
FirebaseAuth.instance.sendPasswordResetEmail(email: email);
}
I fixed some of your code by adding types to the functions:
class AuthService {
Widget handleAuth() {
return StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (BuildContext context, snapshot) {
if (snapshot.hasData) {
return Container(); //change it back to your widget
} else
return Container();//change it back to your widget
});
}
//Sign out
void signOut() async {
try {
return await FirebaseAuth.instance.signOut();
} catch (e) {
print(e.toString());
return null;
}
}
//Sign In
void signIn(String email, String password, BuildContext context) {
FirebaseAuth.instance
.signInWithEmailAndPassword(email: email, password: password)
.then((val) {
return Container();
});
}
//Signup a new user
Future<UserCredential> signUp(String email, String password) async {
return await FirebaseAuth.instance
.createUserWithEmailAndPassword(email: email, password: password);
}
//Reset Password
void resetPasswordLink(String email) {
FirebaseAuth.instance.sendPasswordResetEmail(email: email);
}
}
Anyway, I think it's best not to have Widgets or UI in Auth Class, it is always better to separate UI from logic.
I have created a StreamController to handle authentication. I subscribe a user when the sign in is completed. So I create a class for that:
class AuthAPI {
final FacebookLogin facebookLogin = FacebookLogin();
final Dio _dio = Dio();
final StreamController<User> _authStatusController = StreamController<User>.broadcast();
Stream<User> get onAuthStatusChanged => _authStatusController.stream;
// Facebook Sign In
Future<User> facebookSignIn() async {
FacebookLoginResult result = await facebookLogin.logIn(['public_profile', 'email']);
switch(result.status) {
case FacebookLoginStatus.loggedIn:
return _sendFacebookUserDataToAPI(result);
case FacebookLoginStatus.error:
return null;
case FacebookLoginStatus.cancelledByUser:
print('Cancelled');
return null;
default:
return null;
}
}
// Sign Out
void signOut() async {
facebookLogin.logOut();
_authStatusController.sink.add(null);
_authStatusController.close();
}
Future<User> _sendFacebookUserDataToAPI(FacebookLoginResult result) async {
final String facebookToken = result.accessToken.token;
final Response graphResponse = await _dio.get(
'https://graph.facebook.com/v4.0/me?fields='
'first_name,last_name,email,picture.height(200)&access_token=$facebookToken');
final profile = jsonDecode(graphResponse.data);
ApiProvider apiProvider = ApiProvider();
UserSocialAuth userSocialAuth = UserSocialAuth(
firstName: profile['first_name'],
lastName: profile['last_name'],
email: profile['email'],
provider: 'facebook',
providerUserId: profile['id']
);
Map socialSignIn = await apiProvider.socialSignIn(userSocialAuth);
User user;
if (socialSignIn.containsKey('access_token')) {
Map userData = await apiProvider.currentUser(socialSignIn['access_token']);
user = User.fromJson(userData['data']);
apiProvider.setAccessToken(socialSignIn['access_token']);
_authStatusController.sink.add(user);
print("Login Successful");
} else {
_authStatusController.sink.addError(socialSignIn['error']);
}
_authStatusController.close();
return user;
}
}
and this is my StreamBuilder:
return StreamBuilder(
stream: userBloc.authStatus,
builder: (BuildContext context, AsyncSnapshot snapshot) {
print(snapshot.connectionState);
switch(snapshot.connectionState) {
case ConnectionState.active:
User user = snapshot.data;
if (user == null) {
return SignInSignUpScreen();
}
return _showHomeUI(user, snapshot);
case ConnectionState.done:
User user = snapshot.data;
if (user == null) {
return SignInSignUpScreen();
}
print(user);
return _showHomeUI(user, snapshot);
default:
return Center(child: CircularProgressIndicator());
}
}
);
So, when I make the login, then it shows a CircularProgressIndicator, and if the authentication is successful, then it has to show the home screen. But, it stills showing the login screen, and when I print the output of the connectionState, I see that after the connectionState.done, the connectionState pass to connectionState.waiting and I do not know why.
Here is the output of the console:
And when it reaches to the last connectionState.done, it does not have data.
You're calling _authStatusController.close(); in the end of _sendFacebookUserDataToAPI method – that means that the underlying stream is finished and you stream listener enters "done" state.
You should instead create e.g. dispose() method in AuthAPI class and call _authStatusController.close() there. This method should be called when AuthAPI is no longer needed.
from this link on my web server as
http://instamaker.ir/api/v1/getPersons
i'm trying to get result and printing avatar from that result, unfortunately my implementation with rxDart and Bloc don't get result from this response and i don't get any error
server response this simplified result:
{
"active": 1,
"name": "my name",
"email": " 3 ",
"loginType": " 3 ",
"mobile_number": " 3 ",
...
"api_token": "1yK3PvAsBA6r",
"created_at": "2019-02-12 19:06:34",
"updated_at": "2019-02-12 19:06:34"
}
main.dart file: (click on button to get result from server)
StreamBuilder(
stream: bloc.login,
builder: (context,
AsyncSnapshot<UserInfo>
snapshot) {
if (snapshot.hasData) {
parseResponse(snapshot);
}
},
);
void parseResponse(AsyncSnapshot<UserInfo> snapshot) {
debugPrint(snapshot.data.avatar);
}
LoginBlock class:
class LoginBlock{
final _repository = Repository();
final _login_fetcher = PublishSubject<UserInfo>();
Observable<UserInfo> get login=>_login_fetcher.stream;
fetchLogin() async{
UserInfo userInfo = await _repository.userInfo();
_login_fetcher.sink.add(userInfo);
}
dispose(){
_login_fetcher.close();
}
}
final bloc = LoginBlock();
Repository class:
class Repository {
final userInformation = InstagramApiProviders();
Future<UserInfo> userInfo() => userInformation.checkUserLogin();
}
my model:
class UserInfo {
int _active;
String _name;
...
UserInfo.fromJsonMap(Map<String, dynamic> map)
: _active = map["active"],
_name = map["name"],
...
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['active'] = _active;
data['name'] = _name;
...
return data;
}
//GETTERS
}
BaseUrl class:
class BaseUrl {
static const url = 'http://instamaker.ir';
}
and then InstagramApiProviders class:
class InstagramApiProviders {
Client client = Client();
Future<UserInfo> checkUserLogin() async {
final response = await client.get(BaseUrl.url+'/api/v1/getPersons');
print("entered "+BaseUrl.url+'/api/v1/getPersons');
if (response.statusCode == 200) {
return UserInfo.fromJsonMap(json.decode(response.body));
} else
throw Exception('Failed to load');
}
}
Well the answer here is part of the test that I make to get this done. I can put my all test here but I think that the problem cause was because as StreamBuilder is a widget his builder method callback is only called when the widget is in flutter widget tree. As in your sample you're just creating a StreamBuilder the builder method will never be called bacause this widget isn't in widget tree.
As advice first test your code changing only UI layer... do somenthing like:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: <Widget>[
IconButton(icon: Icon(Icons.assessment), onPressed: () => loginBlock.fetchLogin()),
],
),
body: StreamBuilder<UserInfo>(
stream: loginBlock.login,
builder: (context, snapshot){
if (snapshot.hasData){
parseResponse(snapshot);
return Text('user: ${snapshot.data.name} ');
}
if (snapshot.hasError)
return Text('${snapshot.error}');
else return Text('There is no data');
},
),
);
Here we're putting the StreamBuilder in widget tree so the builder callback is called and maybe you will see the results. If it fails, please comment that I update my answer with my full test code with this working.
Updating the answer with sources that I made tests.
Basic model
class UserInfo {
int _active;
String name;
UserInfo.fromJsonMap(Map<String, dynamic> map) {
_active = map["active"];
name = map["name"];
}
Map<String, dynamic> toJson() => {
'active' : _active,
'name' : name,
};
}
The provider class
class InstagramApiProviders {
Future<UserInfo> checkUserLogin() async {
UserInfo info;
try {
http.Response resp = await http.get("http://instamaker.ir/api/v1/getPersons");
if (resp.statusCode == 200){
print('get response');
print( resp.body );
info = UserInfo.fromJsonMap( Map.from( json.decode(resp.body ) ));
}
}
catch (ex) {
throw ex;
}
print('returning $info');
return info;
}
}
Repository
class Repository {
final userInformation = InstagramApiProviders();
Future<UserInfo> userInfo() => userInformation.checkUserLogin().then((user) => user);
}
BLoC class
class LoginBlock{
final _repository = Repository();
final _login_fetcher = PublishSubject<UserInfo>();
Observable<UserInfo> get login=>_login_fetcher.stream;
fetchLogin() async {
UserInfo info = await _repository.userInfo();
_login_fetcher.sink.add(info);
}
dispose(){
_login_fetcher.close();
}
}
Widget UI
This starts showing There is no data message but when you hit appBar button wait a little and then the data is fetched and updates the UI.
class WidgetToShowData extends StatelessWidget {
final LoginBlock bloc = LoginBlock();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: <Widget>[
IconButton(icon: Icon(Icons.assessment), onPressed: () => loginBlock.fetchLogin()),
],
),
body: StreamBuilder<UserInfo>(
stream: loginBlock.login,
builder: (context, snapshot){
if (snapshot.hasData){
parseResponse(snapshot);
return Text('user: ${snapshot.data.name} ');
}
if (snapshot.hasError)
return Text('${snapshot.error}');
else return Text('There is no data');
},
),
);
}
void parseResponse(AsyncSnapshot<UserInfo> snapshot) {
debugPrint(snapshot.data.name);
}
}