I'm starting with flutter and I made a simple app that manages a Login screen using a REST API.
I'm using the http package and the http_interceptor package to intercept and send the token in the headers.
The problem is... I can catch the errors with the interceptor without any problem. But, there is any way to use a global snackbar that from my interceptor class can "notify" and redirect the user to the login screen showing any error in the app, when, for example, the token is invalid?
This is my Interceptor class:
class ApiInterceptor with ChangeNotifier implements InterceptorContract {
final storage = new FlutterSecureStorage();
@override
Future<RequestData> interceptRequest({RequestData data}) async {
[...] // here is the request interceptor
return data;
}
// The response interceptor:
@override
Future<ResponseData> interceptResponse({ResponseData data}) async {
final decodedResponse = json.decode(data.body);
if (data.statusCode >= 400) {
throw HttpException(decodedResponse['error']);
// here i want to send the notification to a snackBar
// then, i want to redirect the user to the login screen
}
return data;
}
}
[UPDATE I]
Here is a provider that I use. In this provider I use the Interceptor.
import 'dart:convert';
import 'package:cadsanjuan_movil/models/http_exception.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart';
import 'package:http_interceptor/http_interceptor.dart';
import '../config/http_interceptor.dart';
import '../config/.env.dart' as config;
class Auth with ChangeNotifier {
String _endpoint = 'auth';
final storage = new FlutterSecureStorage();
// Http Interceptor
Client http = HttpClientWithInterceptor.build(interceptors: [
ApiInterceptor(),
]);
Future singup(String email, String password) async {
final url = "${config.apiBaseUrl}/$_endpoint/signin";
try {
final response = await http.post(url,
body: json.encode({'email': email, 'password': password}));
final decodedResponse = json.decode(response.body);
/* if (response.statusCode >= 400) {
throw HttpException(decodedResponse['error']);
} */
await storage.write(key: 'token', value: decodedResponse['token']);
await storage.write(key: 'user', value: decodedResponse['user']);
await storage.write(key: 'email', value: decodedResponse['email']);
await storage.write(
key: 'employeeId', value: decodedResponse['employeeId'].toString());
//notifyListeners();
} catch (error) {
throw error;
}
}
}
And these providers are called on my main.dart with MultipleProvider
widget:
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: ApiInterceptor(),
),
ChangeNotifierProvider.value(
value: Auth(),
),
ChangeNotifierProvider.value(
value: TurnActive(),
),
],
child: MaterialApp(
.
.
.
[UPDATE II]
Here is the main.dart
updated... and stills not working.
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
final storage = new FlutterSecureStorage();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'CAD App',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: Scaffold(
body: MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: ApiInterceptor(context: context),
),
ChangeNotifierProvider.value(
value: Auth(context: context),
),
ChangeNotifierProvider.value(
value: TurnActive(context: context),
),
],
child: FutureBuilder(
future: storage.read(key: "token"),
builder: (context, storedKey) {
if (!storedKey.hasData) {
return LoadingData(text: 'Por favor espere...');
} else {
return storedKey.data == null
? LoginPage()
: InitialLoadingPage();
}
},
),
),
),
);
}
}
On my interceptor:
.
.
.
@override
Future<ResponseData> interceptResponse({ResponseData data}) async {
final decodedResponse = json.decode(data.body);
Scaffold.of(context).showSnackBar(SnackBar(
content: Text(decodedResponse['error']),
));
.
.
.
The error is: Scaffold.of() called with a context that does not contain a Scaffold.
The original answer passes the BuildContext
into your ChangeNotifier
service which technically works, but after reviewing it I realized that it is quite unprofessional. This is because the whole concept of using a Provider
or service is to separate the widget building and the background functions. Passing the BuildContext
and creating a Snackbar
from inside the service isn't really great. Bellow is a much more professional, slightly more work to wrap your head around it, but is much more flexible in the long run.
So that all the Widget
code is contained inside of the class you are using for the UI and UX, you need to have some type of function that is in the class but only callable from your ApiInterceptor
. To do this you are going to use what is called a typedef
that can be applied to a variable.
typedef
Your typedef
should be created outside of the class, but still in the main file you're going to be applying it, preferably in the file containing ApiInterceptor
.
typedef void OnInterceptError (String errorMessage);
If you've never worked with typedef
in any language, you're probably extremely confused. All this is doing is creating a function type, that returns void
, and takes a String
for input.
OnInterceptError
inside ApiInterceptor
ApiInterceptor({
@required this.interceptError,
}) : assert(interceptError != null);
final OnInterceptError this.interceptError;
// Response interceptor
@override
Future<ResponseData> interceptResponse({ResponseData data}) async {
final decodedResponse = json.decode(data.body);
if (data.statusCode >= 400) {
throw HttpException(decodedResponse['error']);
// Run `interceptError` to send the notification to a
// `Snackbar`
interceptError(decodedResponse['error']);
}
return data;
}
After setting this up, you can finally get to the good part: setup the UI!!!
OnInterceptError
function...Now that you have where the function is run, you need to create where the function has its... functionality .
Wherever you implement this ApiInterceptor
service, you now should pass something to the effect of the following.
ApiInterceptor(
interceptError: (String errorMessage) {
// Show the `Snackbar` from here, which should have
// access to the `BuildContext` to do so and use
// `interceptError` to create the message for the
// `Snackbar`, if you'd like to do so.
print(interceptError);
}
);
At first it seems really really complicated, but it really is a nice way of doing things because it keeps your services and UI separated. Bellow is the original answer if you want a reference or still want to use that method.
Sadly because of how Dart works grabbing the BuildContext
can be a little bit of a bear, but 100% possible. I'll walk you through the steps:
BuildContext
in ApiInterceptor
Currently your ApiInterceptor
class is being declared without any input variables, so you'll add the following to your class at the top.
ApiInterceptor({
@required this.context,
}) : assert(context != null);
final BuildContext context;
Now every time your class is accessed inside your code base you'll be notified by the IDE that there is a missing variable.
BuildContext
in Auth
You'll sadly have to do the exact same thing with your Auth
provider. I'll spare you the same monologue as the last step since they're almost identical procedures. The following is what you must add to the beginning of the Auth
class.
Auth({
@required this.context,
}) : assert(context != null);
final BuildContext context;
Step 3: Pass BuildContext
in every required case
You can probably figure this out, your IDE does most of the work for you! The following is the completed code for all your classes.
class ApiInterceptor with ChangeNotifier implements InterceptorContract {
ApiInterceptor({
@required this.context,
}) : assert(context != null);
final BuildContext context;
final storage = new FlutterSecureStorage();
@override
Future<RequestData> interceptRequest({RequestData data}) async {
[...] // here is the request interceptor
return data;
}
// The response interceptor:
@override
Future<ResponseData> interceptResponse({ResponseData data}) async {
final decodedResponse = json.decode(data.body);
if (data.statusCode >= 400) {
throw HttpException(decodedResponse['error']);
// here i want to send the notification to a snackBar
// then, i want to redirect the user to the login screen
}
return data;
}
}
class Auth with ChangeNotifier {
Auth({
@required this.context,
}) : assert(context != null);
final BuildContext context;
String _endpoint = 'auth';
final storage = new FlutterSecureStorage();
Future singup(String email, String password) async {
// Http Interceptor
Client http = HttpClientWithInterceptor.build(interceptors: [
ApiInterceptor(context: context),
]);
final url = "${config.apiBaseUrl}/$_endpoint/signin";
try {
final response = await http.post(url,
body: json.encode({'email': email, 'password': password}));
final decodedResponse = json.decode(response.body);
/* if (response.statusCode >= 400) {
throw HttpException(decodedResponse['error']);
} */
await storage.write(key: 'token', value: decodedResponse['token']);
await storage.write(key: 'user', value: decodedResponse['user']);
await storage.write(key: 'email', value: decodedResponse['email']);
await storage.write(
key: 'employeeId', value: decodedResponse['employeeId'].toString());
//notifyListeners();
} catch (error) {
throw error;
}
}
}
And of course, your main()
output:
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
home: Builder(
builder: (BuildContext context) => MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: ApiInterceptor(context: context),
),
ChangeNotifierProvider.value(
value: Auth(context: context),
),
ChangeNotifierProvider.value(
value: TurnActive(),
),
],
child: /* CHILD!!! */,
),
),
),
);
}
Make sure that the Builder
is underneath the Scaffold
in the tree, otherwise it won't recognize the Scaffold
when calling Scaffold.of(context)
.
I hope this helps and makes your day a little easier.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.