Flutter:StreamProvider 的奇怪行为,使用不完整数据重建的小部件
Posted
技术标签:
【中文标题】Flutter:StreamProvider 的奇怪行为,使用不完整数据重建的小部件【英文标题】:Flutter: Strange behavour of StreamProvider, widgets rebuilt with incomplete data 【发布时间】:2020-12-18 17:23:57 【问题描述】:我从 StreamProvider 获得不完整的数据。
下一个最小的小部件树重现了我的问题:选项卡式屏幕上的 SreamProvider。
我的用户对象包含多个值(以及其他属性)的映射,我通过在build()
方法中调用final user = Provider.of<User>(context);
将这些值用于选项卡式视图的一个屏幕,以便在此屏幕中获取这些值应用程序启动或完全重建(即热重载)。
问题:每当我切换选项卡并返回此选项卡时,build()
方法仅被调用一次,final user = Provider.of<User>(context);
返回一个不完整的用户副本:缺少一些数据(一些User 对象内映射的属性)和 build()
永远不会再次调用来完成数据(假设 StreamProvider 应该在某个时候返回完整的对象,可能会导致一些重建。结果:一些数据丢失和一些小部件无法构建屏幕。
class Wrapper extends StatelessWidget
@override
Widget build(BuildContext context)
final firebaseUser = Provider.of<FirebaseUser>(context);
// return either Home or Authenticate widget:
if (firebaseUser == null)
return WelcomeScreen();
else
return StreamProvider<User>.value(
value: FirestoreService(uid: firebaseUser.uid).user,
child: TabsWrapper(),
);
class TabsWrapper extends StatefulWidget
final int initialIndex;
TabsWrapper(this.initialIndex: 0);
@override
_TabsWrapperState createState() => _TabsWrapperState();
class _TabsWrapperState extends State<TabsWrapper> with TickerProviderStateMixin
TabController tabController;
@override
void initState()
super.initState();
tabController = TabController(vsync: this, length: choices.length);
@override
Widget build(BuildContext context)
return DefaultTabController(
initialIndex: widget.initialIndex,
length: 3,
child: Scaffold(
backgroundColor: lightBlue,
bottomNavigationBar: TabBar(
controller: tabController,
labelStyle: navi.copyWith(color: darkBlue),
unselectedLabelColor: darkBlue,
labelColor: darkBlue,
indicatorColor: darkBlue,
tabs: choices.map((TabScreen choice)
return Tab(
text: choice.title,
icon: Icon(choice.icon, color: darkBlue),
);
).toList(),
),
body: TabBarView(
controller: tabController,
children: <Widget>[
FirstScreen(),
SecondScreen(),
ThirdScreen(),
],
),
),
);
有问题的屏幕(FirstScreen):
class FirstScreen extends StatelessWidget
final TabController tabController;
final User user;
FirstScreen ();
@override
Widget build(BuildContext context)
final user = Provider.of<User>(context);
print('**************************************** $user.stats'); //Here I can see the problem
return SomeBigWidget(user: user);
print($user.stats) 显示不完整的地图(统计信息),build()
再也不会调用(带有剩余数据),因此 User 对象仍然不完整。在重新加载或启动应用程序时,它只会被调用两次(并且数据与完整对象一起返回)。
欢迎任何解决方案!
PD:我找到了一种更简单的方法来重现这种情况。无需更改标签:
在FirstScreen()
里面我有一列StatelessWidgets。如果我在其中一个中调用Provider.of<User>(context)
,我会得到该对象的不完整版本。 user.stats
映射具有通过 Provider.of 上述某些小部件访问它时所具有的键值对的一半。
这就是从 Firebase 更新流的方式。 (每次都会创建对象):
Stream<User> get user
Stream<User> userStream = usersCollection.document(uid).snapshots().map((s) => User.fromSnapshot(s));
return userStream;
我也有updateShouldNotify = (_,__) => true;
用户模型:
class User
String uid;
String name;
String country;
Map stats;
User.fromUID(@required this.uid);
User.fromSnapshot(DocumentSnapshot snapshot)
: uid = snapshot.documentID,
name = snapshot.data['name'] ?? '',
country = snapshot.data['country']
try
stats = snapshot.data['stats'] ?? ;
catch (e)
print('[User.fromSnapshot] Exception building user from snapshot');
print(e);
这是 Firestore 中的统计数据映射:
【问题讨论】:
这是一个奇怪的行为,正如你所说,无状态小部件只调用一次 build 方法,除非感兴趣的值(提供者)强制它重建,你在更改选项卡时调用或更改 firebaseUser 或 User 提供者吗? 一点也不。如果我在 Firebase 中进行任何更改,受影响的小部件会使用新数据正确重建 这是一个理论,因为我看不到用户模型是如何构建的,或者你如何通过流发出新值,但如果你说用户不完整,也许你正在使用相同的用户模型以前的 emmited 值并更改其属性?如果是这种情况,StreamProvider 将不会更新其侦听器,因为它会检查 updateShouldNotify 是否前一个值和新值是相同的对象(== 操作) 我会将这个添加到问题中。该模型来自 Firebase,并且每次都会创建新的对象。还有 updateShouldNotify = (,_) => true 您是否尝试过在调试模式下检查(provider.of 中的断点来检查用户模型),我不知道您是仅依赖打印还是截断统计信息因为它的长度 【参考方案1】:虽然 firebase 确实允许您使用地图作为供您使用的类型,但在这种情况下,我认为这不是您的最佳选择,因为它似乎令人困惑。
您的问题有多种解决方案。
我认为最好的方法是与您的用户集合并排创建一个名为“stats”的集合,并让每个 stat 的 docid = 具有该统计信息的用户的 userid,这使您将来更容易查询。
在此之后,您可以使用您已经使用的方法来获取统计信息,就像获取用户一样。
class Stat
String statid;
int avg;
int avg_score;
List<int> last_day;
int n_samples;
int total_score;
int words;
Stat.fromSnapshot(DocumentSnapshot snapshot)
statid = snapshot.documentID,
avg = snapshot.data['average'] ?? '',
//rest of data
也许您需要的简单解决方案是将 User 类中的地图更改为此。
Map<String, dynamic> stats = Map<String, dynamic>();
删除 try 和 catch 块,那么您应该能够像这样访问您的数据user.stats['average'];
希望这会有所帮助,当我可以在模拟器中实际进行测试时,我会更新我的答案,但如果你尝试它并且它有效,请告诉我。
【讨论】:
您好,首先,非常感谢您的帮助。我尝试首先将 Map 定义为以上是关于Flutter:StreamProvider 的奇怪行为,使用不完整数据重建的小部件的主要内容,如果未能解决你的问题,请参考以下文章
Flutter:如何处理使用 StreamProvider、FireStore 和使用 Drawer
Flutter Riverpod:使用 StreamProvider 返回 2 个流
Flutter:StreamProvider 的奇怪行为,使用不完整数据重建的小部件
Flutter Provider,当他的参数改变时更新 StreamProvider
如何在 Flutter (Dart) 中通过新路由获取子级 StreamProvider 数据
Flutter:带有 onAuthStateChanged 的 StreamProvider<FirebaseUser> 始终返回 null 作为第一个值