You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter StreamBuilder无法识别Firebase Auth登录状态问题排查

修复Flutter Firebase Auth登录后StreamBuilder不触发页面跳转的问题

问题描述

在Flutter应用中实现基于Firebase Auth的邮箱优先登录流程:通过StreamBuilder监听用户登录状态自动跳转页面,已登录进入HomeView,未登录进入InsertEmailView;未登录时根据输入邮箱是否已注册,跳转至LoginView或RegisterView,预期流程为InsertEmailView -> LoginView / RegisterView -> HomeView。

异常现象:直接进入LoginView登录后可正常跳转至HomeView,但从InsertEmailView跳转进入LoginView完成登录后,StreamBuilder无法识别登录状态变化,无法自动跳转到HomeView。

相关代码实现

StreamBuilder实现

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 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)
                  )
                );
              }
            },);
          });
        }
      }
    );
  }
}

AuthService实现

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");
    } catch (error) {
      print('Error checking email: $error');
      return false;
    }
  }
}

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();
      },
      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()
          ],
        ),
      ),
    );
  }
}

问题原因

  1. AuthService实例不统一:每次调用AuthService()都会创建新实例,StreamBuilder监听的是一个实例的stream,而登录操作是另一个实例发起的,导致状态变化事件无法传递到监听的stream。
  2. 导航逻辑冲突:在StreamBuilder的builder函数中返回InsertEmailView后,用Navigator.push跳转LoginView会将其叠在InsertEmailView上方,即使StreamBuilder监听到登录状态变化并重建为HomeView,用户仍会看到导航栈上层的LoginView。

修复步骤

1. 将AuthService改为全局单例

确保所有组件使用同一个AuthService实例,保证stream监听和登录操作的一致性:

class AuthService with ChangeNotifier {
  // 单例实现
  static final AuthService _instance = AuthService._internal();
  factory AuthService() => _instance;
  AuthService._internal();

  final FirebaseAuth _auth = FirebaseAuth.instance;

  User? get currentUser => _auth.currentUser;

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

  // 剩余原有方法保持不变
}

2. 重构导航逻辑,使用Navigator.replace替代push

修改StreamBuilder中的导航代码,用pushReplacement替换当前页面,避免导航栈残留未登录页面:

class _WidgetTreeState extends State<WidgetTree>{
  final AuthService _authService = AuthService(); // 复用单例

  @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 {
          // 用MaterialApp包裹未登录页面,统一管理导航栈
          return MaterialApp(
            home: InsertEmailView(onEmailSubmitted: (email) {
              _authService.isEmailInUse(email).then((isEmailInUse) {
                if (isEmailInUse){
                  Navigator.pushReplacement(
                    context, 
                    MaterialPageRoute(
                      builder: (context) => LoginView(email: email)
                    )
                  );
                } else {
                  Navigator.pushReplacement(
                    context, 
                    MaterialPageRoute(
                      builder: (context) => RegisterView(email: email)
                    )
                  );
                }
              });
            }),
          );
        }
      }
    );
  }
}

3. 确保LoginView使用单例AuthService

修改LoginView中的登录方法,确保调用单例实例的登录接口:

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

说明

修改后,用户从InsertEmailView跳转至LoginView登录成功时,AuthService的authStateChanges stream会触发登录状态事件,StreamBuilder监听到变化后会自动重建并返回HomeView;同时pushReplacement移除了导航栈中的未登录页面,用户能直接看到HomeView。

内容的提问来源于stack exchange,提问作者soosmann

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 11:55:56