通过 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
的新更新。即,现在我们将使用 read
和 watch
【参考方案1】:
请自行或通过我下面的解释来理解解决方案。不要在不理解的情况下使用我的答案。虽然这是一个简单的标志,你可以指定/翻转,但理解它是为什么使用 Provider 的核心。
新解决方案
在您的 _getCurrentLocation
方法中,假设已更新到最新的 Provider
pub 版本。变化:
Provider.of<RestaurantsData>(context).addRestaurant();
context.watch<RestaurantsData>().addRestaurant();
到
Provider.of<RestaurantsData>(context, listen: false).addRestaurant();
context.read<RestaurantsData>().addRestaurant();
与旧版本相关的旧解决方案平行绘制,read
与listen: false
的作用相同。要么用于修复由watch
与listen: 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<RestaurantsData>(context)
发生这种情况是因为addPostFrameCallback
导致其参数回调在您的小部件树之外被调用。后一个回调封装了 _getCurrentLocation
本地函数。反过来,此函数具有 Provider 实例调用。这一系列事件导致提供程序调用侦听小部件树之外的更新。
在小部件树之外收听通知更新是错误的,例如用户操作回调或initState
。
要解决此问题,您需要在小部件树之外的代码范围内将 listen
标志分配给其非默认值 false
。例如initState
或用户交互回调或任何不在小部件构建方法下的代码范围。
提供者使用
这就是我使用提供者的方式:
-
当观看/倾听提供者的价值观时,
Consumer
和Selector
对何时引起的挑剔/选择性当您出于不同原因有很多 Provider 侦听更新并且您只想出于一个特定原因重建小部件树时,出于性能原因重建小部件。这些监听变化的方法更加通用:更清楚哪些小部件块正在重建,也可以在没有BuildContext
的情况下访问Provider
,例如来自StatelessWidget
或StatefulWidget
的一些辅助方法,它没有对BuildContext
的引用。
阅读/访问提供者的值时不关心通知/更新/更改。然后使用Provider.of<T>(context, listen: false)
当使用/调用提供者的服务/方法和非值时,使用Provider.of<T>(context, listen: false).myMethod()
例如Provider.of<RestaurantsData>(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<RestaurantsData>()
代替Provider.of
是的,当时没用过,一直在等这个。
相应地更新了我的答案。以上是关于通过 addPostFrameCallback 访问 Flutter Provider 时说小部件在小部件树之外,但颤振检查器显示其他情况的主要内容,如果未能解决你的问题,请参考以下文章
机器学习访存密集计算编译优化框架AStitch,大幅提升任务执行效率