Navigation in flutter without context - flutter

I created a service folder and made a file in it called request. dart, here I intend to place all requests I make into a class called AuthService, with the login request below I want to be able to navigate to the home screen once response.statusCode == 200 or 201 but I am unable to do that because navigation requires a context and my class is neither a Stateful nor Stateless widget, is there any way I can navigate without the context??
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/material.dart';
class AuthService {
login(email, password) async {
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
if (email == "" && password == "") {
return;
}
try {
Map data = {'email': email, 'password': password};
var jsonResponse;
var response = await http
.post('https://imyLink.com/authenticate', body: data);
if (response.statusCode == 200 || response.statusCode == 201) {
//I want to navigate to my home screen once the request made is successful
jsonResponse = json.decode(response.body);
if (jsonResponse != null) {
await sharedPreferences.setString("userToken", jsonResponse["token"]);
var token = sharedPreferences.getString("userToken");
print('Token: $token');
print(jsonResponse);
print("Login successful");
}
} else {
print(response.statusCode);
print('Login Unsuccessful');
print(response.body);
}
} catch (e) {
print(e);
}
}

First, create a class
import 'package:flutter/material.dart';
class NavigationService{
GlobalKey<NavigatorState> navigationKey;
static NavigationService instance = NavigationService();
NavigationService(){
navigationKey = GlobalKey<NavigatorState>();
}
Future<dynamic> navigateToReplacement(String _rn){
return navigationKey.currentState.pushReplacementNamed(_rn);
}
Future<dynamic> navigateTo(String _rn){
return navigationKey.currentState.pushNamed(_rn);
}
Future<dynamic> navigateToRoute(MaterialPageRoute _rn){
return navigationKey.currentState.push(_rn);
}
goback(){
return navigationKey.currentState.pop();
}
}
In your main.dart file.
MaterialApp(
navigatorKey: NavigationService.instance.navigationKey,
initialRoute: "login",
routes: {
"login":(BuildContext context) =>Login(),
"register":(BuildContext context) =>Register(),
"home":(BuildContext context) => Home(),
},
);
Then you can call the function from anywhere in your project like...
NavigationService.instance.navigateToReplacement("home");
NavigationService.instance.navigateTo("home");

OPTION 1
If you will be calling the login method in either a Stateful or Stateless widget. You can pass context as a parameter to the login method of your AuthService class.
I added a demo using your code as an example:
class AuthService {
// pass context as a parameter
login(email, password, context) async {
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
if (email == "" && password == "") {
return;
}
try {
Map data = {'email': email, 'password': password};
var jsonResponse;
var response = await http
.post('https://imyLink.com/authenticate', body: data);
if (response.statusCode == 200 || response.statusCode == 201) {
//I want to navigate to my home screen once the request made is successful
Navigator.of(context).push(YOUR_ROUTE); // new line
jsonResponse = json.decode(response.body);
if (jsonResponse != null) {
await sharedPreferences.setString("userToken", jsonResponse["token"]);
var token = sharedPreferences.getString("userToken");
print('Token: $token');
print(jsonResponse);
print("Login successful");
}
} else {
print(response.statusCode);
print('Login Unsuccessful');
print(response.body);
}
} catch (e) {
print(e);
}
}
OPTION 2
You can access your app's Navigator without a context by setting the navigatorKey property of your MaterialApp:
/// A key to use when building the [Navigator].
///
/// If a [navigatorKey] is specified, the [Navigator] can be directly
/// manipulated without first obtaining it from a [BuildContext] via
/// [Navigator.of]: from the [navigatorKey], use the [GlobalKey.currentState]
/// getter.
///
/// If this is changed, a new [Navigator] will be created, losing all the
/// application state in the process; in that case, the [navigatorObservers]
/// must also be changed, since the previous observers will be attached to the
/// previous navigator.
final GlobalKey<NavigatorState> navigatorKey;
Create the key:
final GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
Pass it to MaterialApp:
new MaterialApp(
title: 'MyApp',
navigatorKey: key,
);
Push routes (both named and non-named routes work):
navigatorKey.currentState.pushNamed('/someRoute');
Find more details about option 2 by following the github issue below: https://github.com/brianegan/flutter_redux/issues/5#issuecomment-361215074

You can use flutter Get package.
Here is link.

you can use this plugin to skip the required context
https://pub.dev/packages/one_context
// go to second page using named route
OneContext().pushNamed('/second');
// go to second page using MaterialPageRoute
OneContext().push(MaterialPageRoute(builder: (_) => SecondPage()));
// go back from second page
OneContext().pop();

Is there a way to use S.R Keshav method to access pages and giving them an argument ?
routes: {
"sce": (BuildContext context, {args}) => MatchConversation(args as int),
"passport": (BuildContext context, {dynamic args}) => Passport(),
},
It looks that the arg is lost when Navigator goes in _pushEntry method. The navigated Page is accessed, but no initial arguments are loaded.

Simple and clean solution without any plugin/package.
Create global variable:
final GlobalKey<NavigatorState> navKey = GlobalKey<NavigatorState>();
Add this global key to the MaterialApp:
child: MaterialApp(
title: 'MyApp',
navigatorKey: navKey,
));
Now you have 2 ways to use it. Either define routes and use route names or use non-named route (this is the only way if you do not want to use global variables and pass parameters directly to a widget).
a) Option 1. Define routes and then use route names:
// Define route names
MaterialApp(
title: 'MyApp',
navigatorKey: navKey,
routes: {
"login": (BuildContext context) => LoginPage(),
"register": (BuildContext context) => RegisterPage(),
);
// Now anywhere inside your code change widget like this without context:
navKey.currentState?.pushNamed('login');
b) Option 2. Push non-named routes to the navigator:
navKey.currentState?.push(MaterialPageRoute(builder: (_) => LoginPage()));
This way allows to pass parameters directly to widget without global variable:
navKey.currentState?.push(MaterialPageRoute(builder: (_) => HomePage('yourStringValue', 32)));

Related

How to create http client in Flutter with token key and use it with Provider?

I want to create http client and I want to use that http client in entire app. Creating http client is not a big deal but I want to add token in header which I get after login.
Right now I'm doing like this:
web_api_service.dart
Here I'm creating http client with dio package with the token after login
class WebApiService{
final String? tokenKey;
WebApiService(this.tokenKey);
Dio _dio = Dio();
Dio get dio {
_dio.options.baseUrl = BASE_URL;
_dio.options.headers = {'token': '$tokenKey'};
return _dio;
}
Future<List<SessionData>?> upcomingSessionData(SessionRequest request) async{
List<SessionData>? sessions;
try{
var response = await dio.post('/session/upcommingSession', data: sessionRequestToJson(request));
final responseMap = response.data;
// print(responseMap);
if(response.statusCode == 200){
if(responseMap['status'] == "Success"){
final data = upcomingSessionFromJson(jsonEncode(responseMap));
sessions = data.data;
print(responseMap);
return sessions;
}
}
} catch (e){
print(e);
rethrow;
}
return sessions;
}
}
main.dart
To get token from Auth I have created a ProxyProvider
void main() async{
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Auth()),
ProxyProvider<Auth, WebApiService>(update: (_, auth, __) => WebApiService(auth.tokenKey)),
],
child: Home(),
);
}
}
home.dart
Here I'm using FutureBuilder to get the data from api
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final _api = Provider.of<WebApiService>(context, listen: false);
return FutureBuilder<List<SessionData>?>(
future: _api.upcomingSessionData(SessionRequest(
trainerId: "110006",
lat: "17.387140",
lng: "78.491684",
role: "Admin"
)),
builder: (BuildContext context, AsyncSnapshot<List<SessionData>?> snapshot){
return SomeWidget();
}
}
}
}
What I want?
I don't want to use FutureBuilder instead I want to call the api from a controller to separate the UI from business logic.
I want to create a http client with token key in header (I get token after login) which I can use in entire app.
Any positive feedback is also appreciated
In a case like this, I often use injector for it. for example using get_it
You can register a singleton to save the token values that you get from authentication logic. let say that we save the token on AuthModel object.
for example:
final getIt = GetIt.instance;
class AuthModel {
String? token;
AuthModel({this.token});
}
void setup() {
getIt.registerSingleton<AuthModel>(AuthModel());
}
call these setup at main, before everything else is called.
then when we need to update the token, or get the value of these token, just simply call the getIt anywhere in the project.
getIt<AuthModel>().token = NEW_TOKEN;
var savedToken = getIt<AuthModel>().token;

Run Function / Get Data when loading new screen in Flutter

I am trying to run a function that gets data via an API call that needs to initialize before / while loading the screen. I have tried using a future builder, and other methods, and perhaps i am not implementing this correctly but i cannot figure out how to do this. I have also called an async method that uses setState within my initstate method which works, but it initially loads and returns an error and what I assume is that after the state rebuilds it displays correctly.Ideally this should get the data before actually loading the screen to avoid errors.
Code for my main function in the main.dart file
void main() async {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]
);
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
final SharedPreferences prefs = await _prefs;
late String initialRoute;
var accessToken = prefs.getString('access_token');
var refreshToken = prefs.getString('refresh_token');
if (accessToken != null && refreshToken != null) {
var response = await sessionRefresh(accessToken);
if (response.statusCode == 200) {
initialRoute = kDashboardID;
}
else if (response.statusCode == 401) {
var refreshResponse = await tokenRefresh(refreshToken);
if (refreshResponse.statusCode == 200) {
prefs.setString('access_token', jsonDecode(refreshResponse.body)["access_token"]);
initialRoute = kDashboardID;
}
else {
initialRoute = kLoginScreenID;
}
}
}
else {
initialRoute = kLoginScreenID;
}
runApp(MyApp(initialRoute: initialRoute,));
}
class MyApp extends StatelessWidget {
//const MyApp({Key? key}) : super(key: key);
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
String initialRoute;
MyApp({required this.initialRoute});
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => UserData(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
routes: {
kLaunchScreenID: (context) => LaunchScreen(),
kLoginScreenID: (context) => LoginScreen(),
kUserRegistrationID: (context) => UserRegistration(),
kAccountVerificationID: (context) => OTPVerification(),
kDashboardID: (context) => Dashboard(),
kAccountsID: (context) => Accounts(),
},
initialRoute: initialRoute,
),
);
}
}
The ideal result would be when the initialRoute equals "kDashboardID" (user dashboard) i need to run a function that gets data so that the data can be used to populate certain widgets (text widgets, etc.) on the dashboard screen.
Not sure how to best to accomplish this.

I have a question about navigating to the next page conditionally in initstate

I want to implement Auto Login with Shared preferences.
What I want to implement is that as soon as 'LoginPage' starts, it goes to the next page without rendering LoginPage according to the Flag value stored in Shared preferences.
However, there is a problem in not becoming Navigate even though implementing these functions and calling them from initstate. What is the problem?
//Login Page
void autoLogIn() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
final String userId = prefs.getString('username');
print("ddddddddddddddd");
SocketProvider provider = Provider.of<SocketProvider>(context);
Future.delayed(Duration(milliseconds: 100)).then((_) {**//I tried giving Delay but it still didn't work.**
Navigator.of(context).pushNamedAndRemoveUntil("/MainPage", (route) => false);
});
}
#override
void initState() {
// TODO: implement initState
loginBloc = BlocProvider.of<LoginBloc>(context);
if(!kReleaseMode){
_idController.text = "TESTTEST";
_passwordController.text = "1234123";
}
initBadgeList();
autoLogIn();**//This is the function in question.**
super.initState();
print("1111111111111111");
}
I don't think you should show LoginPage widget if user is already logged in and then navigate to main page.
I suggest you to use FutureBuilder and show either splash screen or loader while performing await SharedPreferences.getInstance(). In this case your App widget should look like this:
class App extends MaterialApp {
App()
: super(
title: 'MyApp',
...
home: FutureBuilder(
future: SharedPreferences.getInstance(),
builder: (context, snapshot) {
if (snapshot.data != null) {
final SharedPreferences prefs = snapshot.data;
final userId = prefs.getString('username');
...
return userId == null ?? LoginPage() : MainPage();
} else {
return SplashScreenOrLoader();
}
}));
}
But if you still want to show LoginPage first, just replace SplashScreenOrLoader() with LoginPage() in code above.

How to redirect to a login page if Flutter API response is unauthorized?

I am building a Flutter app which uses a Golang API to fetch data. The API will return a 401 unauthorized if the JWT token is not valid. How can I redirect to a login page on any API call if the response status is 401?
Here is my flutter code:
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
Provider.debugCheckInvalidValueType = null;
AppLanguage appLanguage = AppLanguage();
await appLanguage.fetchLocale();
runApp(MyApp(
appLanguage: appLanguage,
));
}
class MyApp extends StatelessWidget {
final AppLanguage appLanguage;
MyApp({this.appLanguage});
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: providers,
child: MaterialApp(
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
initialRoute: RoutePaths.Login,
onGenerateRoute: Router.generateRoute,
)
);
}
}
tables.dart
class Tables extends StatelessWidget {
const Tables({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BaseWidget<TablesModel>(
model: TablesModel(api: Provider.of(context, listen: false)),
onModelReady: (model) => model.fetchTables(),
builder: (context, model, child) => model.busy
? Center(
child: CircularProgressIndicator(),
)
: Expanded(
child: GridView.builder (
---
tables_model.dart
class TablesModel extends BaseModel {
Api _api;
TablesModel({#required Api api}) : _api = api;
List<Tbl> tables;
Future fetchTables() async {
setBusy(true);
tables = await _api.getTables();
setBusy(false);
}
#override
void dispose() {
print('Tables has been disposed!!');
super.dispose();
}
}
api.dart
Future<List<Tbl>> getTables() async {
var tables = List<Tbl>();
try {
var response = await http.get('$_baseUrl/tables/list');
var parsed = json.decode(response.body) as List<dynamic>;
if (parsed != null) {
for (var table in parsed) {
tables.add(Tbl.fromJson(table));
}
}
} catch (e) {print(e); return null;}
return tables;
}
Since you already have a MaterialApp in your tree and the named routes registered, this should be as simple as adding a call to push your login page around the same time you get the response.
First, you should modify getTables to check response for the status code with statusCode property of the Response object and shown with the following code block:
var response = await http.get('$_baseUrl/tables/list');
if(response.statusCode == 401) {
//Act on status of 401 here
}
Now that you have a way of checking when the response has a status code of 401, you can navigate to your login page with the Navigator. The Navigator needs BuildContext, so that must be passed to the getTables function.
This involves modifying getTables to be:
Future<List<Tbl>> getTables(BuildContext context) async {
and fetchTables needs a similar change:
Future fetchTables(BuildContext context) async {
Then, where these methods are called, you pass context down:
In Tables
model.fetchTables(context)
In TablesModel
Future fetchTables(BuildContext context) async {
setBusy(true);
tables = await _api.getTables(context);
setBusy(false);
}
and finally in getTables, you use the passed context to use the Navigator:
Future<List<Tbl>> getTables(BuildContext context) async {
var tables = List<Tbl>();
try {
var response = await http.get('$_baseUrl/tables/list');
//Check response status code
if(response.statusCode == 401) {
Navigator.of(context).pushNamed(RoutePaths.Login);//Navigator is used here to go to login only with 401 status code
return null;
}
var parsed = json.decode(response.body) as List<dynamic>;
if (parsed != null) {
for (var table in parsed) {
tables.add(Tbl.fromJson(table));
}
}
} catch (e) {print(e); return null;}
return tables;
}
Instead of Navigator.of(context).pushNamed(RoutePaths.Login);, you could use Navigator.pushNamed(context, RoutePaths.Login); if you prefer, but as you can read at this answer, they internally do the same thing.
Now when there is a status code of 401, a user will be navigated to the login screen.

Where to handle Firebase Dynamic Links in Flutter?

I use Firebase dynamic links and also named routes. What I want is to install a global listener for the dynamic link events and forward to register page if a token is provided. In the code below I got the exception The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget. which means I have to put navigation code below the home: property of MaterialApp. But when doing this I had to implement the dynamic links event handler for earch route.
class MyApp extends StatelessWidget {
String title = "Framr";
#override
Widget build(BuildContext context) {
FirebaseDynamicLinks.instance.onLink(
onSuccess: (linkData) {
if (linkData != null) {
try {
Navigator.pushNamed(context, '/register', arguments: linkData);
// throws: The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget.
} catch(e) {
print(e);
}
}
return null;
}
);
return MaterialApp(
title: "...",
home: LoginPage(),
routes: {
'/createEvent': (context) => CreateEventPage(),
'/showEvent': (context) => ShowEventPage(),
'/register': (context) => RegisterPage(),
},
);
}
}
I was able to get this work by following the example provided from the dynamic link README with the use of the no_context_navigation package or GlobalKey to workaround around the lack of context to call Navigator.pushNamed(...). Note: You don't have to use no_context_navigation. You can implement the no context routing yourself. Here's an example.
// Add this
import 'package:no_context_navigation/no_context_navigation.dart';
void main() {
runApp(MaterialApp(
title: 'Dynamic Links Example',
// Add this
navigatorKey: NavigationService.navigationKey,
routes: <String, WidgetBuilder>{
'/': (BuildContext context) => MyHomeWidget(), // Default home route
'/helloworld': (BuildContext context) => MyHelloWorldWidget(),
},
));
}
class MyHomeWidgetState extends State<MyHomeWidget> {
.
.
.
#override
void initState() {
super.initState();
this.initDynamicLinks();
}
void initDynamicLinks() async {
FirebaseDynamicLinks.instance.onLink(
onSuccess: (PendingDynamicLinkData dynamicLink) async {
// Add this.
final NavigationService navService = NavigationService();
final Uri deepLink = dynamicLink?.link;
if (deepLink != null) {
// This doesn't work due to lack of context
// Navigator.pushNamed(context, deepLink.path);
// Use this instead
navService.pushNamed('/helloworld', args: dynamicLink);
}
},
onError: (OnLinkErrorException e) async {
print('onLinkError');
print(e.message);
}
);
final PendingDynamicLinkData data = await FirebaseDynamicLinks.instance.getInitialLink();
final Uri deepLink = data?.link;
if (deepLink != null) {
// This doesn't work due to lack of context
// Navigator.pushNamed(context, deepLink.path);
// Use this instead
navService.pushNamed('/helloworld', args: dynamicLink);
}
}
.
.
.
}
// pubspec.yaml
no_context_navigation: ^1.0.4