So I am using a riverpod with ChangeNotifier, GoRouter and Hive
So this is my RouteService class with parameter of HiveBox;
class RouteService extends ChangeNotifier {
late final Box box;
bool _initialized = false;
bool _loginState = false;
RouteService({required this.box});
bool get loginState => _loginState;
bool get initialized => _initialized;
set loginState(bool value) {
_loginState = value;
box.put('login', value);
notifyListeners();
}
set initializedState(bool value) {
_initialized = value;
notifyListeners();
}
Future<void> onAppStart() async {
_initialized = box.get('initialized', defaultValue: false);
_loginState = box.get('login', defaultValue: false);
await Future.delayed(const Duration(seconds: 2));
///TODO set a downloading resources here
_initialized = true;
notifyListeners();
}
}
And this is my BlackboltRouter class which contains the GoRouter and the Ref object from flutter riverpod package. (please focus on redirect)
class BlackboltRouter {
GoRouter get router => _goRouter;
BlackboltRouter(this.ref);
late Ref ref;
late final GoRouter _goRouter = GoRouter(
// refreshListenable: routeService,
initialLocation: APP_PAGE.home.toPath,
routes: [
ShellRoute(
builder: (context, state, child) => ResponsiveWrapper.builder(
ClampingScrollWrapper.builder(context, child),
maxWidth: 1200,
minWidth: 480,
breakpoints: const [
ResponsiveBreakpoint.resize(480, name: MOBILE),
ResponsiveBreakpoint.autoScale(600, name: TABLET),
ResponsiveBreakpoint.resize(1000, name: DESKTOP),
ResponsiveBreakpoint.autoScale(2460, name: '4K')
],
),
routes: [
GoRoute(
path: APP_PAGE.home.toPath,
name: APP_PAGE.home.toName,
builder: (_, __) => RootPage(),
),
GoRoute(
path: APP_PAGE.splash.toPath,
name: APP_PAGE.splash.toName,
builder: (context, state) => SplashPage(),
),
GoRoute(
path: APP_PAGE.login.toPath,
name: APP_PAGE.login.toName,
// TODO add login page
builder: (context, state) => SizedBox.shrink(),
),
GoRoute(
path: APP_PAGE.game.toPath,
name: APP_PAGE.game.toName,
builder: (context, state) {
final questGameKey = state.params['questGameKey'] ?? 'unknown';
return GamePage(questGameKey: questGameKey);
},
),
],
),
],
// errorBuilder: (context, state) => ErrorPage(),
redirect: (context, state) {
final routeService = ref.read(routerServiceProvider);
final homeLocation = state.namedLocation(APP_PAGE.home.toName);
final splashLocation = state.namedLocation(APP_PAGE.splash.toName);
final isInitialized = routeService.initialized;
final isGoingToInit = state.subloc == splashLocation;
if (!isInitialized && !isGoingToInit) {
return splashLocation;
} else if (!isInitialized && isGoingToInit) {
return homeLocation;
}
return null;
// return null;
},
);
}
This my MyApp class
const MyApp({
required this.playerProgressPersistence,
required this.settingsPersistence,
required this.adsController,
required this.gamesServicesController,
required this.questGameBox,
required this.utilGameBox,
super.key,
});
#override
Widget build(BuildContext context) {
return AppLifecycleObserver(
child: ProviderScope(
overrides: [
gameCategoryRepositoryProvider.overrideWithValue(
GameCategoryHiveRepository(
HiveDatabaseGameCategoryApi(questGameBox),
),
),
routerServiceProvider.overrideWithValue(
RouteService(box: utilGameBox),
),
],
child: Consumer(builder: (context, ref, _) {
final goRouter = ref.watch(blackboltRouterProvider);
return BlackboltWidget(
title: 'One sound, One word',
routerConfig: goRouter.router,
);
}),
),
);
}
}
Lastly my providers:
final routerServiceProvider =
ChangeNotifierProvider<RouteService>((ref) => throw UnimplementedError());
/// The provider for the BlackboltRouter
final blackboltRouterProvider = Provider<BlackboltRouter>((ref) {
// final routerService = ref.watch(routerServiceProvider);
return BlackboltRouter(ref);
});
As you can see in our riverpod for the routeServiceProvider, I returned a throw UnimplementedError because I need to pass a value for HiveBox so I decide to throw UnimpletedError first. Then in our MyApp class is that I begin to initialized the Hive boxes and I override the value of my routeServiceProvider. So finally, in my Blackbolt Router I subscribe the routeServiceProvide, which is the expected value of routeServiceProvider is not null. But the error occurs there, it gives me a report that
The following ProviderException was thrown building DefaultSelectionStyle:
An exception was thrown while building ChangeNotifierProvider<RouteService>#c5428.
Thrown exception:
An exception was thrown while building _NotifierProvider<RouteService>#d5e71.
Thrown exception:
UnimplementedError
Related
I have a app that was developed with FlutterFlow and I`m implementing Google AdMob in it. I need a page that will always be visible in my app life cycle, this page is holding the AdMob banner.
I have already created the page, my problem is: how can I make this page always visible?
This is where I handle de navigation in my app, I`m using GoRouter:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:page_transition/page_transition.dart';
import '../flutter_flow_theme.dart';
import '../../index.dart';
import '../../main.dart';
import '../lat_lng.dart';
import '../place.dart';
import 'serialization_util.dart';
import 'package:consulta_beneficios/home_tab.dart';
export 'package:go_router/go_router.dart';
export 'serialization_util.dart';
const kTransitionInfoKey = '__transition_info__';
class AppStateNotifier extends ChangeNotifier {
bool showSplashImage = true;
void stopShowingSplashImage() {
showSplashImage = false;
notifyListeners();
}
}
GoRouter createRouter(AppStateNotifier appStateNotifier) => GoRouter(
initialLocation: '/',
debugLogDiagnostics: true,
refreshListenable: appStateNotifier,
errorBuilder: (context, _) => appStateNotifier.showSplashImage
? Builder(
builder: (context) => Container(
color: Color(0xFF12378F),
child: Center(
child: Image.asset(
'assets/images/logo-white.png',
width: 150,
height: 150,
fit: BoxFit.contain,
),
),
),
)
: HomeWidget(),
routes: [
FFRoute(
name: '_initialize',
path: '/',
builder: (context, _) => appStateNotifier.showSplashImage
? Builder(
builder: (context) => Container(
color: Color(0xFF12378F),
child: Center(
child: Image.asset(
'assets/images/logo-white.png',
width: 150,
height: 150,
fit: BoxFit.contain,
),
),
),
)
: HomeWidget(),
routes: [
FFRoute(
name: 'home',
path: 'home',
builder: (context, params) => HomeWidget(),
),
FFRoute(
name: 'search',
path: 'search',
builder: (context, params) => SearchWidget(),
),
FFRoute(
name: 'article',
path: 'article',
builder: (context, params) => ArticleWidget(
data: params.getParam('data', ParamType.JSON),
),
)
].map((r) => r.toRoute(appStateNotifier)).toList(),
).toRoute(appStateNotifier),
],
urlPathStrategy: UrlPathStrategy.path,
);
extension NavParamExtensions on Map<String, String?> {
Map<String, String> get withoutNulls => Map.fromEntries(
entries
.where((e) => e.value != null)
.map((e) => MapEntry(e.key, e.value!)),
);
}
extension _GoRouterStateExtensions on GoRouterState {
Map<String, dynamic> get extraMap =>
extra != null ? extra as Map<String, dynamic> : {};
Map<String, dynamic> get allParams => <String, dynamic>{}
..addAll(params)
..addAll(queryParams)
..addAll(extraMap);
TransitionInfo get transitionInfo => extraMap.containsKey(kTransitionInfoKey)
? extraMap[kTransitionInfoKey] as TransitionInfo
: TransitionInfo.appDefault();
}
class FFParameters {
FFParameters(this.state, [this.asyncParams = const {}]);
final GoRouterState state;
final Map<String, Future<dynamic> Function(String)> asyncParams;
Map<String, dynamic> futureParamValues = {};
// Parameters are empty if the params map is empty or if the only parameter
// present is the special extra parameter reserved for the transition info.
bool get isEmpty =>
state.allParams.isEmpty ||
(state.extraMap.length == 1 &&
state.extraMap.containsKey(kTransitionInfoKey));
bool isAsyncParam(MapEntry<String, dynamic> param) =>
asyncParams.containsKey(param.key) && param.value is String;
bool get hasFutures => state.allParams.entries.any(isAsyncParam);
Future<bool> completeFutures() => Future.wait(
state.allParams.entries.where(isAsyncParam).map(
(param) async {
final doc = await asyncParams[param.key]!(param.value)
.onError((_, __) => null);
if (doc != null) {
futureParamValues[param.key] = doc;
return true;
}
return false;
},
),
).onError((_, __) => [false]).then((v) => v.every((e) => e));
dynamic getParam<T>(
String paramName,
ParamType type, [
bool isList = false,
]) {
if (futureParamValues.containsKey(paramName)) {
return futureParamValues[paramName];
}
if (!state.allParams.containsKey(paramName)) {
return null;
}
final param = state.allParams[paramName];
// Got parameter from `extras`, so just directly return it.
if (param is! String) {
return param;
}
// Return serialized value.
return deserializeParam<T>(
param,
type,
isList,
);
}
}
class FFRoute {
const FFRoute({
required this.name,
required this.path,
required this.builder,
this.requireAuth = false,
this.asyncParams = const {},
this.routes = const [],
});
final String name;
final String path;
final bool requireAuth;
final Map<String, Future<dynamic> Function(String)> asyncParams;
final Widget Function(BuildContext, FFParameters) builder;
final List<GoRoute> routes;
GoRoute toRoute(AppStateNotifier appStateNotifier) => GoRoute(
name: name,
path: path,
pageBuilder: (context, state) {
final ffParams = FFParameters(state, asyncParams);
final page = ffParams.hasFutures
? FutureBuilder(
future: ffParams.completeFutures(),
builder: (context, _) => builder(context, ffParams),
)
: builder(context, ffParams);
final child = page;
final transitionInfo = state.transitionInfo;
return transitionInfo.hasTransition
? CustomTransitionPage(
key: state.pageKey,
child: child,
transitionDuration: transitionInfo.duration,
transitionsBuilder: PageTransition(
type: transitionInfo.transitionType,
duration: transitionInfo.duration,
reverseDuration: transitionInfo.duration,
alignment: transitionInfo.alignment,
child: child,
).transitionsBuilder,
)
: MaterialPage(key: state.pageKey, child: child);
},
routes: routes,
);
}
class TransitionInfo {
const TransitionInfo({
required this.hasTransition,
this.transitionType = PageTransitionType.fade,
this.duration = const Duration(milliseconds: 300),
this.alignment,
});
final bool hasTransition;
final PageTransitionType transitionType;
final Duration duration;
final Alignment? alignment;
static TransitionInfo appDefault() => TransitionInfo(hasTransition: false);
}
This is my MateriaApp widget:
#override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'consulta-beneficios',
localizationsDelegates: [
FFLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
locale: _locale,
supportedLocales: const [Locale('en', '')],
theme: ThemeData(brightness: Brightness.light),
darkTheme: ThemeData(brightness: Brightness.dark),
themeMode: _themeMode,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
);
}
If I use MaterialApp I can solve this issue with the home property. But I dont know how to do it with MaterialApp.router`.
I'm using a basic GoRouter with shellroute setup with a side navbar that is meant to remain consistent across pages. Both a login or logout call to Firebase will generate the assertion, but I don't understand why? Any pointers would be appreciated. Code below:
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
top-level providers:
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),
],
),
);
});
}
}
Error on login or logout:
Assertion failed: registry.containskey(page) is not true.
I worked out what was wrong - need to use PageBuilder with Shellroute rather than builder for NavBar. Then worked fine. Hope this helps someone else.
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:[]
I've also had this error, in my case it was thrown on every hot reload and hot restart of the app.
After looking into the stack trace of the error I found out that it's caused by multiple widgets using the same global key (which should never happen, as keys are supposed to uniquely identify elements).
In your case, the global keys for navigators:
final GlobalKey<NavigatorState> _rootNavigatorKey =
GlobalKey(debugLabel: 'root');
final GlobalKey<NavigatorState> _shellNavigatorKey =
GlobalKey(debugLabel: 'shell');
are global variables and are reused when Flutter builds new instances of GoRouter on reload. The solution is to move them inside the router generator function, as here:
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,
// ...
Now new keys will be generated for new instances of GoRouter and the key conflict is gone.
Also, if you want to store your GoRouter object as a global variable, create a function that creates the object (with the global keys as variables inside the function) and create a global variable from this function, like in this example:
final GoRouter router = createRouter();
GoRouter createRouter() {
final rootNavigatorKey = GlobalKey<NavigatorState>();
final shellNavigatorKey = GlobalKey<NavigatorState>();
return GoRouter(
navigatorKey: rootNavigatorKey,
// ...
Anyone used Streambuilder with Flutter and GoRouter before to persist User state?
Currently, I'm using the default Flutter navigation and it's working fine (see below) but I'm trying to replace with GoRouter now
My previous navigation setup using default Flutter navigation
home: StreamBuilder(
stream: AuthMethods().authChanges,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasData) {
return const TabsScreen();
}
return const AuthScreen();
}),
);
}
My current goRouter code. I'm wondering where I can use Streambuilder in d code below to persist user state for my app (or whether Streambuilder can be used at all with go router)
GoRouter routeConstruct() {
return GoRouter(
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
return const PublicScreen();
},
routes: <RouteBase>[
GoRoute(
path: Events_Screen,
builder: (BuildContext context, GoRouterState state) {
return const EventsScreen();
},
),
GoRoute(
path: "$EventDetail_Screen/:eventId",
builder: (BuildContext context, GoRouterState state) {
return EventDetailScreen(eventId: state.params['eventId']!);
},
),
...
]),
],
);
Can I use Streambuilder with Flutter and GoRouter to persist User authentication state? If can, where in the goRouter code can I use Streambuilder to determine which widget to show based on Auth state? Thanks
###########################
class AuthMethods
class AuthMethods {
final FirebaseAuth _auth = FirebaseAuth.instance;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
Stream<User?> get authChanges => _auth.authStateChanges();
// --> use of bang ! operator here
User get user => _auth.currentUser!;
Future<bool> signInWithGoogle(BuildContext context) async {
bool res = false;
try {
final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn();
final GoogleSignInAuthentication? googleAuth =
await googleUser?.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth?.accessToken, idToken: googleAuth?.idToken);
UserCredential userCredential =
await _auth.signInWithCredential(credential);
User? user = userCredential.user;
if (user != null) {
if (userCredential.additionalUserInfo!.isNewUser) {
await _firestore.collection('users').doc(user.uid).set({
'username': user.displayName,
'uid': user.uid,
'profilePhoto': user.photoURL,
});
}
res = true;
}
} on FirebaseAuthException catch (e) {
showSnackBar(context, e.message!);
res = false;
}
return res;
}
class RouteNotifier
class RouterNotifier extends ChangeNotifier {
RouterNotifier(this._authMethods) {
_authMethods.authChanges.listen((event) {
notifyListeners();
});
}
final AuthMethods _authMethods;
String? redirect(BuildContext context, GoRouterState state) {
final onLoginPage = state.location == '/$AuthScreen';
final onHomePage = state.location == '/$Tabs_Screen';
// --> The getter 'isEmpty' isn't defined for the type 'User'.
// --> User get user => _auth.currentUser!; used a bang operator
if (_authMethods.user.isEmpty && onHomePage) {
return '/$AuthScreen';
}
// --> The getter 'isEmpty' isn't defined for the type 'User'.
if (_authMethods.currentUser.isNotEmpty && onLoginPage) {
return '/$Tabs_Screen';
}
return null;
}
}
router.dart
GoRouter routeConstruct() {
return GoRouter(
refreshListenable: router,
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
return const PublicScreen();
},
routes: <RouteBase>[
GoRoute(
path: Events_Screen,
builder: (BuildContext context, GoRouterState state) {
return const EventsScreen();
},
),
GoRoute(
path: "$EventDetail_Screen/:eventId",
builder: (BuildContext context, GoRouterState state) {
return EventDetailScreen(eventId: state.params['eventId']!);
},
),
]),
],
);
}
before go router 5.0.0 :
go router has class called GoRouterRefreshStream, so in go router
GoRouter (
....
refreshListenable: GoRouterRefreshStream(AuthMethods().authChanges),
but after go router 5.0.0, they remove it
so you must create your own notifier :
class RouterNotifier extends ChangeNotifier {
RouterNotifier(this._authMethods) {
_authMethods.authChanges.listen((event) {
notifyListeners();
});
}
final AuthMethods _authMethods;
}
on go Router:
GoRouter(
debugLogDiagnostics: false,
....
...
refreshListenable: RouterNotifier(AuthMethods()),
...
);
and how go router can work like stream builder you use before?
redirect come in handy:
final router = RouterNotifier(AuthMethods());
GoRouter (
...
refreshListenable: router,
redirect: router.redirect,
in RouterNotifier class :
class RouterNotifier extends ChangeNotifier {
RouterNotifier(this._authMethods) {
_authMethods.authChanges.listen((event) {
notifyListeners();
});
}
final AuthMethods _authMethods;
String? redirect(BuildContext context, GoRouterState state) {
final onLoginPage = state.location == '/login';
final onHomePage = state.location == '/';
if (_authMethods.currentUser.isEmpty && onHomePage) {
return '/login';
}
if (_authMethods.currentUser.isNotEmpty && onLoginPage) {
return '/';
}
return null;
}
}
class User model :
class User {
String id;
.........
some variable i need from db;
static const empty = User(id: '');
bool get isEmpty => this == User.empty;
bool get isNotEmpty => this != User.empty;
}
on Stream :
Stream<User> get user {
// map the user if user null is mean empty
return _firebaseAuth.authStateChanges().asyncMap((fUser) async {
if (fUser == null) {
const User user = User.empty;
return user;
} else {
// in my case i need to take some data from Firestore
....
await datafromDb() // if you dont need to wait future, remove async map, just use normal map
return fUser.withBiodata();
Description
I am trying to Navigate between screens with a Navigation Menu using go_router plugin. When I click the item in the menu, nothing happens but if I change the URL the screen does change.
Video shows the problem
Expect
Every time I navigate back and forth, both URL and screen change
My Code
app_router.dart
class AppRouter {
AppRouter(this._appBloc);
final AppBloc _appBloc;
GoRouter get router => GoRouter(
routes: pages.values.toList(growable: false),
errorBuilder: (context, state) => ErrorPage(
key: state.pageKey,
),
refreshListenable: GoRouterRefreshStream(_appBloc.stream),
navigatorBuilder: _buildRouterView,
redirect: _redirect,
);
String? _redirect(GoRouterState state) {
final loggedIn = _appBloc.state.status == AppStatus.authenticated;
final name = state.subloc;
final loggingIn = name == '/login' || name == '/';
if (!loggedIn) return loggingIn ? null : '/login';
if (loggingIn) return '/app';
return null;
}
static Map<String, GoRoute> pages = {
route_names.onboard: GoRoute(
name: route_names.onboard,
path: routes[route_names.onboard]!,
pageBuilder: (context, state) => OnboardPage.page(key: state.pageKey),
routes: [
GoRoute(
path: route_names.login.subRoutePath,
name: route_names.login,
pageBuilder: (context, state) => LoginPage.page(key: state.pageKey),
),
GoRoute(
path: route_names.signUp.subRoutePath,
name: route_names.signUp,
pageBuilder: (context, state) => LoginPage.page(key: state.pageKey),
),
],
),
'app': GoRoute(
path: '/app',
// All /app pages get the main scaffold
builder: (context, state) {
return Text("App Main");
},
routes: [
ExplorePage.route,
PlanPage.route,
AccountPage.route,
]),
};
Widget _buildRouterView(BuildContext context, GoRouterState state, Widget child) {
return Builder(
builder: (context) => BlocBuilder<AppBloc, AppState>(builder: (context, appState) {
if (appState.status == AppStatus.unauthenticated) {
return child;
}
return HomePageSkeleton(
child: child,
);
}),
);
}
}
app.dart
class AppView extends StatelessWidget {
// ignore: prefer_const_constructors_in_immutables
AppView({super.key, required AppBloc appBloc}) {
_appBloc = appBloc;
_appRouter = AppRouter(_appBloc);
}
late final AppBloc _appBloc;
late final AppRouter _appRouter;
#override
Widget build(BuildContext context) {
return BlocListener<AppBloc, AppState>(
listener: (context, state) {
if (state == const AppState.unauthenticated()) {
_appRouter.router.goNamed(route_names.login);
}
},
child: MaterialApp.router(
supportedLocales: AppLocalizations.supportedLocales,
routeInformationParser: _appRouter.router.routeInformationParser,
routeInformationProvider: _appRouter.router.routeInformationProvider,
routerDelegate: _appRouter.router.routerDelegate,
),
);
}
}
HomePageSkeleton.class
// inside build method
class HomePageSkeleton extends StatelessWidget with NavigationMixin {
const HomePageSkeleton({Key? key,required this.child}) : super(key: key);
final Widget child;
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: Responsive.isMobile(context) ? AppBottomNavigation(index: 0) : const SizedBox(),
body: SafeArea(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (Responsive.isTablet(context) || Responsive.isLaptop(context))
// It takes 1/6 part of the screen
Expanded(
child: SideMenu(index: 0, onSelected: (index) => onTabSelected(index, context)),
),
Expanded(
// It takes 5/6 part of the screen
flex: 5,
child: child),
],
),
),
);
}
}
//onTapSelected method
void onTabSelected(int index, BuildContext context) {
switch (index) {
case 0:
// context.goNamed(route_names.explore);
context.go('/app/explore');
break;
case 1:
// context.goNamed(route_names.plan);
context.go('/app/plan');
break;
case 2:
// context.goNamed(route_names.account);
context.go('/app/account');
break;
default:
throw Exception('Unknown view');
}
}
I change my class AppRouter into:
class AppRouter {
AppRouter(AppBloc appBloc)
: _router = GoRouter(
routes: getPages().values.toList(growable: false),
errorBuilder: (context, state) => ErrorPage(
key: state.pageKey,
),
refreshListenable: GoRouterRefreshStream(appBloc.stream),
navigatorBuilder: _buildRouterView,
redirect: (GoRouterState state) {
_redirect(state, appBloc);
},
),
_appBloc = appBloc;
final AppBloc _appBloc;
final GoRouter _router;
GoRouter get router {
return _router;
}
... and it worked
according to this official video from author of go_router package :
Flutter Navigator 2.0 made easy with go_router
and this Flutter Navigator 2.0: Using go_router complete guide , I implemnted go_router in my app. However, I'm having a strange problem. Everything works properly the first time I run my app. I can fetch user information from the server and send them to the home page. However, I'm getting an issue regarding fetching data after restarting my app. Would you please tell me why I'm receiving this error?
this my main.dart :
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final sharedPrefs = await SharedPreferences.getInstance();
runApp(MyApp(sharedPrefs));
}
class MyApp extends StatefulWidget {
final SharedPreferences sharedPreferences;
MyApp(this.sharedPreferences);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late LoginState loginState;
late Auth auth;
#override
void initState() {
loginState = LoginState(widget.sharedPreferences);
auth = Auth(widget.sharedPreferences);
super.initState();
}
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (ctx) => auth,
),
ChangeNotifierProvider(
create: (ctx) => loginState,
),
ChangeNotifierProvider(
create: (ctx) => AppRouter(loginState),
),
ListenableProxyProvider<Auth, Orders>(
update: (_, authObj, prevOrders) =>
Orders(authObj.usrName, authObj.objId),
),
ListenableProxyProvider<Auth, FavMeals>(
update: (_, authObj, prevOrders) =>
FavMeals(authObj.usrName, authObj.objId),
],
child: Sizer(builder: (context, orientation, deviceType) {
final _router = Provider.of<AppRouter>(context, listen: false).router;
return MaterialApp.router(
routerDelegate: _router.routerDelegate,
routeInformationParser: _router.routeInformationParser,
scaffoldMessengerKey: scaffoldMessengerKey,
theme: ThemeData(
primarySwatch: myMaterialColor),
localizationsDelegates: [
GlobalCupertinoLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
);
}));
}
}
and here is my router.dart file :
class AppRouter with ChangeNotifier {
final LoginState authState;
AppRouter(this.authState);
late final router = GoRouter(
redirect: (state) {
final loginLoc = state.namedLocation(authScreen);
// 2
final loggingIn = state.subloc == loginLoc;
// 3
// 4
final loggedIn = authState.loggedIn;
final rootLoc = state.namedLocation(root);
// 5
if (!loggedIn && !loggingIn) return loginLoc;
if (loggedIn && (loggingIn)) return rootLoc;
return null;
},
refreshListenable: authState,
routes: [
GoRoute(
name: root,
path: '/',
builder: (context, state) => TabsScreen(),
),
GoRoute(
name: mainScreen,
path: '/main-screen',
builder: (context, state) => HomeScreen(),
),
GoRoute(
name: splashScreen,
path: '/splash-screen',
builder: (context, state) => SplashScreen(),
)
GoRoute(
name: authScreen,
path: '/auth-screen',
builder: (context, state) => AuthScreen(),
),
GoRoute(
name: orderSreen,
path: '/order-screen',
builder: (context, state) => OrderScreen(),
),
],
errorBuilder: (context, state) => Scaffold(
body: Center(
child: Text(state.error.toString()),
),
),
);
}
there is a method in my app within my '/' (root) screen that trying to fetch user data and then load the screen, this method doesnt work properly any more after I migrate to go_router :
#override
void initState() {
_selectedIndex = 0;
_pageController = PageController(initialPage: _selectedIndex!);
initUserStatus().then((_) {
setState(() {
_isloading = false;
});
}, onError: (error) {
GoRouter.of(context).goNamed(noNetwork);
print(error.toString());
});
super.initState();
}
Future<void> initUserStatus() async {
final userProvider = Provider.of<Users>(context, listen: false);
await userProvider.userRetrieving();
await Provider.of<EatenMeals>(context, listen: false).serverList();
var user = userProvider.initialUser;
dailyLimitAlert(user);
if (user.isPremium) {
var sub = await userProvider.subRemainDays();
if (sub < 1) {
await userProvider.changePremiumStatus();
}
} else {
print(user.isPremium);
}
}