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

Posted

技术标签:

【中文标题】Flutter:如何从不是小部件的类中调用 SnackBar【英文标题】:Flutter: How to call a SnackBar form a class that is not a widget 【发布时间】:2020-05-19 18:20:51 【问题描述】:

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

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

问题是......我可以毫无问题地使用拦截器捕获错误。但是,有什么方法可以使用来自我的拦截器类的全局快餐栏,它可以“通知”并将用户重定向到显示应用程序中任何错误的登录屏幕,例如,当令牌无效时?

这是我的拦截器类:

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.

【问题讨论】:

通过你的类构造函数传递ScaffoldContext怎么样? 怎么样?该课程没有从任何小部件调用...你能解释一下吗?感谢您的回答! 你的代码中ApiInterceptor在哪里使用?你的树下应该有某个地方可以为此传递BuildContext 好的,在某些提供程序上调用了 ApiInterceptor。我会用一个例子来更新这个问题...... 完成@odonckers!感谢朋友的帮助。 【参考方案1】:

更新答案

原始答案将BuildContext 传递到您的ChangeNotifier 服务中,该服务在技术上有效,但在查看后我意识到它非常不专业。这是因为使用Provider 或服务的整个概念是将小部件构建和后台功能分开。传递BuildContext 并从服务内部创建Snackbar 并不是很好。 Bellow 是一项更专业的工作,需要更多的工作来解决它,但从长远来看更加灵活。

想法

因此,所有Widget 代码都包含在您用于 UI 和 UX 的类中,您需要在类中具有某种类型的函数,但只能从您的 ApiInterceptor 调用。为此,您将使用可以应用于变量的称为 typedef 的东西。

第 1 步:创建typedef

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

typedef void OnInterceptError (String errorMessage);

如果您从未使用过任何语言的typedef,您可能会非常困惑。所做的只是创建一个函数类型,它返回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 类在声明时没有任何输入变量,因此您将在顶部添加以下内容。

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

final BuildContext context;

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

第 2 步:在 Auth 中需要 BuildContext

很遗憾,您将不得不对您的 Auth 提供者执行完全相同的操作。因为它们几乎是相同的程序,所以我不会像最后一步那样给你同样的独白。以下是您必须添加到 Auth 类开头的内容。

Auth(
  @required this.context,
) : assert(context != null);

final BuildContext context;

第 3 步:在每个必需的情况下传递 BuildContext

您可能会明白这一点,您的 IDE 会为您完成大部分工作!以下是您所有课程的完整代码。

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

我希望这会有所帮助,并使您的一天有点更轻松。

【讨论】:

我在尝试实现您的代码时遇到问题我的朋友....当我尝试将上下文传递给 ApiInterceptor 时,IDE 出现错误:“在初始化程序中只能访问静态成员。”这里:客户端 http = HttpClientWithInterceptor.build(interceptors: [ApiInterceptor(context: context), ]); 我已经解决了将客户端 (Client http = ...) 移动到 Future 函数的问题。但是当我尝试它时,我有另一个错误说找不到任何脚手架来显示 SnackBar... 在 main.dart 中,在孩子中我有脚手架。 哦...我忘了你的BuildContext 必须有一个活跃的Scaffold 你通过它之前。所以我会尝试将您的MultiProvider 移到MaterialApp 内的树上。如果这不起作用,请尝试将其移到 Scaffold 内部更远一些,这样更有可能起作用。 同样的错误我的朋友。请看我的更新二。感谢您花时间帮助我!我真的很感激。 我刚刚用一个更好的解决方案更新了我的答案。在看了你的评论,然后看了我的回答之后,我意识到我向你展示的方式是多么不专业。从技术上讲,它可以工作,但不可扩展,并且不适用于一类中的 UI 概念和另一类中的服务。希望上面的答案能解决问题。

以上是关于Flutter:如何从不是小部件的类中调用 SnackBar的主要内容,如果未能解决你的问题,请参考以下文章

Flutter:如何从浮动操作按钮调用小部件状态类的方法

如何从另一个小部件类更新小部件树 - Flutter

使用 Flutter,如何在单独的登录页面小部件中显示在 AuthService 类中捕获的 Firebase Auth 错误消息?

Flutter -PDF -- 错误 - 这个小部件创建了 20 多个页面。这可能是小部件或文档中的问题

在 Flutter 测试中获取小部件的位置?

如何从 Flutter 中的子小部件调用父小部件功能