Flutter:如何从 class 调用不是小部件的 SnackBar

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

我从 flutter 开始,我制作了一个使用 REST 管理登录屏幕的简单应用程序 API。

我正在使用 http 包和 http_interceptor 包来拦截和发送 headers 中的令牌。

问题是...我可以毫无问题地用拦截器捕获错误。但是,有任何方法可以使用来自我的拦截器 class 的全局小吃店 "notify" 并将用户重定向到显示应用程序中的任何错误的登录屏幕,例如,当令牌无效时?

这是我的拦截器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;
  }
}


[更新一]

这是我使用的提供商。在此提供程序中,我使用拦截器。

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

在我的 main.dart 上使用 MultipleProvider 小部件调用这些提供程序:

@override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider.value(
          value: ApiInterceptor(),
        ),
        ChangeNotifierProvider.value(
          value: Auth(),
        ),
        ChangeNotifierProvider.value(
          value: TurnActive(),
        ),
      ],
      child: MaterialApp(
.
.
.

[更新二]

这是 main.dart 更新...但仍然无法正常工作。

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();
              }
            },
          ),
        ),
      ),
    );
  }
}

在我的拦截器上:

.
.
.
@override
  Future<ResponseData> interceptResponse({ResponseData data}) async {
    final decodedResponse = json.decode(data.body);

    Scaffold.of(context).showSnackBar(SnackBar(
      content: Text(decodedResponse['error']),
    ));
.
.
.

错误是: Scaffold.of() called with a context that does not contain a Scaffold.

更新答案

最初的答案将 BuildContext 传递到您的 ChangeNotifier 服务中,这在技术上是可行的,但在查看之后我意识到它非常不专业。这是因为使用 Provider 或服务的整个概念是将小部件构建和后台功能分开。从服务内部传递 BuildContext 并创建 Snackbar 并不是很好。 Bellow 是一个更专业的,稍微多一点的工作来绕过它,但在长 运行.

中更灵活

想法

因此,所有 Widget 代码都包含在您用于 UI 和 UX 的 class 中,您需要在class 但只能从您的 ApiInterceptor 调用。为此,您将使用可以应用于变量的所谓 typedef

第 1 步:创建 typedef

您的 typedef 应该在 class 之外创建,但仍然在您要应用它的主文件中,最好是在包含 ApiInterceptor 的文件中。

typedef void OnInterceptError (String errorMessage);

如果您从未使用过任何语言的 typedef,您可能会感到非常困惑。所有这一切都在创建一个函数类型,即 returns void,并采用 String 作为输入。

第 2 步:在 ApiInterceptor

中使用 OnInterceptError
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;
}

设置好之后,终于可以进入到好的部分了:设置UI!!!

第 3 步:创建 OnInterceptError 函数...

现在您已经有了函数所在的位置 运行,您需要在函数所在的位置创建... 功能.

无论您在何处实施此 ApiInterceptor 服务,您现在都应该传递一些内容以达到以下效果。

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

起初看起来真的很复杂,但它确实是一种很好的做事方式,因为它使您的服务与 UI 分开。如果您想参考或仍想使用该方法,下面是原始答案。

原回答

遗憾的是,由于 Dart 的工作方式,抓住 BuildContext 可能有点困难,但 100% 有可能。我将引导您完成这些步骤:

第 1 步:在 ApiInterceptor

中要求 BuildContext

目前您的 ApiInterceptor class 声明时没有任何输入变量,因此您需要将以下内容添加到顶部的 class。

ApiInterceptor({
  @required this.context,
}) : assert(context != null);

final BuildContext context;

现在,每次在您的代码库中访问您的 class 时,IDE 都会通知您缺少变量。

第 2 步:在 Auth

中要求 BuildContext

遗憾的是,您将不得不对 Auth 提供者执行完全相同的操作。由于它们几乎是相同的过程,因此我将避免与最后一步相同的独白。以下是您必须添加到 Auth class.

开头的内容
Auth({
  @required this.context,
}) : assert(context != null);

final BuildContext context;

第 3 步:在每个需要的情况下通过 BuildContext

您或许可以解决这个问题,您的 IDE 会为您完成大部分工作!以下是所有 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;
    }
  }
}

当然,您的 main() 输出:

@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!!! */,
        ),
      ),
    ),
  );
}

确保Builder在树中Scaffold下面,否则在调用Scaffold.of(context)时无法识别Scaffold

我希望这对您有所帮助,让您的一天变得一点更轻松。