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

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.

Related

riverpod state not updating

(Update at the end of the post) I want to add my normal firebase auth with additional user information. In this example, name and goal calories. For that, I created this register function:
Future<void> signUpWithEmailAndPassword(String email, String password, BuildContext context, WidgetRef ref, widget) async {
FocusManager.instance.primaryFocus?.unfocus();
try {
await auth.createUserWithEmailAndPassword(email: email, password: password);
ref.read(isUp.notifier).state = false;
ref.read(writeItemViewModelProvider).setInitValue();
} on FirebaseAuthException catch (e) {
the function setInitValue() looks like this:
class FirestoreDb extends ChangeNotifier {
Future<void> setInitValue() async {
await firebaseFirestore.collection('/users/${auth.currentUser!.uid}/UserInfo').doc(auth.currentUser!.uid).set({
'name': null,
'calories': null,
});
}
}
Here seems to work everything fine. Inside firestore a file gets created and my user also. Without this additional user infos my auth works also fine. So I think there is a problem with my stream of the user information. Because: I have to check if the registert user has already added information or not.
I do this with a second .when function:
#override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authStateProvider);
final watcher = ref.watch(itemsProvider);
return authState.when(
data: (data) {
if (data != null) {
return watcher.when(data: (calo) {
if (calo.first.calories != null) {
return const RootPage();
} else {
return UserInformation();
}
}, error: (e, trace) => ErrorScreen(e, trace), loading: () => const LoadingScreen());
the first .when function is for the auth, here seems to be no problem, but the secons is strange. When I login first time, it says bad state. From now on, every time I register with a different account, I only get the old data from the previous account until I hot restart.
After the user information, you get to this page:
#override
Widget build(BuildContext context, WidgetRef ref) {
final streamData = ref.watch(itemsProvider);
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
appBar: AppBar(toolbarHeight: 0, backgroundColor: Colors.transparent),
resizeToAvoidBottomInset: false,
body: streamData.when( data: (calo) {
return Text(calo.first.calories.toString());
}, error: (e, trace) => ErrorScreen(e, trace), loading: () => const LoadingScreen())
);
}
where I can see that s old information until hot restart.
So something with my stream is not updating the state correctly.
When I wrap delete the .when function and use a Streambuilder listening to the stream directly everything works.
Here is my itemsProvider:
final itemsProvider = StreamProvider<List<UsersModel>>(
(ref) => ref.read(itemRepositoryProvider).itemsStream,
);
final itemRepositoryProvider = Provider((ref) => ReadData());
class ReadData{
Stream<List<UsersModel>> get itemsStream {
return firebaseFirestore.collection('/users/${auth.currentUser!.uid}/UserInfo').snapshots().map((QuerySnapshot query) {
List<UsersModel> user = [];
for (var usersIter in query.docs) {
final usersModel = UsersModel.fromDocumentSnapshot(documentSnapshot: usersIter);
user.add(usersModel);
}
return user;
});
}
}
I check with debugging and "print points" the way of the compiler and recognised the problem but have no answer why the compiler do this:
#override
Widget build(BuildContext context, WidgetRef ref) {
print("inside UserInfoBuild");
final watcher = ref.watch(itemsProvider);
return watcher.when(data: (userInfoData) {
print("inside AsyncValue<List<UsersModel>>");
if (userInfoData.first.calories != null) {
return const RootPage();
} else {
return UserInformation(); [...]
declare provider:
final itemsProvider = StreamProvider<List<UsersModel>>(
(ref) {
print("inside stream provider");
return ref.read(itemRepositoryProvider).itemsStream;
},
);
so, my guess was that the print order should be:
I/flutter: inside UserInfoBuild
I/flutter: inside stream provider
I/flutter: inside AsyncValue<List<UsersModel>>
but its actually just:
I/flutter: inside UserInfoBuild
I/flutter: inside AsyncValue<List<UsersModel>>
so the compiler skips the final itemsProvider = StreamProvider.
Just after a hot restart it executes the line of code
I think the key point is 'get' itemsStream. You have two ways to try.
// 1.
final itemsProvider = StreamProvider<List<UsersModel>>(
(ref) => firebaseFirestore.collection('/users/${auth.currentUser!.uid}/UserInfo').snapshots().map((QuerySnapshot query) {
List<UsersModel> user = [];
for (var usersIter in query.docs) {
final usersModel = UsersModel.fromDocumentSnapshot(documentSnapshot: usersIter);
user.add(usersModel);
}
return user;
}),
);
// 2.
You can use StreamController to get data from firebaseFirestore.collection in ReadData class, and use a Stream variable to sync that value. Update StreamProvider to the Stream variable.

ChangeNotifierProvider does not update the model

i am quite new with flutter. I am trying to add a ChangeNotifierProvider into my app. I use flutter_azure_b2c to log in a user, in order to handle to login outcome I have the following code:
AzureB2C.registerCallback(B2COperationSource.POLICY_TRIGGER_INTERACTIVE,
(result) async {
if (result.reason == B2COperationState.SUCCESS) {
List<String>? subjects = await AzureB2C.getSubjects();
if (subjects != null && subjects.isNotEmpty) {
B2CAccessToken? token = await AzureB2C.getAccessToken(subjects[0]);
if (!mounted || token == null) return;
final encodedPayload = token.token.split('.')[1];
final payloadData =
utf8.fuse(base64).decode(base64.normalize(encodedPayload));
final claims = Claims.fromJson(jsonDecode(payloadData));
var m = Provider.of<LoginModel>(context);
m.logIn(claims);
}
}
});
The problem is that when it arrives to var m = Provider.of<LoginModel>(context); the execution stops with out errors without executing m.logIn(claims);, so the model is not changed and the consumer is not called.
Any idea?
This is my consumer:
class App extends StatelessWidget {
const App({super.key});
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => LoginModel(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: appTheme,
home: Consumer<LoginModel>(
builder: (context, value, child) =>
value.claims != null ? const Home() : const Login(),
)),
);
}
}
class LoginModel extends ChangeNotifier {
Claims? _claims;
logIn(Claims claims) {
_claims = claims;
notifyListeners();
}
logOut() {
_claims = null;
notifyListeners();
}
Claims? get claims => _claims;
}
My LoginWidget:
class Login extends StatefulWidget {
const Login({super.key});
#override
LoginState createState() => LoginState();
}
class LoginState extends State<Login> {
B2CConfiguration? _configuration;
checkLogin(BuildContext context) async {
List<String>? subjects = await AzureB2C.getSubjects();
if (subjects != null && subjects.isNotEmpty) {
B2CAccessToken? token = await AzureB2C.getAccessToken(subjects[0]);
if (!mounted || token == null) return;
final encodedData = token.token.split('.')[1];
final data =
utf8.fuse(base64).decode(base64.normalize(encodedData));
final claims = Claims.fromJson(jsonDecode(data));
var m = Provider.of<LoginModel>(context, listen: true);
m.logIn(claims); //<-- debugger never reaches this line
}
}
#override
Widget build(BuildContext context) {
// It is possible to register callbacks in order to handle return values
// from asynchronous calls to the plugin
AzureB2C.registerCallback(B2COperationSource.INIT, (result) async {
if (result.reason == B2COperationState.SUCCESS) {
_configuration = await AzureB2C.getConfiguration();
if (!mounted) return;
await checkLogin(context);
}
});
AzureB2C.registerCallback(B2COperationSource.POLICY_TRIGGER_INTERACTIVE,
(result) async {
if (result.reason == B2COperationState.SUCCESS) {
if (!mounted) return;
await checkLogin(context);
}
});
// Important: Remeber to handle redirect states (if you want to support
// the web platform with redirect method) and init the AzureB2C plugin
// before the material app starts.
AzureB2C.handleRedirectFuture().then((_) => AzureB2C.init("auth_config"));
const String assetName = 'assets/images/logo.svg';
final Widget logo = SvgPicture.asset(
assetName,
);
return SafeArea(
child: //omitted,
);
}
}
I opened an issue as well, but it did not help me.
Try this
var m = Provider.of<LoginModel>(context, listen: false)._claims;
You are using the Provider syntax but not doing anything really with it. You need to set it like this Provider.of<LoginModel>(context, listen: false).login(claims) and call it like this Provider.of<LoginModel>(context, listen: false)._claims;
I fixed it, moving the callback registrations from the build method to the initState method.

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;

Nested Future in Flutter

I'm new to Flutter, (comming from web and especially JS/VueJS)
I'm have a db in firebase that has a collection called edito and inside, i have different artist with a specific Id to call Deezer Api with it.
So what i want to do is first called my db and get the Id for each of artist and then put this id in a function as parameter to complete the url.
I did 2 Future function, one to call the db and one to call the api.
But i don't understand how to use one with the others in the build to get a listview with the information of the api of deezer for each data.
i'm getting the list but it's stuck in and endless loop.
All of my app will be on this nested function, is it possible to do this and call it in any widget that i want ?
here is my code, thanks
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class GetAlbum extends StatefulWidget {
#override
_GetAlbumState createState() => _GetAlbumState();
}
class _GetAlbumState extends State<GetAlbum> {
Map mapResponse;
Future<QuerySnapshot> getDocument() async{
return FirebaseFirestore.instance.collection("edito").get();
}
Future<dynamic> fetchData(id) async{
http.Response response;
response = await http.get('https://api.deezer.com/album/' + id);
if(response.statusCode == 200){
setState(() {
mapResponse = json.decode(response.body);
});
}
}
Future<dynamic> getDocut;
Future<dynamic> getArtist;
#override
void initState() {
getDocut = getDocument();
getArtist = fetchData(null);
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<QuerySnapshot>(
future : getDocut,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot){
if(!snapshot.hasData) {
return CircularProgressIndicator();
}else{
return new ListView(
children: snapshot.data.docs.map<Widget>((document){
print(document.data().length);
return FutureBuilder(
future: fetchData(document.data()['idDeezer'].toString()),
builder: (context, snapshot){
return Container(
child: mapResponse==null?Container(): Text(mapResponse['title'].toString(), style: TextStyle(fontSize: 30),),
);
}
);
}).toList(),
);
}
},
);
}
}
Here's a simplified example of making two linked Future calls where the 2nd depends on data from the first, and using the results in a FutureBuilder:
import 'package:flutter/material.dart';
class FutureBuilder2StatefulPage extends StatefulWidget {
#override
_FutureBuilder2StatefulPageState createState() => _FutureBuilder2StatefulPageState();
}
class _FutureBuilder2StatefulPageState extends State<FutureBuilder2StatefulPage> {
Future<String> _slowData;
#override
void initState() {
super.initState();
_slowData = getAllSlowData(); // combined async calls into one future
}
// linked async calls
Future<String> getAllSlowData() async {
int id = await loadId(); // make 1st async call for id
return loadMoreData(id: id); // use id in 2nd async call
}
Future<int> loadId() async {
int _id = await Future.delayed(Duration(seconds: 2), () => 42);
print('loadId() completed with: $_id'); // debugging
return _id;
}
Future<String> loadMoreData({int id}) async {
return await Future.delayed(Duration(seconds: 2), () => 'Retrieved data for id:$id');
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('FutureBldr Stateful'),
),
body: FutureBuilder<String>(
future: _slowData,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Center(child: Text(snapshot.data));
}
return Center(child: Text('Loading...'));
},
),
);
}
}
This avoids having to nest the FutureBuilder which may be error prone.
And calling future methods directly from a FutureBuilder is not recommended since the call could be made many times if its containing widget is rebuilt (which can happen a lot).
I tried to add firebase in the first one but i get null for the id in the get AllSlowDAta but i got it right with the Future.delayed.
// linked async calls
Future<String> getAllSlowData() async {
String id = await loadId(); // make 1st async call for id
return loadMoreData(id: id); // use id in 2nd async call
}
Future<dynamic> loadId() async {
//return await Future.delayed(Duration(seconds: 2), () => '302127');
await FirebaseFirestore.instance.collection("edito")
.get()
.then((QuerySnapshot querySnapshot) {
querySnapshot.docs.forEach((doc) {
return doc.data()["idDeezer"];
});
});
}
Future<dynamic> loadMoreData({String id}) async {
http.Response response;
response = await http.get('https://api.deezer.com/album/' + id);
if(response.statusCode == 200){
setState(() {
return json.decode(response.body);
});
}
}

Navigation in flutter without context

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)));