I am getting incomplete data from a StreamProvider.
The next minimum widget tree reproduces my problem: A SreamProvider over a Tabbed Screen.
My User object contains a map of several values (among other properties) and I use these values for one screen of the Tabbed View by calling final user = Provider.of<User>(context); inside the build() method in order to get these values in this screen whenever the app starts or is completely rebuilt (i.e. hot reload).
The problem: Whenever I switch tabs and go back to this tab, the build() method is called only once with final user = Provider.of<User>(context); returning an incomplete copy of User: some data is missing (some properties of a map inside the User object) and build() is never called again to complete the data (given that the StreamProvider should return the complete object at some point, possibly causing some rebuilds. The result: some data is missing and some widgets of the Screen cannot be built.
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(),
],
),
),
);
}
}
The problematic Screen (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);
}
}
The print(${user.stats}) shows the incomplete map (stats) and the build() is never called again (with the remaining data) so the User object remains incomplete. It is only called twice (and the data returned with the complete object) when reloading or launching the app.
Any solution will be welcome!
PD: I found a simpler way to reproduce the situation. No need to change tabs:
Inside FirstScreen() I have a column of StatelessWidgets. If I call Provider.of<User>(context) within one of them I get an incomplete version of the object. The user.stats map has half of the key-value pairs that it has when accesing it through Provider.of some widgets above.
This is how the stream is updated from Firebase. (the object is created every time):
Stream<User> get user {
Stream<User> userStream = usersCollection.document(uid).snapshots().map((s) => User.fromSnapshot(s));
return userStream;
}
I also have updateShouldNotify = (_,__) => true;
The User model:
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);
}
}
}
