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&lt;User&gt;(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 = (_,__) =&gt; 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 定义为 但没有帮助。然后关于创建新集合的事情肯定会起作用,但它实际上并不能解决问题(我宁愿不创建新集合只是因为我无法完成这项工作。我还可以将每个统计字段定义为***字段用户对象(当然它也可以工作),但我们仍然不知道为什么会发生这种情况,对吗? 我只是不知道为什么 Provider.of 在一个地方为我提供了整个正确的对象,但在小部件树中向下两步的小部件中却没有(其中不完整)。跨度> 因为您需要在该小部件@Jorge 中再次声明提供程序,无论您希望数据出现在哪里,您都需要在小部件中重新声明提供程序,否则它不会重建该小部件。跨度> @Jorge 就地图而言,它需要像 Map 一样声明。这就是在数据库中设置统计信息的方式。它不仅仅是 Map stats 它是 Map stats。对统计信息进行此更改后的新错误是什么? 1) 当您说“声明提供者”时,您的意思是在小部件中调用 Provider.of(context),不是吗?因为我确实在需要对象的任何地方(在 build() 方法中)调用该指令。在上面的小部件中,整个对象到达(好)但在其他嵌套小部件中它到达不完整。这种差异让我很恼火。 2) 当我将 Map 声明为 时,什么也没发生。没有抛出错误,只是地图仍然不完整地返回。

以上是关于Flutter:StreamProvider 的奇怪行为,使用不完整数据重建的小部件的主要内容,如果未能解决你的问题,请参考以下文章

Flutter:如何处理使用 StreamProvider、FireStore 和使用 Drawer

Flutter Riverpod:使用 StreamProvider 返回 2 个流

Flutter:StreamProvider 的奇怪行为,使用不完整数据重建的小部件

Flutter Provider,当他的参数改变时更新 StreamProvider

如何在 Flutter (Dart) 中通过新路由获取子级 StreamProvider 数据

Flutter:带有 onAuthStateChanged 的​​ StreamProvider<FirebaseUser> 始终返回 null 作为第一个值