通过 addPostFrameCallback 的 Flutter Provider 访问表示小部件在小部件树之外,但 flutter inspector 显示否则

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

我正在 Flutter 中构建我的第一个大型应用程序,也是第一个需要状态管理的应用程序,所以我求助于 Provider,这是推荐用于状态管理的软件包。但是,我遇到了一些问题,我在 main.dart 文件中声明了我的提供者,并沿着树向下我想进行更改并与其中一个提供者进行交互,但无论我尝试哪种解决方案,我都会收到相同的错误: "Tried to listen to a value exposed with provider, from outside of the widget tree."。即使根据 flutter 检查器,我收到此错误,我尝试更改提供者的小部件位于小部件树内部("HomeScreen" 屏幕来自我更新提供者的地方)。

下面我也分享一下我的代码: 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,在没有 "WidgetsBinding.instance.addPostFrameCallback(() => _getCurrentLocation(context));" 行的情况下调用 getLocation 函数。我尝试过不将上下文传递给函数,以及我尝试过的其他解决方案。

非常感谢您的帮助,在此先感谢您。如果您对代码有任何疑问,我将非常乐意回答所有问题。

请自行理解或通过我下面的解释来理解解决方案。不要在不理解的情况下就使用我的答案。虽然这是一个简单的标志,但您可以 specify/flip,理解它是使用 Provider 的核心。

新解决方案

在您的 _getCurrentLocation 方法中,假设已更新到最新的 Provider 发布版本。变化:

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

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

与旧版本相关的旧解决方案平行绘制,readlisten: false 的作用相同。 Either 用于修复由 watch 扮演与 listen: true 相同角色引起的 OP 异常。 重要 可以找到关于此的解释 here and here. Thanks to user Vinoth Vino for alerting this new change via his


旧解决方案

在您的 _getCurrentLocation 方法中,更改

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

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

说明

如错误所示

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

您正在从窗口小部件树外部的提供程序实例中获取通知更新。也就是说,您的 Provider 实例正在调用 Provider 方法 NotifyListeners(),该方法向所有侦听器发送更新。您问题中的这个特定调用正在监听这些更新,即:Provider.of<RestaurantsData>(context)

发生这种情况是因为 addPostFrameCallback 导致其参数回调在您的小部件树之外被调用。后一个回调封装了 _getCurrentLocation 局部函数。反过来,此函数具有 Provider 实例调用。这一系列事件导致提供程序调用侦听小部件树外部的更新。

在您的小部件树之外收听通知更新是错误的,例如用户操作回调或 initState.

要解决此问题,您需要在小部件树外部的代码范围内将 listen 标志分配给其非默认值 false。例如initState 或用户交互回调或不直接位于小部件构建方法下的任何代码范围。

提供商使用

这是我使用提供商的方式:

  1. 正在观看/正在收听提供商的时,Consumer in general and Selector picky/selective 当您出于不同原因有大量提供程序侦听更新并且您只想出于一个特定原因重建您的小部件树时,出于性能原因何时导致小部件重建。这些用于侦听更改的方法更加通用:使正在重建的小部件块更加清晰,并且还可以在没有 BuildContext 的情况下访问 Provider 例如来自 StatelessWidget 或没有引用 BuildContext.
  2. StatefulWidget 的一些辅助方法
  3. 阅读/访问提供商的而不关心notifications/updates/changes到他们。然后使用 Provider.of<T>(context, listen: false)
  4. using/calling提供商的服务/方法不是值,使用Provider.of<T>(context, listen: false).myMethod(),例如Provider.of<RestaurantsData>(context, listen: false).addRestaurant() 因为在这种情况下,大多数时候您不需要收听 Provider 更新。

相关参考资料