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

1

2

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). Widget tree according to the Flutter inspector

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.

Middleman answered 24/1, 2020 at 14:19 Comment(3)
Hope my answer fixed your problem and explained its root cause. If so please mark it as accepted. Your question has a good amount of problem-relevant information. Maybe including each of the used Provider's code would have been better if this had been an even more complicated problem.Luxate
Thank you so much for your answer. I will keep in mind to include my models next time for providing a clearer picture.Middleman
For the record @Diego, the solution is now updated to reflect Provider's new update. i.e, now we'd use read and watchLuxate
L
16

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.

New Solution

In your _getCurrentLocation method, which is hypothetically updated to the latest Provider pub version. Change:

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

TO

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

Drawing parallel to the old solution related to the old verison, read plays the same role as listen: false. Either is used to fix the OP's exception that's caused by watch playing the same role as listen: true. Important explanation on this can be found here and here. Thanks to user Vinoth Vino for alerting this new change via his comment.


Old 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

Luxate answered 24/1, 2020 at 16:25 Comment(3)
From Provider v4.1.0, Now you can also use context.read<RestaurantsData>() instead of Provider.ofScuba
Yep this wasn't used back then, have been waiting on this.Luxate
Updated my answer accordingly.Luxate

© 2022 - 2024 — McMap. All rights reserved.