通过 addPostFrameCallback 访问 Flutter Provider 时说小部件在小部件树之外,但颤振检查器显示其他情况

Posted

技术标签:

【中文标题】通过 addPostFrameCallback 访问 Flutter Provider 时说小部件在小部件树之外,但颤振检查器显示其他情况【英文标题】:Flutter Provider access via addPostFrameCallback says widget is outside the widget tree but flutter inspector shows otherwise 【发布时间】:2020-05-10 21:20:05 【问题描述】:

我正在 Flutter 中构建我的第一个大型应用程序,也是第一个需要状态管理的应用程序,因此我求助于 Provider,这是用于状态管理的推荐包。但是我遇到了一些问题,我在 main.dart 文件中声明了我的 Providers,我想在树中进行更改并与其中一个 Providers 交互,但无论我尝试什么解决方案,我都会收到相同的错误:“尝试过从小部件树外部侦听提供者公开的值。”。即使根据颤振检查器,我尝试更改提供程序的小部件位于小部件树内部(“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,调用 getLocation 函数而不使用“WidgetsBinding.instance.addPostFrameCallback(() => _getCurrentLocation(context));”线。在我尝试过的其他解决方案中,我尝试过不将上下文传递给函数。

非常感谢您的帮助,并提前感谢您。如果您对代码有任何疑问,我将非常乐意为您解答。

【问题讨论】:

希望我的回答解决了您的问题并解释了其根本原因。如果是,请将其标记为已接受。您的问题包含大量与问题相关的信息。如果这是一个更复杂的问题,也许包括每个使用过的 Provider 的代码会更好。 非常感谢您的回答。下次我会记住包括我的模型以提供更清晰的图片。 作为记录@Diego,该解决方案现已更新以反映Provider 的新更新。即,现在我们将使用 readwatch 【参考方案1】:

请自行或通过我下面的解释来理解解决方案。不要在不理解的情况下使用我的答案。虽然这是一个简单的标志,你可以指定/翻转,但理解它是为什么使用 Provider 的核心。

新解决方案

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

Provider.of&lt;RestaurantsData&gt;(context).addRestaurant(); context.watch&lt;RestaurantsData&gt;().addRestaurant();

Provider.of&lt;RestaurantsData&gt;(context, listen: false).addRestaurant(); context.read&lt;RestaurantsData&gt;().addRestaurant();

与旧版本相关的旧解决方案平行绘制,readlisten: false 的作用相同。要么用于修复由watchlisten: true 扮演相同角色而导致的OP 异常。 重要说明可以在here 和here 找到。感谢用户 Vinoth Vino 通过他的 comment 提醒这一新变化。


旧解决方案

在您的 _getCurrentLocation 方法中,更改

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

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

说明

如错误所示

试图从小部件树的外部侦听提供者公开的值。

您正在从小部件树外部的 Provider 实例获取通知更新。即您的 Provider 实例正在调用 Provider 方法 NotifyListeners() ,该方法将更新发送到所有侦听器。您问题中的这个特定调用正在收听这些更新,即:Provider.of&lt;RestaurantsData&gt;(context)

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

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

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

提供者使用

这就是我使用提供者的方式:

    观看/倾听提供者的价值观时,ConsumerSelector 对何时引起的挑剔/选择性当您出于不同原因有很多 Provider 侦听更新并且您只想出于一个特定原因重建小部件树时,出于性能原因重建小部件。这些监听变化的方法更加通用:更清楚哪些小部件块正在重建,也可以在没有BuildContext 的情况下访问Provider,例如来自StatelessWidgetStatefulWidget 的一些辅助方法,它没有对BuildContext 的引用。 阅读/访问提供者的时不关心通知/更新/更改。然后使用Provider.of&lt;T&gt;(context, listen: false)使用/调用提供者的服务/方法非值时,使用Provider.of&lt;T&gt;(context, listen: false).myMethod() 例如Provider.of&lt;RestaurantsData&gt;(context, listen: false).addRestaurant(),因为在这种情况下,大多数时候您不需要收听 Provider 的更新。

相关参考资料

要进一步了解listen 标志行为 以及异常背后的原因,请查看GitHub docs here 和source code docs。如果您真的感兴趣,请查看this GitHub discussion。

要了解listen 标志默认值,请查看这些作者的问题cmets here 和here。

【讨论】:

从Provider v4.1.0开始,现在也可以使用context.read&lt;RestaurantsData&gt;()代替Provider.of 是的,当时没用过,一直在等这个。 相应地更新了我的答案。

以上是关于通过 addPostFrameCallback 访问 Flutter Provider 时说小部件在小部件树之外,但颤振检查器显示其他情况的主要内容,如果未能解决你的问题,请参考以下文章

机器学习访存密集计算编译优化框架AStitch,大幅提升任务执行效率

使用iptables基于MAC地址进行访控

DBA札记 | Global DBA日本访学归来,你会明白读博其实很精彩

Yolov5 计算访存量MAC与计算量FLOPS

智能访客机操作简单

mac cmd 打开访达