flutterfirebasefirebase-authenticationstream-builderflutter-streambuilder

Flutter StreamBuilder does not recognize Firebase Auth login


I am implementing an email-first process to login to my Flutter app with Firebase Auth. For the user flow I am using a StreamBuilder that should recognize if a user is signed in or not. Depending on being signed in or not the user should be redirected appropriately. The problem is that the StreamBuilder does not navigate to the HomeView() after a user signed in.

Here is the implementation of my StreamBuilder: If the user is logged in, the app should go to the HomeView(), otherwise to InsertEmailView(). When not logged in, depending on if the inserted email is used or not, navigate to the LoginView() or the RegisterView().
So this is the wanted navigation: InsertEmailView -> LoginView / RegisterView -> HomeView.
However, if a user starts in LoginView() (without usage of InsertEmailView()), the redirection to HomeView() works, otherwise not.

class _WidgetTreeState extends State<WidgetTree>{
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<User?>(
      stream: AuthService().authStateChanges, 
      builder: (context, snapshot){
        if (snapshot.connectionState == ConnectionState.waiting){
          return const CircularProgressIndicator();
        } else if (snapshot.hasData){
          return HomeView();
        } else {
          //return const LoginView(email: "1@1.de");
          return InsertEmailView(onEmailSubmitted: (email) {
            AuthService().isEmailInUse(email).then((isEmailInUse) {
              print(isEmailInUse);
              if (isEmailInUse){
                Navigator.push(
                  context, 
                  MaterialPageRoute(
                    builder: (context) => LoginView(email: email)
                  )
                );
              } else {
                Navigator.push(
                  context, 
                  MaterialPageRoute(
                    builder: (context) => RegisterView(email: email)
                  )
                );
              }
            },);
          });
        }
      }
    );
  }
}

As you can see, I implemented an AuthService() that implements necessary Firebase Auth methods I need, and even checks if the given email is used or not (which works well).
I implemented authStateChanges as follows:

class AuthService with ChangeNotifier {
  final FirebaseAuth _auth = FirebaseAuth.instance;

  User? get currentUser => _auth.currentUser;

  Stream<User?> get authStateChanges => _auth.authStateChanges();

  Future<void> signInWithEmailAndPassword({
    required String email, 
    required String password
  }) async {
    await _auth.signInWithEmailAndPassword(
      email: email, 
      password: password
    );
  }
  
  Future<void> createUserWithEmailAndPassword({
    required String email, 
    required String password
  }) async {
    await _auth.createUserWithEmailAndPassword(
      email: email, 
      password: password
    );
  }

  Future<void> signOut() async {
    await _auth.signOut();
  }

  Future<bool> isEmailInUse(String email) async {
    try {
      List<String> signInMethods = await _auth.fetchSignInMethodsForEmail(email);
      return signInMethods.contains("password"); // usual auth method is "password"
    } catch (error) {
      print('Error checking email: $error');
      return false;
    }
  }
}

Here you also have the implementation of LoginView():

class LoginView extends StatefulWidget {
  final String email;

  const LoginView({super.key, required this.email});

  @override
  State<LoginView> createState() => _LoginViewState(email: email);
}

class _LoginViewState extends State<LoginView> {
  String? errorMessage = '';
  String email;

  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  _LoginViewState({required this.email}){
    _emailController.text = email;
  }

  Future<void> signInWithEmailAndPassword() async {
    try {
      await AuthService().signInWithEmailAndPassword(
        email: _emailController.text, 
        password: _passwordController.text
      );
    } on FirebaseAuthException catch (e) {
      setState(() {
        errorMessage = e.message;
      });
    }
  }

  Widget _title(){
    return const Padding(
      padding: EdgeInsets.all(30.0),
      child: Text(
        "Logge dich mit\nE-Mail und Passwort ein!",
        textAlign: TextAlign.center,
        style: TextStyle(
          fontSize: 20,
          fontWeight: FontWeight.bold,
        ),
      ),
    );
  } 

  Widget _entryField(
    String title,
    TextEditingController controller,
    bool obscureText
  ){
    return SizedBox(
      width: 200,
      child: TextField(
        controller: controller,
        obscureText: obscureText,
        decoration: InputDecoration(
          labelText: title,
          border: const OutlineInputBorder(),
        ),
      ),
    );
  }

  Widget _errorMessage(){
    return Text(
      errorMessage == '' ?  '' : 'Humm ? $errorMessage',
      style: const TextStyle(
        color: Colors.red
      ),
    );
  }

  Widget _signInButton(){
    return ElevatedButton(
      onPressed: () {
        signInWithEmailAndPassword();//.then((value) => {
          //Navigator.push(context, MaterialPageRoute(builder: (context) => HomeView()))
        //});
      },
      child: const Text("Anmelden"),
    );
  }

  Widget _switchToRegister(){
    return ElevatedButton(
      onPressed: () {
        Navigator.push(context, MaterialPageRoute(builder: (context) => RegisterView(email: email),));
      },
      child: const Text("Doch registrieren?")
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            _title(),
            _entryField('E-Mail', _emailController, false),
            const SizedBox(height: 16),
            _entryField('Passwort', _passwordController, true),
            const SizedBox(height: 16),
            _errorMessage(),
            const SizedBox(height: 16),
            _signInButton(),
            const SizedBox(height: 16),
            _switchToRegister()
          ],
        ),
      ),
    );
  }
}

I can imagine that the Navigator breaks the snapshot implementation. I also tried implementing AuthService as a variable to not have multiple AuthServices in the StreamBuilder which also did not work.

Does anyone know how to fix my issue?


Solution

  • I'm not entirely sure if this works, but I believe the main problem is that you are pushed to a new route so you are no longer in your StreamBuilder, so I think it might be fixed if you simple pop back to it after sign in. That is. change

      Widget _signInButton(){
        return ElevatedButton(
          onPressed: () {
            signInWithEmailAndPassword();//.then((value) => {
              //Navigator.push(context, MaterialPageRoute(builder: (context) => HomeView()))
            //});
          },
          child: const Text("Anmelden"),
        );
      }
    

    to

      Widget _signInButton(){
        return ElevatedButton(
          onPressed: () {
            signInWithEmailAndPassword().then((value) => {
              Navigator.pop(context);
            });
          },
          child: const Text("Anmelden"),
        );
      }
    

    What also is a problem is that you call AuthService() all the time which creates a new instance of it every time. It needs to be a singleton. The easiest way to do this is to add these lines to the class

      static final AuthService _singleton = AuthService._internal();
      
      factory AuthService() {
        return _singleton;
      }
      
      AuthService._internal();