Доступ провайдера флаттера через addPostFrameCallback говорит, что виджет находится вне дерева виджетов, но инспектор флаттера показывает иначе - PullRequest
1 голос
/ 24 января 2020

Я создаю свое первое большое приложение во Flutter, и первое, где мне нужно State Management, поэтому я обратился к Provider - рекомендованному пакету для State Management. Однако у меня возникают некоторые проблемы, когда я объявляю своих провайдеров в файле main.dart и вниз по дереву. Я хочу внести изменения и взаимодействовать с одним из провайдеров, но независимо от того, какое решение я пробую, я получаю одну и ту же ошибку: «Пробовал прослушать значение, предоставляемое поставщиком, из-за пределов дерева виджетов. " Я получаю эту ошибку, даже если, по словам инспектора флаттера, виджет, из которого я пытаюсь внести изменения в провайдера, находится внутри дерева виджетов (экран «Домашний экран» находится там, где я обновляю провайдера). Widget tree according to the Flutter inspector

Ниже я также делюсь своим кодом: main.dart:

import 'package:flutter/material.dart';
import 'package:tic_tac_2/screens/welcome_screen.dart';
import 'package:provider/provider.dart';
import 'package:tic_tac_2/models/restaurants_data.dart';
import 'package:tic_tac_2/models/promotions_data.dart';
import 'package:tic_tac_2/models/user.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<User>(create: (context) => User(),),
        ChangeNotifierProvider<RestaurantsData>(create: (context) => RestaurantsData(),),
        ChangeNotifierProvider<PromotionsData>(create: (context) => PromotionsData(),),
      ],
      child: MaterialApp(
        title: 'Tic Tac',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: WelcomeScreen(),
      ),
    );
  }
}

welcome_screen.dart:

import 'package:flutter/material.dart';
import 'package:animated_text_kit/animated_text_kit.dart';
import 'package:tic_tac_2/components/rounded_button.dart';
import 'login_screen.dart';
import 'register_screen.dart';

class WelcomeScreen extends StatelessWidget {
  static const String id = 'welcome_screen';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Color(0xff000080),
      body: Padding(
        padding: EdgeInsets.symmetric(horizontal: 24.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            Row(
              children: <Widget>[
                Hero(
                  tag: 'logo',
                  child: Container(
                    child: Image.asset('images/pin.png'),
                    height: 60.0,
                  ),
                ),
                TypewriterAnimatedTextKit(
                  text: ['Tic Tac'],
                  textStyle: TextStyle(
                      fontWeight: FontWeight.w900,
                      fontSize: 45.0,
                      color: Colors.white
                  ),
                ),
              ],
            ),
            SizedBox(
              height: 48.0,
            ),
            RoundedButton(
              title: 'Entrar',
              colour: Colors.lightBlueAccent,
              onPressed: () {
                Navigator.push(context, MaterialPageRoute(builder: (context) => LoginScreen()));
                //Navigator.pushNamed(context, LoginScreen.id);
              },
            ),
            RoundedButton(
              title: 'Registro',
              colour: Colors.blueAccent,
              onPressed: () {
                Navigator.push(context, MaterialPageRoute(builder: (context) => RegistrationScreen()));
                //Navigator.pushNamed(context, RegistrationScreen.id);
              },
            ),
          ],
        ),
      ),
    );
  }
}

login_screen. dart:

import 'package:flutter/material.dart';
import 'package:tic_tac_2/components/rounded_button.dart';
import 'package:tic_tac_2/constants.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:modal_progress_hud/modal_progress_hud.dart';
import 'home_screen.dart';
import 'package:tic_tac_2/models/user.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:rflutter_alert/rflutter_alert.dart';
import 'package:email_validator/email_validator.dart';

final _firestore = Firestore.instance;

class LoginScreen extends StatefulWidget {
  static const String id = 'login_screen';
  @override
  _LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _formKey = GlobalKey<FormState>();

  bool showSpinner = false;
  final _auth = FirebaseAuth.instance;
  String email;
  String password;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: ModalProgressHUD(
        inAsyncCall: showSpinner,
        child: Padding(
          padding: EdgeInsets.symmetric(horizontal: 24.0),
          child: Form(
            key: _formKey,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                Flexible(
                  child: Hero(
                    tag: 'logo',
                    child: Container(
                      height: 200.0,
                      child: Image.asset('images/pin.png'),
                    ),
                  ),
                ),
                SizedBox(
                  height: 48.0,
                ),
                TextFormField(
                  validator: (val) => !EmailValidator.validate(val, true)
                      ? 'Correo inválido'
                      : null,
                  keyboardType: TextInputType.emailAddress,
                  textAlign: TextAlign.center,
                  onChanged: (value) {
                    email = value;
                  },
                  decoration: kTextFieldDecoration.copyWith(
                      hintText: 'Escribe tu correo'),
                ),
                SizedBox(
                  height: 8.0,
                ),
                TextFormField(
                  validator: (val) =>
                      val.length < 6 ? 'La contraseña es muy corta' : null,
                  obscureText: true,
                  textAlign: TextAlign.center,
                  onChanged: (value) {
                    password = value;
                  },
                  decoration: kTextFieldDecoration.copyWith(
                      hintText: 'Escribe tu contraseña'),
                ),
                SizedBox(
                  height: 24.0,
                ),
                RoundedButton(
                  title: 'Entrar',
                  colour: Colors.lightBlueAccent,
                  onPressed: () async {
                    if (_formKey.currentState.validate()) {
                      setState(() {
                        showSpinner = true;
                      });
                      try {
                        final user = await _auth.signInWithEmailAndPassword(
                            email: email, password: password);
                        if (user != null) {
                          return _firestore
                              .collection('user')
                              .document(user.user.uid)
                              .get()
                              .then((DocumentSnapshot ds) {
                            User localUser = User(
                                uid: user.user.uid,
                                email: email,
                                role: ds.data['role']);
                            Navigator.push(
                                context,
                                MaterialPageRoute(
                                    builder: (context) => HomeScreen(
                                          user: user.user,
                                          newUser: localUser,
                                        )));
                          });
                        }
                        setState(() {
                          showSpinner = false;
                        });
                      } catch (e) {
                        setState(() {
                          showSpinner = false;
                        });
                        Alert(
                                context: context,
                                title: "Error en el registro",
                                desc: e)
                            .show();
                        print(e);
                      }
                    }
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

home_screen.dart:

import 'package:tic_tac_2/models/user.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'dart:async';
import 'package:tic_tac_2/models/restaurants_data.dart';
import 'package:provider/provider.dart';
import 'package:tic_tac_2/models/promotions_data.dart';
import 'package:tic_tac_2/widgets/RestaurantList.dart';
import 'package:geolocator/geolocator.dart';

Geoflutterfire geo = Geoflutterfire();
FirebaseUser loggedInUser;
User localUser;

class HomeScreen extends StatefulWidget {
  final FirebaseUser user;
  final User newUser;

  const HomeScreen({Key key, this.user, this.newUser}) : super(key: key);

  static const String id = 'home_screen';

  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final _firestore = Firestore.instance;
  GoogleMapController mapController;
  var pos;
  Stream<dynamic> query;

  StreamSubscription subscription;

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    subscription.cancel();
  }



  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    if (localUser == null) {
      localUser = widget.newUser;
      loggedInUser = widget.user;
    }
  }

  @override
  Widget build(BuildContext context) {
    void _getCurrentLocation(BuildContext context) async {
      try {
        Position position = await Geolocator()
            .getCurrentPosition(desiredAccuracy: LocationAccuracy.low);
        print('lat');
        print(position.latitude);
        print('lng');
        print(position.longitude);

        final QuerySnapshot restaurants = await _firestore.collection('restaurants').getDocuments();
        for(var restaurant in restaurants.documents) {
          print(restaurant);
          Provider.of<RestaurantsData>(context).addRestaurant(
            name: restaurant.data['name'],
            owner: restaurant.data['owner'],
            location: restaurant.data['location'],
            uid: restaurant.data['uid'],
          );
        }
      } catch (e) {
        print(e);
      }
    }

    WidgetsBinding.instance.addPostFrameCallback((_) => _getCurrentLocation(context));
    print(Provider.of<RestaurantsData>(context).restaurants);
    return Scaffold(
      backgroundColor: Color(0xff000080),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Container(
            padding: EdgeInsets.only(
              top: 60.0,
              bottom: 30.0,
              left: 30.0,
              right: 30.0,
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                CircleAvatar(
                  child: Icon(
                    Icons.list,
                    size: 30.0,
                    color: Color(0xff000080),
                  ),
                  backgroundColor: Colors.white,
                  radius: 30.0,
                ),
                SizedBox(
                  height: 10.0,
                ),
                Text(
                  'Tic Tac',
                  style: TextStyle(
                    fontSize: 50.0,
                    color: Colors.white,
                    fontWeight: FontWeight.w700,
                  ),
                ),
                Text(
                  'Restaurantes',
                  style: TextStyle(color: Colors.white, fontSize: 18.0),
                )
              ],
            ),
          ),
          Expanded(
            child: Container(
              padding: EdgeInsets.symmetric(horizontal: 20.0),
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.only(
                  topLeft: Radius.circular(20.0),
                  topRight: Radius.circular(20.0),
                ),
              ),
              child:
              Provider.of<RestaurantsData>(context).restaurants.length > 0
                  ? RestaurantList()
                  : Container(),
            ),
          ),
        ],
      ),
    );
  }
}

Насколько я могу судить, причиной проблемы в файле home_screen является " getCurrentLocation (BuildContext) context) {} "функция и как и когда я ее вызываю. Я попытался превратить все в statelessWidgets, вызывая функцию getLocation без "WidgetsBinding.instance.addPostFrameCallback (() => _getCurrentLocation (context));" линия. Я пытался не передавать контекст функции в числе других решений, которые я пробовал.

Я очень ценю вашу помощь и хотел бы поблагодарить вас заранее. Если у вас есть какие-либо сомнения относительно кода, я буду более чем рад ответить на все из них.

1 Ответ

6 голосов
/ 24 января 2020

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

Решение

В вашем методе _getCurrentLocation измените

Provider.of<RestaurantsData>(context).addRestaurant()

до

Provider.of<RestaurantsData>(context, listen: false).addRestaurant()

Пояснение

Как показывает ошибка

Попытка прослушать значение, предоставляемое поставщиком, извне виджета tree.

Вы получаете уведомление об обновлении из своего экземпляра Provider за пределами дерева виджетов. т. е. ваш экземпляр Provider вызывает метод Provider NotifyListeners(), который отправляет обновления всем слушателям. И этот конкретный вызов в вашем вопросе прослушивает эти обновления, а именно: Provider.of<RestaurantsData>(context)

Это происходит потому, что addPostFrameCallback вызывает вызов своего параметра за пределами вашего дерева виджетов. Этот последний обратный вызов инкапсулирует _getCurrentLocation локальную функцию. В свою очередь эта функция имеет вызов экземпляра Provider. Эта последовательность событий привела к вызову провайдера для прослушивания обновлений вне дерева виджетов.

Ошибочно прослушивать обновления уведомлений вне дерева виджетов, например, обратные вызовы действий пользователя или initState.

Чтобы решить эту проблему, вам нужно присвоить listen флаг его нестандартному значению false в областях кода за пределами вашего дерева виджетов. например, initState или обратные вызовы взаимодействия с пользователем, или любая область кода, не относящаяся непосредственно к методу сборки виджета.

Использование поставщика

Вот как я использую поставщика:

  1. При просмотре / прослушивании значений Провайдера , Consumer в целом и Selector за Придирчиво к тому, когда вызывать перестройку виджета по соображениям производительности, когда у вас много обновлений прослушивания провайдера по разным причинам, и вы просто хотите перестроить дерево виджетов по одной конкретной причине. Эти методы для прослушивания изменений более универсальны: делают более понятным, какой блок виджетов перестраивается, а также позволяют получить доступ к Provider без BuildContext, например, из StatelessWidget или некоторому вспомогательному методу StatefulWidget, который не имеет ссылки на BuildContext.
  2. при чтении / доступе к значениям поставщика без заботы об уведомлениях / обновлениях / изменениях в них , Затем используйте Provider.of<T>(context, listen: false)
  3. Когда используя / вызывая Услуги провайдера / методы и не значения , используйте Provider.of<T>(context, listen: false).myMethod() Например, Provider.of<RestaurantsData>(context, listen: false).addRestaurant(), так как в большинстве случаев вам не нужно слушать Provider обновлений в этом случае.

Ссылки по теме

...