简体   繁体   中英

Flutter: How to call a SnackBar form a class that is not a widget

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.

错误

Updated answer

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.

The idea

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.

Step 1: Create the 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.

Step 2: Using 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!!!

Step 3: Create the 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.

Original answer

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:

Step 1: Require 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.

Step 2: Require 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM