我使用的是带有shellroute设置的基本GoRouter,带有一个侧边导航条,旨在保持页面之间的一致。对Firebase的登录或注销调用都会生成断言,但我不明白为什么?任何提示将不胜感激。下列代码:
Pubspec:
flutter:
sdk: flutter
firebase_core: ^2.1.0
firebase_auth: ^4.0.2
firebase_storage: ^11.0.2
firebase_crashlytics: ^3.0.2
firebase_analytics: ^10.0.2
flutter_riverpod: ^2.0.2
cloud_firestore: ^4.0.2
intl: ^0.17.0
equatable: ^2.0.3
google_sign_in: ^5.0.7
sign_in_with_apple: ^4.1.0
crypto: ^3.0.1
rxdart: ^0.27.1
flutter_form_builder: ^7.7.0
form_builder_validators: ^8.3.0
logger: ^1.0.0
shared_preferences: ^2.0.7
google_fonts: ^3.0.1
package_info_plus: ^1.0.6
responsive_framework: ^0.2.0
flex_color_scheme: ^6.0.1
go_router: ^6.0.0
顶级提供者:
final GlobalKey<NavigatorState> _rootNavigatorKey =
GlobalKey(debugLabel: 'root');
final GlobalKey<NavigatorState> _shellNavigatorKey =
GlobalKey(debugLabel: 'shell');
final providers = [EmailAuthProvider()];
final firebaseAuthService = Provider<FirebaseAuthService>(
(ref) => FirebaseAuthService(FirebaseAuth.instance));
class AuthenticationNotifier extends StateNotifier<bool> {
AuthenticationNotifier(this._authenticationRepository) : super(false) {
_authenticationRepository.firebaseAuth.authStateChanges().listen((user) {
if (user == null) {
state = false;
} else {
state = true;
}
});
}
final FirebaseAuthService _authenticationRepository;
}
final authenticationListenerProvider =
StateNotifierProvider<AuthenticationNotifier, bool>(
(ref) => AuthenticationNotifier(ref.watch(firebaseAuthService)),
);
final goRouterProvider = Provider<GoRouter>((ref) {
final auth = ref.watch(authenticationListenerProvider);
return GoRouter(
navigatorKey: _rootNavigatorKey,
initialLocation: '/home',
routes: <RouteBase>[
/// Application shell
ShellRoute(
navigatorKey: _shellNavigatorKey,
builder: (BuildContext context, GoRouterState state, Widget child) {
return ScaffoldWithNavBar(child: child);
},
routes: <RouteBase>[
GoRoute(
path: '/',
pageBuilder: (BuildContext context, GoRouterState state) {
return NoTransitionPage(child: HomePage());
},
),
GoRoute(
path: '/home',
pageBuilder: (BuildContext context, GoRouterState state) {
return NoTransitionPage(child: HomePage());
},
),
GoRoute(
path: '/login',
pageBuilder: (BuildContext context, GoRouterState state) {
return NoTransitionPage(
child: SignInScreen(
providers: providers,
actions: [
AuthStateChangeAction<SignedIn>((context, state) {
if (state.user != null) GoRouter.of(context).go('/home');
}),
],
));
},
),
GoRoute(
path: '/account',
redirect: ((context, state) {
if (auth == false) {
return '/login';
} else {
return null;
}
}),
pageBuilder: (BuildContext context, GoRouterState state) {
return NoTransitionPage(child: AccountPage());
},
),
GoRoute(
path: '/surveys',
pageBuilder: (BuildContext context, GoRouterState state) {
return NoTransitionPage(child: SurveyPage());
},
),
],
),
],
);
});
class ScaffoldWithNavBar extends ConsumerWidget {
ScaffoldWithNavBar({
required this.child,
Key? key,
}) : super(key: key);
/// The widget to display in the body of the Scaffold.
/// In this sample, it is a Navigator.
final Widget child;
int selectedIndex = 0;
@override
Widget build(BuildContext context, WidgetRef ref) {
final auth = ref.watch(authenticationListenerProvider);
return Scaffold(
body: Row(
children: [
NavigationRail(
selectedIndex: _calculateSelectedIndex(context),
onDestinationSelected: ((value) =>
_onItemTapped(value, auth, context)),
labelType: NavigationRailLabelType.all,
destinations: [
const NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
const NavigationRailDestination(
icon: Icon(Icons.account_box),
label: Text('Account'),
),
const NavigationRailDestination(
icon: Icon(Icons.access_alarm),
label: Text('Surveys'),
),
if (auth == false)
NavigationRailDestination(
label: Text('SignIn'), icon: Icon(Icons.accessibility_new)),
if (auth == true)
NavigationRailDestination(
label: Text('SignOut'),
icon: Icon(Icons.add_circle_outline_outlined))
],
),
Expanded(child: child)
],
),
);
}
static int _calculateSelectedIndex(BuildContext context) {
final String location = GoRouterState.of(context).location;
if (location.startsWith('/home')) {
return 0;
}
if (location.startsWith('/account')) {
return 1;
}
if (location.startsWith('/surveys')) {
return 2;
}
if (location.startsWith('/login')) {
return 3;
}
return 0;
}
void _onItemTapped(int index, bool auth, BuildContext context) {
switch (index) {
case 0:
GoRouter.of(context).go('/home');
break;
case 1:
GoRouter.of(context).go('/account');
break;
case 2:
GoRouter.of(context).go('/surveys');
break;
case 3:
if (auth == true) {
FirebaseAuthService.signOut();
} else {
GoRouter.of(context).go('/login');
}
break;
}
}
}
main.dart
void main() async {
runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
final sharedPreferences = await SharedPreferences.getInstance();
runApp(ProviderScope(overrides: [
sharedPreferencesServiceProvider.overrideWithValue(
SharedPreferencesService(sharedPreferences),
),
], child: MyApp()));
},
((error, stack) =>
FirebaseCrashlytics.instance.recordError(error, stack)));
}
class MyApp extends ConsumerWidget {
MyApp({Key? key}) : super(key: key);
// Define an async function to initialize FlutterFire
Future<void> _initializeFlutterFire() async {
// Wait for Firebase to initialize
if (_kTestingCrashlytics) {
// Force enable crashlytics collection enabled if we're testing it.
await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
} else {
// Else only enable it in non-debug builds.
// You could additionally extend this to allow users to opt-in.
await FirebaseCrashlytics.instance
.setCrashlyticsCollectionEnabled(!kDebugMode);
}
// Pass all uncaught errors to Crashlytics.
Function? originalOnError = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails errorDetails) async {
await FirebaseCrashlytics.instance.recordFlutterError(errorDetails);
// Forward to original handler.
originalOnError!(errorDetails);
};
}
@override
Widget build(BuildContext context, WidgetRef ref) {
if (!kIsWeb) _initializeFlutterFire();
return Consumer(builder: (context, ref, child) {
final theme = ref.watch(themeProvider);
final router = ref.watch(goRouterProvider);
return MaterialApp.router(
routerConfig: router,
theme: theme[0],
darkTheme: theme[1],
themeMode: ThemeMode.system,
debugShowCheckedModeBanner: false,
builder: (context, widget) => ResponsiveWrapper.builder(
ClampingScrollWrapper.builder(
context,
widget!,
),
minWidth: 480,
defaultScale: true,
breakpoints: [
ResponsiveBreakpoint.resize(480, name: MOBILE),
ResponsiveBreakpoint.autoScale(800, name: TABLET),
ResponsiveBreakpoint.resize(1000, name: DESKTOP),
],
),
);
});
}
}
登录或注销错误:
断言失败:registry.containskey(page)不为真。
我也有这个错误,在我的情况下,它是抛出的每次热加载和热重启的应用程序
在查看了错误的堆栈跟踪之后,我发现这是由多个小部件使用相同的全局键引起的(这应该永远不会发生,因为键应该唯一地标识元素)。
在你的例子中,导航器的全局键是:
final GlobalKey<NavigatorState> _rootNavigatorKey =
GlobalKey(debugLabel: 'root');
final GlobalKey<NavigatorState> _shellNavigatorKey =
GlobalKey(debugLabel: 'shell');
是全局变量,当Flutter在重新加载时构建新的GoRouter
实例时重用。解决方案是将它们移动到router生成器函数中,如下所示:
final goRouterProvider = Provider<GoRouter>((ref) {
final auth = ref.watch(authenticationListenerProvider);
// MOVE HERE
final GlobalKey<NavigatorState> _rootNavigatorKey =
GlobalKey(debugLabel: 'root');
final GlobalKey<NavigatorState> _shellNavigatorKey =
GlobalKey(debugLabel: 'shell');
return GoRouter(
navigatorKey: _rootNavigatorKey,
// ...
现在将为GoRouter
的新实例生成新的密钥,并且密钥冲突消失了。
同样,如果你想把你的GoRouter
对象存储为一个全局变量,创建一个函数来创建这个对象(用全局键作为函数内部的变量),并从这个函数中创建一个全局变量,就像下面的例子:
final GoRouter router = createRouter();
GoRouter createRouter() {
final rootNavigatorKey = GlobalKey<NavigatorState>();
final shellNavigatorKey = GlobalKey<NavigatorState>();
return GoRouter(
navigatorKey: rootNavigatorKey,
// ...
我找出了什么是错误的-需要使用PageBuilder与Shellroute而不是生成器的NavBar。然后工作得很好。希望这能帮助到其他人。
return GoRouter(
navigatorKey: _rootNavigatorKey,
initialLocation: '/home',
routes: <RouteBase>[
/// Application shell
ShellRoute(
navigatorKey: _shellNavigatorKey,
pageBuilder: (BuildContext context, GoRouterState state, Widget child) {
return NoTransitionPage(child: ScaffoldWithNavBar(child: child));
},
routes:[]