Firebase 身份验证不会在 Flutter 中保留在 iOS 或 Android 上

Posted

技术标签:

【中文标题】Firebase 身份验证不会在 Flutter 中保留在 iOS 或 Android 上【英文标题】:Firebase auth not persisting on iOS or Android in Flutter 【发布时间】:2021-08-29 19:03:04 【问题描述】:

首先,我在 Stack Overflow 上就这个话题经历了 20 多个不同的问题和解决方案(其中大部分与网页版有关),我也尝试过 twitter,甚至 FlutterDev Discord 服务器也无法好像发现了这个问题。

我正在使用 firebase 对我的应用进行移动身份验证,无论我尝试什么,我似乎都无法让持久身份验证状态在 iosandroid 上工作。

这是我的主要内容:

Future<void> main() async 
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp();
    runApp(
        MultiProvider(
            ...
        child: const MyApp(),
        ),
    );


class MyApp extends StatelessWidget 
    const MyApp(Key? key) : super(key: key);
    final const ColorScheme colorScheme = ColorScheme(
        ...
    );
    
    @override
    Widget build(BuildContext context) 
        bool isDebug = false;
        if (Constants.DEBUG_BANNER == 'true') 
            isDebug = true;
        
        return MaterialApp(
            theme: ThemeData(
                ...
            ),
            routes: 
               // This is a general layout of how all my routes are in case this is the issue
               Screen.route: (BuildContext context) => const Screen(),
            ,
            home: const HomeScreen(),
            debugShowCheckModeBanner: isDebug,
        );
    

... 只是我认为与我的问题无关的代码,因此为了简洁起见,我将其隐藏起来。主要是主题和私人数据

让我们从我的 google-sign-in-button 开始,如有必要,我可以分享其他重要的信息。我们在 iOS 上使用 Facebook、Google 和 Apple。

class GoogleSignInButton extends StatefulWidget 
  const GoogleSignInButton(Key? key) : super(key: key);

  @override
  _GoogleSignInButtonState createState() => _GoogleSignInButtonState();


class _GoogleSignInButtonState extends State<GoogleSignInButton> 
  bool _isSigningIn = false;

  @override
  Widget build(BuildContext context) 
    return Padding(
      padding: const EdgeInsets.only(bottom: 16.0),
      child: _isSigningIn
          ? CircularProgressIndicator(
              valueColor: AlwaysStoppedAnimation<Color>(MRRM.colorScheme.primary),
            )
          : OutlinedButton(
              key: const Key('google_sign_in_button'),
              style: ButtonStyle(
                backgroundColor: MaterialStateProperty.all(Colors.white),
                shape: MaterialStateProperty.all(
                  RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(40),
                  ),
                ),
              ),
              onPressed: () async 
                setState(() 
                  _isSigningIn = true;
                );

                context.read<Member>().signInWithGoogle(context: context).then<void>((void user) 
                  setState(() 
                    _isSigningIn = false;
                  );

                  Navigator.pushReplacementNamed(context, UserInfoScreen.route);
                );
              ,
              child: Padding(
                padding: const EdgeInsets.fromLTRB(0, 10, 0, 10),
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    const Image(
                      image: AssetImage('assets/images/png/google_logo.png'),
                      height: 35.0,
                    ),
                    Padding(
                        padding: const EdgeInsets.only(left: 10),
                        child: Text(
                          'Sign in with Google',
                          style: TextStyle(
                            fontSize: 20,
                            color: MRRM.colorScheme.secondary,
                            fontWeight: FontWeight.w600,
                          ),
                        ))
                  ],
                ),
              ),
            ),
    );
  

我正在使用provider pub,这是context.read&lt;Object?&gt;() 的来源。

这里是signInWithGoogle函数;

Future<String> signInWithGoogle(required BuildContext context) async 
    final FirebaseAuth _auth = FirebaseAuth.instance;

    final GoogleSignIn googleSignIn = GoogleSignIn();

    final GoogleSignInAccount? googleSignInAccount =
        await googleSignIn.signIn();

    if (googleSignInAccount != null) 
      final GoogleSignInAuthentication googleSignInAuthentication =
          await googleSignInAccount.authentication;

      final AuthCredential credential = GoogleAuthProvider.credential(
        accessToken: googleSignInAuthentication.accessToken,
        idToken: googleSignInAuthentication.idToken,
      );

      try 
        final UserCredential userCredential =
            await _auth.signInWithCredential(credential);

        _firebaseUser = userCredential.user!;
        _authType = AuthType.Google;
        _uuId = _firebaseUser.uid;
        notifyListeners();
       on FirebaseAuthException catch (e) 
        if (e.code == 'account-exists-with-different-credential') 
          ScaffoldMessenger.of(context).showSnackBar(
            customSnackBar(
              content: 'The account already exists with different credentials.',
            ),
          );
         else if (e.code == 'invalid-credential') 
          ScaffoldMessenger.of(context).showSnackBar(
            customSnackBar(
              content: 'Error occurred while accessing credentials. Try again.',
            ),
          );
        
       catch (e) 
        ScaffoldMessenger.of(context).showSnackBar(
          customSnackBar(
            content: 'Error occurred using Google Sign-In. Try again.',
          ),
        );
      
    
    return getMemberLogin();
  

这包含在我的 Member 对象中,它只存储所有 Auth 数据以及来自我们内部 API 之一的特定于成员的数据,并且成员数据作为 App State 对象存储在提供程序中,它链接在 main.dart 文件中

getMemberLogin() 函数只是从身份验证中获取 UUID 并将其发送到 API 并获取内部成员数据,我希望一个简单的发布请求不是导致这种情况的原因。但如果您认为它可能会让我知道,我会尝试在混淆任何 NDA 相关数据的同时发布它。

这是处理初始路由并转到loadingScreen 的主屏幕/启动屏幕,它应该检查是否存在持久登录并转到 UserInfo 屏幕而不是 Auth 屏幕。

class HomeScreen extends StatefulWidget 
  const HomeScreen(Key? key) : super(key: key);
  static const String route = '/home';

  @override
  _HomeScreenState createState() => _HomeScreenState();


class _HomeScreenState extends State<HomeScreen> 
  @override
  Widget build(BuildContext context) 
    return Scaffold(
      body: Column(
        key: const Key('Home'),
        children: <Widget>[
          Expanded(
            child: Image.asset('assets/images/png/Retail_Rebel_Primary.png'),
          ),
          BlinkingTextButton(
            key: const Key('blinking_text_button'),
            textButton: TextButton(
              child: Text(
                'Tap to continue',
                style: TextStyle(
                  color: MRRM.colorScheme.primary,
                  fontSize: 16.0,
                ),
              ),
              onPressed: () 
                Navigator.of(context).pushReplacementNamed(LoadingScreen.route);
              ,
            ),
          ),
          Container(
            height: 8.0,
          ),
        ],
      ),
    );
  

最后,这是主屏幕导航到的LoadingScreen

class LoadingScreen extends StatelessWidget 
  const LoadingScreen(Key? key) : super(key: key);
  static const String route = '/loadingScreen';
  @override
  Widget build(BuildContext context) 
    return StreamBuilder<User?>(
      stream: FirebaseAuth.instance.authStateChanges(),
      builder: (BuildContext context, AsyncSnapshot<User?> snapshot) 
        if (snapshot.connectionState == ConnectionState.waiting) 
          if (snapshot.hasData) 
            print('user is logged in');
            SchedulerBinding.instance!.addPostFrameCallback((_) 
              Navigator.of(context).pushReplacementNamed(UserInfoScreen.route);
            );
            return const Text('');
           else 
            print('no user is logged in');
            SchedulerBinding.instance!.addPostFrameCallback((_) 
              Navigator.of(context).pushReplacementNamed(AuthScreen.route);
            );
            return const Text('');
          
        
        return const SplashScreen();
      ,
    );
  

不确定我处理路由的方式是否可能是问题,但我使用Navigator.of(context).pushReplacementNamed(); 是很常见的,除非需要弹出然后我通常只使用Navigator.of(context).pop();。我通常只将.pop() 用于模态框/alertDialogs,以及诸如 QR 扫描仪之类的东西返回到上一个屏幕。

对不起,如果信息太多,或者我忘记了很多东西。一个多星期以来,我一直在努力解决这个问题,现在有点沮丧。

感谢您的所有回复。

仅仅因为我认为看看我已经看过的内容很重要,这里列出了我看过的其他几个问题,但没有帮助。

This 我相信它的日期为 2020 年 8 月,特别是考虑到 onAuthStateChanges 已更改为流 authStateChanges()

我也尝试过以文档here 中描述的确切方式实现身份验证,但同样的问题。

我也尝试过使用:

FirebaseAuth.instance.authStateChanges().then((User? user) 
    if (user != null) 
       Navigator.of(context).pushReplacementNamed(UserInfoScreen.route);
     else 
       Navigator.of(context).pushReplacementNamed(AuthScreen.route);
    

这没有用。我还尝试简单地检查是否有当前用户:

User user = FirebaseAuth.instance.currentUser;
if (user != null && user.uid != null) 
    Navigator.of(context).pushReplacementNamed(UserInfoScreen.route);
 else 
    Navigator.of(context).pushReplacementNamed(AuthScreen.route);

仍然总是去AuthScreen 我也尝试了所有这些方法作为异步任务,看看是否可能只需要一秒钟的时间来加载,以及同样的问题。最奇怪的是使用当前方法,如果我从LoadingScreen 中取出if(snapshot.connectionState == ConnectionState.waiting),它将立即打印出no user is logged in,然后是user is logged in,然后再次no user is logged in,然后它将导航到AuthScreen

【问题讨论】:

最后一个方法可以用ConnectionState.active代替connectionstate.waiting Welp 成功了,谢谢...... 【参考方案1】:

如果您按照我在上面所做的操作并进行一次更改,它将适用于持久登录。

改变:

class LoadingScreen extends StatelessWidget 
  const LoadingScreen(Key? key) : super(key: key);
  static const String route = '/loadingScreen';
  @override
  Widget build(BuildContext context) 
    return StreamBuilder<User?>(
      stream: FirebaseAuth.instance.authStateChanges(),
      builder: (BuildContext context, AsyncSnapshot<User?> snapshot) 
        if (snapshot.connectionState == ConnectionState.waiting) 
          if (snapshot.hasData) 
            print('user is logged in');
            SchedulerBinding.instance!.addPostFrameCallback((_) 
              Navigator.of(context).pushReplacementNamed(UserInfoScreen.route);
            );
            return const Text('');
           else 
            print('no user is logged in');
            SchedulerBinding.instance!.addPostFrameCallback((_) 
              Navigator.of(context).pushReplacementNamed(AuthScreen.route);
            );
            return const Text('');
          
        
        return const SplashScreen();
      ,
    );
  

class LoadingScreen extends StatelessWidget 
  const LoadingScreen(Key? key) : super(key: key);
  static const String route = '/loadingScreen';
  @override
  Widget build(BuildContext context) 
    return StreamBuilder<User?>(
      stream: FirebaseAuth.instance.authStateChanges(),
      builder: (BuildContext context, AsyncSnapshot<User?> snapshot) 
        if (snapshot.connectionState == ConnectionState.active) 
          if (snapshot.hasData) 
            print('user is logged in');
            SchedulerBinding.instance!.addPostFrameCallback((_) 
              Navigator.of(context).pushReplacementNamed(UserInfoScreen.route);
            );
            return const Text('');
           else 
            print('no user is logged in');
            SchedulerBinding.instance!.addPostFrameCallback((_) 
              Navigator.of(context).pushReplacementNamed(AuthScreen.route);
            );
            return const Text('');
          
        
        return const SplashScreen();
      ,
    );
  

【讨论】:

以上是关于Firebase 身份验证不会在 Flutter 中保留在 iOS 或 Android 上的主要内容,如果未能解决你的问题,请参考以下文章

Flutter - 将 Firebase 通知推送给没有 Firebase 身份验证的特定用户

Flutter 中的 Firebase 身份验证状态检查

Flutter + Firebase Auth:有啥方法可以在 Web 上使用 Firebase 电话身份验证重新发送短信验证码?

Firebase (Flutter) 验证电话号码始终需要 reCAPTCHA

用于 Flutter 桌面嵌入的 Firebase 身份验证插件

Flutter:Firebase 身份验证和 Firestore 快照流