刚刚开始一个 flutter 个人项目。我正在开发 Flutter 应用程序中的入职页面,该页面将在创建新用户后出现。目前有两种方式创建用户:电子邮件/密码或 Google 登录。我的入门页面可以通过
Navigator.pushNamed(context, '/onboarding');
正确推送。但是,当我尝试使用 GoogleSignin 执行相同的流程时,它决定在推送新页面之前更改页面。这使它失去上下文并给出此错误。
[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: This widget has been unmounted, so the State no longer has a context (and should be considered defunct).
Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active.
#0 State.context.<anonymous closure> (package:flutter/src/widgets/framework.dart:951:9)
#1 State.context (package:flutter/src/widgets/framework.dart:957:6)
#2 _LoginPageState._handleGoogleSignIn (package:silo_app/pages/auth/login_page.dart:64:29)
<asynchronous suspension>
这是我如何处理谷歌登录以及如何为我的主页或authpages渲染页面的代码。
void _handleGoogleSignIn() async {
bool isNewUser = false;
try {
isNewUser = await GoogleSignInService().signInWithGoogle(context);
} catch (error) {
print("Google sign-in failed: $error");
} finally {
if (mounted) {
Navigator.pop(context);
} else {
print("not mounted so it can't be popped");
}
if (isNewUser) {
Navigator.pushNamed(context, '/onboarding');
} else {
Navigator.pushReplacementNamed(context, '/home');
}
}
}
class GoogleSignInService {
final GoogleSignIn _googleSignIn = GoogleSignIn();
Future<bool> signInWithGoogle(BuildContext context) async {
print("Start of 3rd party signin");
try {
print('Signing in with Google');
final GoogleSignInAccount? googleSignInAccount =
await _googleSignIn.signIn();
if (googleSignInAccount == null) {
print('Google sign-in was cancelled.');
return false;
}
final GoogleSignInAuthentication googleSignInAuthentication =
await googleSignInAccount.authentication;
final AuthCredential authCredential = GoogleAuthProvider.credential(
accessToken: googleSignInAuthentication.accessToken,
idToken: googleSignInAuthentication.idToken,
);
final UserCredential credentials =
await FirebaseAuth.instance.signInWithCredential(authCredential);
User? user = credentials.user;
if (user != null) {
print('User signed in successfully: ${user.uid}');
DocumentSnapshot userDoc = await FirebaseFirestore.instance
.collection("Users")
.doc(user.uid)
.get();
if (!userDoc.exists) {
print('User does not exist in Firestore, creating user...');
await FirebaseFirestore.instance
.collection("Users")
.doc(user.uid)
.set({
"email": user.email,
"username": user.displayName ?? user.email!.split("@")[0],
"dateCreated": DateTime.now(),
"uid": user.uid,
});
print("User created successfully");
return true;
} else {
print('User already exists, navigating to home...');
return false;
}
}
return false;
} catch (error) {
print(error.toString());
return false;
}
}
}
class AuthPage extends StatelessWidget {
const AuthPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFF010922),
body: StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
} else if (snapshot.hasData) {
return HomePage();
} else {
return LoginOrRegisterPage();
}
}),
);
}
}
我怀疑问题在于上下文和小部件如何根据streamBuilder 进行更改。因为当它得到谷歌的签名时,它会自动改变。但是,为什么它适用于普通的 firebase 寄存器呢?以及阻止小部件在我的代码执行完成之前退出的解决方案是什么。
我让服务返回一个变量,判断它是否是新用户,然后处理调用 Google 登录服务的页面中的路由。我认为这会让小部件保持安装状态,但当异步signInWithCredential通过时,它似乎立即切换到主页。我想知道最好的方法是什么。
感谢您的帮助!
当
state
的 FirebaseAuth
更改时,您的 StreamBuilder
函数会触发,它会删除 LoginOrRegisterPage
,并且 context
会更改为 mounted = false
。在这种情况下,您无法使用它的上下文导航到其他屏幕。
您正确地认识到,当身份验证状态发生变化时,
StreamBuilder
会重建小部件,从而导致小部件丢失上下文。发生这种情况是因为 authStateChanges()
流在导航逻辑完成之前触发了重建,从而导致错误。
要正确处理此问题,您可以使用
StatefulWidget
更有效地管理状态并单独控制导航。
StatefulWidget
进行身份验证和导航通过在
StatefulWidget
内处理导航,您可以更好地管理状态并在 StreamBuilder
外执行导航。
以下是修改代码的方法:
class AuthPage extends StatefulWidget {
const AuthPage({Key? key}) : super(key: key);
@override
_AuthPageState createState() => _AuthPageState();
}
class _AuthPageState extends State<AuthPage> {
bool _isNewUser = false; // To track if the user is new
bool _isLoading = true; // To handle loading state
bool _isNavigating = false; // To prevent multiple navigations
@override
void initState() {
super.initState();
_checkUserStatus(); // Check if the user is new or existing
}
Future<void> _checkUserStatus() async {
setState(() {
_isLoading = true;
});
User? user = FirebaseAuth.instance.currentUser;
if (user != null) {
DocumentSnapshot userDoc = await FirebaseFirestore.instance
.collection("Users")
.doc(user.uid)
.get();
if (!userDoc.exists) {
_isNewUser = true;
}
}
setState(() {
_isLoading = false;
});
_navigateUser();
}
void _navigateUser() {
if (_isNavigating) return; // Prevent multiple navigations
_isNavigating = true;
if (_isNewUser) {
Navigator.pushNamed(context, '/onboarding');
} else {
Navigator.pushReplacementNamed(context, '/home');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFF010922),
body: _isLoading
? Center(
child: CircularProgressIndicator(),
)
: StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
} else if (snapshot.hasData) {
return HomePage(); // Show the home page if a user is authenticated
} else {
return LoginOrRegisterPage(); // Show the login page if no user is authenticated
}
},
),
);
}
}
使用
StatefulWidget
管理 state
并在检查其 status
后处理用户导航。
_checkUserStatus
方法:此方法通过检查Firestore
中的数据来确定用户是新用户还是现有用户。
_navigateUser
方法:根据用户是新用户还是现有用户来管理导航逻辑。
_isNewUser
:确定用户是否应该进行入职培训。
_isLoading
:检查用户时显示加载指示器status
。
_isNavigating
:在 StreamBuilder
重建时防止多次导航。
Better State Control
:使用 StatefulWidget
可以让您更好地控制 navigation
流,将其与对 StreamBuilder
变化做出反应的 authentication state
分开。
Avoid Context Loss
:只有在确定用户状态后才会触发导航逻辑,防止上下文丢失和相关错误。
通过遵循此方法,您可以确保您的入职流程对于
Google Sign-In
和 email/password
身份验证都能正确运行。