Flutter Provider access via addPostFrameCallback says widget is outside the widget tree but flutter inspector shows otherwise

会有一股神秘感。 提交于 2020-02-25 05:32:50

问题


I am building my first big app in Flutter, and the first one where I need State Management, so I turned to Provider which is the recommended package to use for State Management. However I am having some issues where I declare my Providers in the main.dart file and down the tree I want to make changes and interact with one of the Providers but no matter what solution I try, I keep getting the same error: "Tried to listen to a value exposed with provider, from outside of the widget tree.". I get this error even though according the flutter inspector, the widget from where I am trying to make changes to the provider is inside of the widget tree (the "HomeScreen" screen is from where I am updating the provider).

Below I also share my code: 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(),
            ),
          ),
        ],
      ),
    );
  }
}

The thing causing the problem in the home_screen file, as far as I can tell, is the "getCurrentLocation(BuildContext context){}" function, and how and when I call it. I have tried turning everything into statelessWidgets, calling the getLocation funtion without the "WidgetsBinding.instance.addPostFrameCallback(() => _getCurrentLocation(context));" line. I have tried not passing the context to the function, among other solutions that I have tried.

I really appreciate your help and I would like to thank you in advance. If you have any doubts regarding the code I will be more than happy to answer all of them.


回答1:


Please understand the solution either on your own or via my explanation below. Don't just use my answer without understanding it. Although this is a simple flag you can just specify/flip, understanding it is the core of why Provider is even used.

Solution

In your _getCurrentLocation method, change

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

to

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

Explanation

As the error illustrates

Tried to listen to a value exposed with provider, from outside of the widget tree.

You're getting notification update from your Provider instance from outside the widget tree. i.e. your Provider instance is calling Provider method NotifyListeners() which sends updates to all listeners. And this particular invocation in your question is listening to those updates, which is: Provider.of<RestaurantsData>(context)

This is happening because addPostFrameCallback is causing its parameter callback to be called outside your widget tree. This latter callback is encapsulating _getCurrentLocation local function. In turn this function has the Provider instance invocation. This sequence of events led the provider invocation to listen to updates outside the widget tree.

It's erroneous to listen to notification updates outside your widget tree e.g. user-action callbacks or initState.

To fix this issue, you need to assign listen flag to its non-default value false in code scopes outside your widget tree. e.g. initState or user-interaction callbacks or any code scope not directly under the widget's build method.

Provider Usage

This is how I use provider:

  1. When watching/listening to Provider's values, Consumer in general and Selector for being picky/selective about when to cause a widget rebuild for performance reasons when you have a lot of Provider listen updates for different reasons and you just want to rebuild your widget tree for one particular reason. These methods for listening to changes are more versatile: makes it more clear which block of widgets are being rebuilt and also makes it's possible to access Provider without BuildContext e.g. from StatelessWidget or some helper method of a StatefulWidget that does not have a reference to BuildContext.
  2. When reading/accessing Provider's values without caring about notifications/updates/changes to them. Then use Provider.of<T>(context, listen: false)
  3. When using/calling Provider's services/methods and not values, use Provider.of<T>(context, listen: false).myMethod() e.g. Provider.of<RestaurantsData>(context, listen: false).addRestaurant() since most of the time you don't need to listen to Provider updates in this case.

Related References

  • To further understand listen flag behavior and the reasoning behind your exception, check out the GitHub docs here and source code docs. If you're REALLY interested, check this GitHub discussion.

  • To understand listen flag default value, check these author's issue comments here and here.



来源:https://stackoverflow.com/questions/59898274/flutter-provider-access-via-addpostframecallback-says-widget-is-outside-the-widg

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!