Flutter: как вызвать SnackBar из класса, который не является виджетом - PullRequest
0 голосов
/ 04 февраля 2020

Я начинаю с 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;
  }
}


[ОБНОВЛЕНИЕ I]

Вот поставщик, которым я пользуюсь. В этом провайдере я использую перехватчик.

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 widget:

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

[ОБНОВЛЕНИЕ II]

Здесь 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.

error

1 Ответ

2 голосов
/ 04 февраля 2020

Обновленный ответ

Оригинальный ответ передает BuildContext в ваш ChangeNotifier сервис, который технически работает, но после просмотра я понял, что это довольно непрофессионально. Это связано с тем, что вся концепция использования Provider или службы заключается в том, чтобы отделить здание виджета от фоновых функций. Проходить BuildContext и создавать Snackbar изнутри сервиса не очень хорошо. Беллоу гораздо более профессиональный, немного более трудоемкий, чтобы обернуть вокруг него голову, но гораздо более гибкий в долгосрочной перспективе.

Идея

Так что весь код Widget содержится внутри класса, который вы используете для пользовательского интерфейса и UX, вам нужно иметь некоторый тип функции, который есть в классе, но вызывается только из вашего ApiInterceptor. Для этого вы будете использовать то, что называется typedef, которое может быть применено к переменной.

Шаг 1: Создайте typedef

Ваш typedef должен быть создан вне класса, но все же в основном файле, который вы собираетесь применять, желательно в файле, содержащем ApiInterceptor.

typedef void OnInterceptError (String errorMessage);

Если вы никогда не работали с typedef на любом языке вы, вероятно, очень запутались. Все это делает создание типа функции, которая возвращает void и принимает String для ввода.

Шаг 2: Использование OnInterceptError внутри 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;
}

После настройки вы, наконец, можете приступить к хорошему: настройте пользовательский интерфейс !!!

Шаг 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);
  }
);

Сначала это кажется действительно очень сложным, но это действительно хороший способ сделать что-то, потому что он разделяет ваши сервисы и пользовательский интерфейс. Ниже приведен оригинальный ответ, если вам нужна ссылка или вы все еще хотите использовать этот метод.

Оригинальный ответ

К сожалению, из-за того, как Дарт работает, захват BuildContext может быть немного медведь, но 100% возможно. Я проведу вас по шагам:

Шаг 1. Требуется BuildContext в ApiInterceptor

В настоящее время ваш класс ApiInterceptor объявляется без каких-либо входных переменных, поэтому вы будете добавьте следующее к своему классу вверху.

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

final BuildContext context;

Теперь каждый раз, когда к вашему классу обращаются внутри базы кода, среда IDE уведомляет вас о наличии отсутствующей переменной.

Шаг 2. Требуется BuildContext в Auth

К сожалению, вам придется сделать то же самое с вашим 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 при вызове Scaffold.of(context).

Надеюсь, это поможет и сделает ваш день на немного проще.

...