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

问题描述

我从StreamProvider获取的数据不完整。

一个最小的小部件树重现了我的问题:选项卡式屏幕上的SreamProvider。

“我的用户”对象包含多个值的映射(以及其他属性),我通过在final user = Provider.of<User>(context);方法调用build()将这些值用于选项卡式视图的一个屏幕,以获取这些值在该屏幕上,只要应用程序启动或完全重建(即热重载)即可。

问题:每当我切换选项卡并返回此选项卡时,build()方法仅被调用一次,而final user = Provider.of<User>(context);返回的是用户的不完整副本:一些数据丢失(用户对象内映射的某些属性),并且再也不会调用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),则会得到该对象的不完整版本。通过上面的某些小部件的Provider。访问user.stats映射时,它具有键/值对的一半。

这是从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中的统计数据映射:

stats map

解决方法

尽管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 and catch块,那么您应该能够像这样user.stats['average'];

访问数据

希望这会有所帮助,当我可以将其实际在模拟器中进行测试时,我会更新我的答案,但是如果您尝试使用它并且可以让我知道。