I get this error when I open the App.
when I am trying to initialize the router from authStore get this error
"You are trying to use contextless navigation without
a GetMaterialApp or Get.key.
If you are testing your app, you can use:
[Get.testMode = true], or if you are running your app on
a physical device or emulator, you must exchange your [MaterialApp]
for a [GetMaterialApp].
"
main.dart
#override
Widget build(BuildContext context) {
return GetMaterialApp(
navigatorKey: Get.key,
smartManagement: SmartManagement.full,
initialBinding: InitialBinding(),
debugShowCheckedModeBanner: false,
title: 'Ponte Delivery',
initialRoute: Routers.home,
getPages: Pages.getPages,
theme: Themes.light,
);
}
class InitialBinding implements Bindings {
#override
void dependencies() {
Get.lazyPut<LocalStorage>(() => LocalStorageImpl());
Get.put<HttpClient>(HttpClientImpl(Dio()), permanent: true);
Get.put<AuthStore>(AuthStore(Get.find<LocalStorage>()), permanent: true);
}
}
AuthStore
#override
void onInit() {
ever(isLogged, fireRoute);
super.onInit();
token = _storage.read(TOKEN);
user = _storage.read(USER, construct: (map) => UserModel.fromMap(map));
if (token == null && user == null) {
isLogged.value = false;
}
}
fireRoute(logged) {
print(logged);
if (!logged) {
Get.offNamed(Routers.initialRoute);
}
}
I am using already GetMaterialApp.
I tried to use debugger and I got error in this Line
Get.offNamed(Routers.initialRoute);
Related
I am trying to set the theme of my app on the response of login data after getting the role but my theme is not updating as per expectation. this is how my main() looks. my code is showing no error and I tried to debug nothing seems wrong.
Widget build(BuildContext context) {
return ChangeNotifierProvider<ThemeModel>(
create: (_) => ThemeModel(),
child: Consumer<ThemeModel>(
builder: (context, ThemeModel themeNotifier, child) {
return Sizer(builder: (context, orientation, deviceType) {
return MaterialApp(
theme: themeNotifier.theme == 'consultant'
? counsultantApptheme()
: themeNotifier.theme == 'rmo'
? rmoApptheme()
: counsultantApptheme(),
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,
initialRoute: startroute.toString(),
routes: routes,
);
});
}));
and this how I am updating after response of login API
if (snapshot.data!.data!.consultantYN == 'Y') {
Provider.of<ThemeModel>(context, listen: false).theme =
'consultant';
} else {
Provider.of<ThemeModel>(context, listen: false).theme = 'rmo';
}
and this is my function where I am setting theme and calling notifyListeners() in class extends by ChangeNotifier
//theme_model.dart
import 'package:flutter/material.dart';
import 'package:nmc/widgets/theme_config/theme_preference.dart';
class ThemeModel extends ChangeNotifier {
late String _theme;
late ThemePreferences _preferences;
String get theme => _theme;
ThemeModel() {
_theme = 'default';
_preferences = ThemePreferences();
getPreferences();
}
//Switching themes in the flutter apps - Flutterant
set theme(String value) {
_theme = value;
_preferences.setTheme(value);
notifyListeners();
}
getPreferences() async {
_theme = await _preferences.getTheme();
notifyListeners();
}
}
My Flutter project is migrating to go_router and I have trouble understanding how to access riverpod providers in either a GoRoute's build method or redirect method, as I need to access the user's data to control the navigation.
I have a top-level redirect that checks if a user is logged in and sends them to a LoginPage if not. All users can access so-called activities in my app, but only admins can edit them. Whether a user is an admin is stored in a riverpod userDataProvider, which always contains a user if the user is logged in. Now if a user attempts to enter the route /:activityId?edit=true, I want to check whether they are allowed to by accessing the userDataProvider. However, I do not see what the clean way of accessing this provider is.
I found somewhere (can't find the thread anymore), that one way is to use ProviderScope.containerOf(context).read(userDataProvider), but I have never seen this before and it seems a bit exotic to me. Is this the way to go?
My GoRoute looks something like this:
GoRoute(
path: RouteName.event.relPath,
builder: (context, state) {
final String? id = state.params['id'];
final bool edit = state.queryParams['edit'] == 'true';
if (state.extra == null) {
// TODO: Fetch data
}
final data = state.extra! as Pair<ActivityData, CachedNetworkImage?>;
if (edit) {
return CreateActivity(
isEdit: true,
data: data.a,
banner: data.b,
);
}
return ActivityPage(
id: id!,
data: data.a,
banner: data.b,
);
},
redirect: (context, state) {
final bool edit = state.queryParams['edit'] == 'true';
if (edit) {
// IMPORTANT: How to access the ref here?
final bool isAdmin =
ref.read(userDataProvider).currentUser.customClaims.admin;
if (isAdmin) {
return state.location; // Includes the queryParam edit
} else {
return state.subloc; // Does not include queryParam
}
} else {
return state.path;
}
},
),
In my current application, I used something similar approach like this :
Provider registration part (providers.dart) :
final routerProvider = Provider<GoRouter>((ref) {
final router = RouterNotifier(ref);
return GoRouter(
debugLogDiagnostics: true,
refreshListenable: router,
redirect: (context, state) {
router._redirectLogic(state);
return null;
},
routes: ref.read(routesProvider));
});
class RouterNotifier extends ChangeNotifier {
final Ref _ref;
RouterNotifier(this._ref) {
_ref.listen<AuthState>(authNotifierProvider, (_, __) => notifyListeners());
}
String? _redirectLogic(GoRouterState state) {
final loginState = _ref.watch(authNotifierProvider);
final areWeLoggingIn = state.location == '/login';
if (loginState.state != AuthenticationState.authenticated) {
return areWeLoggingIn ? null : '/login';
}
if (areWeLoggingIn) return '/welcome';
return null;
}
}
Main app building as router (app.dart):
class App extends ConsumerWidget {
const App({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context, WidgetRef ref) {
final GoRouter router = ref.watch(routerProvider);
return MaterialApp.router(
routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
debugShowCheckedModeBanner: false,
title: 'Flutter Auth',
}
}
}
And as entrypoint (main.dart):
Future<void> main() async {
F.appFlavor = Flavor.dev;
WidgetsFlutterBinding.ensureInitialized();
await setup();
runApp(ProviderScope(
observers: [
Observers(),
],
child: const App(),
));
}
I am trying to use firebase data to route different pages using Getx. First I have a splash screen and want to automatically go to different pages according to conditions. If the user has already login, it will redirect the Home page, if not the route to the login page. But I can't use initState() on the Stateless widget as I using Getx, I don't want a Stateful widget.
class SplashPage extends StatelessWidget {
RxBool isloading = true.obs;
#override
Widget build(BuildContext context) {
String Uid = "";
return isloading.value
? SpinKitThreeInOut(
color: Colors.red,
)
: Obx(() {
return Get.find<AuthController>().user != null
? homeMethod()
: login();
});
}
Widget homeMethod() {
return Home(AuthController.instance.user.toString());
isloading.value = false;
}
}
But I ain't able to override isloading.value = false;
My Getx Auth Controller:
class AuthController extends GetxController {
static AuthController instance = Get.find();
FirebaseAuth auth = FirebaseAuth.instance;
Rxn<User> _firebaseUser = Rxn<User>();
String? get user => _firebaseUser.value?.uid;
#override
void onReady() {
// TODO: implement onReady
super.onReady();
_firebaseUser.value = auth.currentUser;
_firebaseUser.bindStream(auth.userChanges());
ever(_firebaseUser, _initialScreen);
}
/* #override
void onInit() {
_firebaseUser.bindStream(_auth.authStateChanges());
}*/
_initialScreen(User? user) {
if (user == null) {
Get.offAll(login());
} else {
String userId = user.uid;
Get.offAll(Home(userId));
}
}
Future<User?> LogInAccounts(String Email, String Password) async {
FirebaseAuth auth = FirebaseAuth.instance;
try {
User? user = (await auth.signInWithEmailAndPassword(
email: Email, password: Password))
.user;
if (user != null) {
Fluttertoast.showToast(msg: "Account Create Sucessfully");
return user;
} else {
Fluttertoast.showToast(msg: "Account Create Failed!");
return user;
}
} catch (e) {
return null;
}
}
}
Updated Answer
You can use bindStream and do it that way, but instead of trying to turn your User object into a stream this can be done with a simple RxBool. Firebase already provides a function to listen to auth state changes.
class AuthController extends GetxController {
RxBool loggedIn = false.obs;
#override
void onInit() {
super.onInit();
_subscribe();
}
void _subscribe() {
FirebaseAuth.instance.authStateChanges().listen((User? user) {
if (user == null) {
loggedIn(false);
log('User is currently signed out');
} else {
loggedIn(true);
log('User is signed in');
}
});
}
}
Then you can add another couple methods to your GetX class.
void initNaviationListener() {
/// inital startup naviation
_navigateBasedOnLogin();
/// future navigation based on auth state changes
ever(loggedIn, (value) {
_navigateBasedOnLogin();
});
}
void _navigateBasedOnLogin() {
if (loggedIn.value == false) {
Get.offAndToNamed(LoginPage.id);
} else {
Get.offAndToNamed(HomePage.id);
}
}
Then you can call initNaviationListener in the onReady of GetMaterialApp
GetMaterialApp(
/// onReady is called after GetMaterialApp is fully initialized
onReady: () => Get.find<AuthController>().initNaviationListener(),
theme: ThemeData.dark(),
initialRoute: LoginPage.id,
getPages: [
GetPage(
name: SplashPage.id,
page: () => SplashPage(),
),
GetPage(
name: HomePage.id,
page: () => HomePage(),
),
GetPage(
name: LoginPage.id,
page: () => LoginPage(),
),
],
)
That will navigate on app start to the corresponding screen and also respond to any future changes in auth status.
Original Answer
You don't have to navigate from the SplashPage you can do it from the controller.
Let's say your GetMaterialApp looks like this. This takes you to SplashPage first.
GetMaterialApp(
initialRoute: SplashPage.id,
getPages: [
GetPage(
name: SplashPage.id,
page: () => SplashPage(),
),
GetPage(
name: HomePage.id,
page: () => HomePage(),
),
GetPage(
name: LoginPage.id,
page: () => LoginPage(),
),
],
)
Then check logged in status and navigate to the corresponding screen from your AuthController.
class AuthController extends GetxController {
#override
void onInit() {
super.onInit();
_navigateBasedOnLogin();
}
Future<void> _navigateBasedOnLogin() async {
final loggedIn = await _isLoggedIn();
if (loggedIn) {
Get.offAndToNamed(HomePage.id); // offAndToNamed will remove the SplashScreen from the navigation stack
} else {
Get.offAndToNamed(LoginPage.id);
}
}
Future<bool> _isLoggedIn() async {
/// run your code to check logged in status and return true or false
}
}
Then just init the AuthController in your main.
void main() async {
Get.put(AuthController());
runApp(MyApp());
}
With this setup, your SplashScreen can be a generic loading screen with zero logic.
You can handle initialRoute of GetMaterialApp using isLogin flag
class _MyAppState extends State<MyApp> {
bool isLogin = false;
#override
void initState() {
isLogin = isAlreadyLogin();// Your function to check is user logged in.
super.initState();
}
#override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
title: 'Rider App',
translationsKeys: AppTranslation.translationsKeys,
locale: Get.find<CacheManager>().getLocale(),
getPages: AppPages.pages,
initialRoute: isLogin ? Routes.homeScreen : Routes.loginScreen,
initialBinding: InitialBinding(),
);
}
class Routes {
static const homeScreen = '/home-screen';
static const loginScreen = '/login-screen';
}
class AuthController extends GetxController {
late Rx<User?> firebaseUser;
#override
void onReady() async {
super.onReady();
firebaseUser = Rx<User?>(FirebaseAuth.instance.currentUser);
firebaseUser.bindStream(firebaseAuth.instance.userChanges());
ever(firebaseUser, _setInitialScreen);
}
_setInitialScreen(user) async{
if (user != null) {
Get.offAllNamed(Routes.home);
} else {
Get.offAllNamed(Routes.login);
}
}
}
All examples that I've found are using "navigatorObservers" from the MaterialApp constructor
static FirebaseAnalytics analytics = FirebaseAnalytics.instance;
static FirebaseAnalyticsObserver observer =
FirebaseAnalyticsObserver(analytics: analytics);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Firebase Analytics Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
navigatorObservers: <NavigatorObserver>[observer],
home: MyHomePage(
title: 'Firebase Analytics Demo',
analytics: analytics,
observer: observer,
),
);
}
but my app uses MatterialApp.router from the Navigator 2.0 pattern and could not find an equivalent for attaching an navigatorObserver in order to track screen change events for firebase analytics. Any workarounds or suggestions on this?
The MaterialApp.router constructor has required routerDelegate property. This delegate is usually a wrapper of the Navigator widget. This widget has observers property - that is exactly what you are looking for.
Here is an example of the RouterDelegate, which registers both Firebase and Segment observers:
class AppNavigator extends RouterDelegate<void>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<void> {
AppNavigator({
#required Page<void> initialPage,
this.analyticsObserver,
this.segmentObserver
}) : assert(initialPage != null),
navigatorKey = GlobalKey<NavigatorState>() {
_pagesStack = [initialPage];
}
final FirebaseAnalyticsObserver analyticsObserver;
final SegmentObserver segmentObserver;
...
#override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [..._pagesStack],
observers: [analyticsObserver, segmentObserver],
onPopPage: (route, dynamic result) {
if (!route.didPop(result)) {
return false;
}
for (final page in _pagesStack) {
if (page == route.settings) {
_pagesStack.remove(page);
notifyListeners();
break;
}
}
return true;
},
);
}
}
Note, that under the hood by default the Firebase Analytics module expects your page routes to have a name property set as a part of RouteSettings:
// From FirebaseAnalyticsObserver
void _sendScreenView(PageRoute<dynamic> route) {
final String? screenName = nameExtractor(route.settings);
if (screenName != null) {
analytics.setCurrentScreen(screenName: screenName).catchError(
(Object error) {
final _onError = this._onError;
if (_onError == null) {
debugPrint('$FirebaseAnalyticsObserver: $error');
} else {
_onError(error as PlatformException);
}
},
test: (Object error) => error is PlatformException,
);
}
}
You can override this behavior by providing custom nameExtractor property to the FirebaseAnalyticsObserver constructor.
I am implementing remember me option on the login screen, want to call shared preference before the widget is created. we have the one and only entry point that is the main function, but how we can call a function here to read primitive data (email/password).
void main() {
setupLocator();
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
theme: new ThemeData(fontFamily: 'OpenSans-Light'),
initialRoute: "/",
onGenerateRoute: Router.generateRoute,
));
}
reading bool value
Future<bool> read(String key) async {
final prefs = await SharedPreferences.getInstance();
return prefs.getbool(key);
}
I also try to run a asyn function before route
String firstNav;
void main() {
setupLocator();
readSharedPref();
if(firstNav!=null)
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
theme: new ThemeData(fontFamily: 'OpenSans-Light'),
initialRoute: firstNav,
onGenerateRoute: Router.generateRoute,
));
}
void readSharedPref() async {
Utiles df=Utiles();
String isRem=await df.read("remember");
if (isRem.contains("true")) {
firstNav='homeview';
} else {
firstNav='/';
}
}
You need to set your main function as async, and add an await and a line of code:
void main() async{
//Add this lines is necessary now that your main is async
WidgetsFlutterBinding.ensureInitialized();
//Now you have to "await" the readSharedPref() function
await readSharedPref();
// And here comes all your code
}
Instead of waiting waiting for sharedPreference to load before building any widgets, just show a loader widget with progress indicator until the shared preference is loaded, and when it's loaded, show the required view based on the value loaded from sharedPreference, here is how you can modify your code, (replace HomeView and RootView widgets with your respective widgets for your homeView and / routes)
void main() {
setupLocator();
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
theme: new ThemeData(fontFamily: 'OpenSans-Light'),
initialRoute: Loader(),
onGenerateRoute: Router.generateRoute,
));
}
class Loader extends StatefulWidget {
#override
_LoaderState createState() => _LoaderState();
}
class _LoaderState extends State<Loader> {
Widget firstNav;
#override
void initState() {
super.initState();
readSharedPref();
}
void readSharedPref() async {
Utiles df=Utiles();
String isRem=await df.read("remember");
if (isRem.contains("true")) {
setState(() {
// firstNav='homeview';
firstNav=HomeView(); // replace HomeView with the widget you use for homeview route
});
} else {
setState(() {
// firstNav='/';
firstNav=RootView(); // replace RootView with the widget you use for / route
});
}
}
#override
Widget build(BuildContext context) {
return firstNav != null ? firstNav : Center(child: CircularProgressIndicator(),);
}
}