StreamBuilder - Bad state: Use multiple StreamBuilder on one screen - flutter

Since I use multiple StreamBuilder in my screen I get a Bad state error.
I know that I have to use a StreamController and use it with .broadcast().
Because I dont create the streams by myself I dont know how to change the controller of these streams.
This is my code:
class MyScreen extends StatefulWidget {
#override
_MyScreenState createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Column(
children: [
StreamBuilder<List<int>>(
stream: streamOne?.value,
builder: (c, snapshot) {
final newValueOne = snapshot.data;
return Text(newValueOne);
}),
StreamBuilder<List<int>>(
stream: streamTwo?.value,
builder: (c, snapshot) {
final newValueTwo = snapshot.data;
return Text(newValueTwo);
}),
StreamBuilder<List<int>>(
stream: streamThree?.value,
builder: (c, snapshot) {
final newValueThree = snapshot.data;
return Text(newValueThree);
}),
],
),
),
);
}
}
I tried to have it as BroadcastStreams:
class MyScreen extends StatefulWidget {
#override
_MyScreenState createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Column(
children: [
StreamBuilder<List<int>>(
stream: streamOne?.asBroadcastStream(),
builder: (c, snapshot) {
final newValueOne = snapshot.data;
return Text(newValueOne);
}),
StreamBuilder<List<int>>(
stream: streamTwo?.asBroadcastStream(),
builder: (c, snapshot) {
final newValueTwo = snapshot.data;
return Text(newValueTwo);
}),
StreamBuilder<List<int>>(
stream: streamThree?.asBroadcastStream(),
builder: (c, snapshot) {
final newValueThree = snapshot.data;
return Text(newValueThree);
}),
],
),
),
);
}
}
This didnt work and gave me still a bad state error.
Would be great if somone could help me here.
Thank you very much!

Inside your streamBuilder builder, you have to check that the snapshot has actually received the data, otherwise your Text widget is receiving null, thus, throwing a bad state error:
StreamBuilder<List<int>>(
stream: streamThree.asBroadcastStream(),
builder: (c, snapshot) {
if(snapshot.hasData){
final newValueThree = snapshot.data;
return Text(newValueThree);
} else {
// return any other widget like CircularProgressIndicator
}
}),
You can also check on
snpashot.connectionState == ConnectionState.done
and
snpashot.connectionState == ConnectionState.active
and
snpashot.connectionState == ConnectionState.waiting

Thank you #Arnaud Delubac. I also had to check if the array I get from the stream is not empty:
StreamBuilder<List<int>>(
stream: streamThree.asBroadcastStream(),
builder: (c, snapshot) {
if (snapshot.hasData && snapshot.data.isNotEmpty && snapshot.connectionState == ConnectionState.active) {
final newValueThree = snapshot.data;
return Text(newValueThree);
} else {
// return any other widget like CircularProgressIndicator
}
}),

Related

displaying data from different firestore collections

I'm attempting display data from two diffrent collections within firestore , I treied to nest both streambuilds so i can particulary display the data as one stream , however I keep on getting the error bad state field doesnt exist with doc snapshot how can i fixing thus error , or is there another much more effective method i can use to display data from two diffrent collections in one class?
below is screenshot of the data(s) i want to display:
class OrderStream extends StatelessWidget {
static const String route = "/Order";
final CollectionReference meal =
FirebaseFirestore.instance.collection("menu");
final CollectionReference profile =
FirebaseFirestore.instance.collection("users");
OrderStream({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder(
stream: profile.snapshots(),
builder: (context, AsyncSnapshot<QuerySnapshot> streamSnapshot) {
return StreamBuilder(
stream: meal.snapshots(),
builder:
(context, AsyncSnapshot<QuerySnapshot> streamSnapshot) {
if (!streamSnapshot.hasData) {
return const SizedBox(
height: 250,
child: Center(
child: CircularProgressIndicator(),
),
);
} else {
return ListView.builder(
itemCount: streamSnapshot.data!.docs.length,
itemBuilder: (context, index) {
final DocumentSnapshot documentSnapshot =
streamSnapshot.data!.docs[index];
return Column(
children: [
Text( documentSnapshot['price'],)
Text( documentSnapshot['name'],)
]
),
),
}
This is probably happening due to similar name for both snapshots.
The best way to check this is by renaming the snapshot for individual Streambuilder().
StreamBuilder(
stream: profile.snapshots(),
builder: (context, AsyncSnapshot<QuerySnapshot> profileStreamSnapshot) {
return StreamBuilder(
stream: meal.snapshots(),
builder:
(context, AsyncSnapshot<QuerySnapshot> mealStreamSnapshot) {
if (!streamSnapshot.hasData) {
//modified (renamed snapshot variable) code here
}
You can merge those two streams into 1 using library like rxdart which has combineLatest2 method although you can also use something like StreamZip to get the same effect.
I have used rxdart combineLatest2 as follows:
import 'package:rxdart/rxdart.dart';//import ⇐
class MyHomePage extends StatelessWidget {
final CollectionReference profile =
FirebaseFirestore.instance.collection("users");
final CollectionReference meal =
FirebaseFirestore.instance.collection("menu");
MyHomePage({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder(
stream: Rx.combineLatest2(profile.snapshots(), meal.snapshots(),
(QuerySnapshot profileSnapshot, QuerySnapshot mealSnapshot) {
return [...profileSnapshot.docs, ...mealSnapshot.docs];
}),
builder: (context, AsyncSnapshot<List<DocumentSnapshot>> snapshot) {
if (!snapshot.hasData) {
return const SizedBox(
height: 250,
child: Center(
child: CircularProgressIndicator(),
),
);
} else {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
final DocumentSnapshot documentSnapshot =
snapshot.data![index];
final Map<String, dynamic> data =
documentSnapshot.data() as Map<String, dynamic>;
if (data.containsKey("price") && data.containsKey("name")) {
return Column(
children: [Text(data["price"]), Text(data["name"])],
);
} else {
return Container();
}
},
);
}
}),
);
}
}
You can also use Stream.merge() as follows:
final Stream<QuerySnapshot> mealsStream = meal.snapshots();
final Stream<QuerySnapshot> profilesStream = profile.snapshots();
//.. All that Scaffold stuff
stream: Stream.merge([mealsStream, profilesStream]),

Multiple StreamBuilder that using same stream work incorrectly?

Let say I have code like this
class ScreenA extends StatelessWidget {
const ScreenA();
#override
Widget build(BuildContext context) {
final Stream<List<Order>> ordersStream = Order.stream;
return Column(
children: [
StreamBuilder(
stream: ordersStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
return Text(snapshot.data!.toString());
} else {
return const LoadingCircle();
}
},
),
TextButton(
child: Text('to ScreenB'),
onPressed: () => MaterialPageRoute(
builder: (context) =>
ScreenB(ordersStream), // pass stream to another screen
),
),
],
);
}
}
class ScreenB extends StatelessWidget {
const ScreenB(this.ordersStream);
final Stream<List<Order>> ordersStream;
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: ordersStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
return Text(snapshot.data!.toString());
} else {
return const LoadingCircle();
}
},
);
}
}
This seem to work, but when ScreenA already got data, in ScreenB snapshot.ConnectionState will mark as ConnectionState.waiting until ordersStream emit new value (It should get data as same as ScreenA).
For example;
at second 0: created ordersStream
at second 1: ordersStream emit value [Order1,Order2]
at second 5: I press button to go to ScreenB <- ordersStream in ScreenB should get [Order1,Order2] too, but it didn't give any value and has state ConnectionState.waiting.
at second 10: ordersStream emit value [Order3,Order4] <- ordersStream in ScreenB get value now.
I tried using StreamProvider, but it doesn't fit my code project, how to make this work correctly without StreamProvider?
Try adding the stream key inside the builders.
StreamBuilder(
stream: ordersStream, //add stream here
builder: (BuildContext context, snapshot) {
return Container();
}
)
Edit
You can pass the current data along with the stream and in the second screen you can set the initial data with the data that you recieved from screen 1
StreamBuilder(
initialData: YourCurrentDataFromScreen1,
stream: streamHere,
builder: (BuildContext context, snapshot) {
return Container();
}
}
This is how broadcast stream works.
You can start listening to such a stream at any time, and you get the events that are fired while you listen.
See Broadcast streams.

how to migrate snapshot.data.data(); to null safety

this is the code can any help me how to change this to null safety, I am getting errors in .data(). i changed my project to null-safety, and then i am facing this issue
this is the error
The method 'data' can't be unconditionally invoked because the receiver can be 'null'.
Try making the call conditional (using '?.') or adding a null check to the target ('!').
Map<String, dynamic>? documentData = snapshot.data.data();
class HomeSlider extends StatefulWidget {
final String? doc_id;
HomeSlider({this.doc_id});
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<HomeSlider> {
FirebaseServices _firebaseServices = FirebaseServices();
int activeIndex = 1;
#override
Widget build(BuildContext context) {
super.build(context);
return Container(
// height: 200,
child: FutureBuilder(
future: _firebaseServices.sliderRef
.doc(widget.doc_id == null ? "Slider" : widget.doc_id)
.get(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text("Error: ${snapshot.error}"),
);
}
if (snapshot.connectionState == ConnectionState.done) {
// the error is here in data()
// Firebase Document Data Map
Map<String, dynamic> documentData = snapshot.data.data();
List? imageList = documentData['images'];
List? suid = documentData['suid'];
return SliderBody(
imageList: imageList,
suid: suid,
);
}
return Center(
child: CircularProgressIndicator(),
);
}));
}}
you should go with
Map<String, dynamic>? documentData = snapshot!.data.data();
The problem was in builder: (context, snapshot) { after adding the AsyncSnapshot and finally like this. builder: (context, AsyncSnapshot snapshot) { and also add ! before .data()
class HomeSlider extends StatefulWidget {
final String? doc_id;
HomeSlider({this.doc_id});
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<HomeSlider> {
FirebaseServices _firebaseServices = FirebaseServices();
int activeIndex = 1;
#override
Widget build(BuildContext context) {
super.build(context);
return Container(
// height: 200,
child: FutureBuilder(
future: _firebaseServices.sliderRef
.doc(widget.doc_id == null ? "Slider" : widget.doc_id)
.get(),
builder: (context,AsyncSnapshot snapshot) {
if (snapshot.hasError) {
return Center(
child: Text("Error: ${snapshot.error}"),
);
}
if (snapshot.connectionState == ConnectionState.done) {
// the error is here in data()
// Firebase Document Data Map
Map<String, dynamic> documentData = snapshot.data!.data();
List? imageList = documentData['images'];
List? suid = documentData['suid'];
return SliderBody(
imageList: imageList,
suid: suid,
);
}
return Center(
child: CircularProgressIndicator(),
);
}));
}}

How to get result of FutureBuilder from the parent FutureBuilder?

The entry point is _processError function. I expect to get a widdet there. And this _processError runs from a parent FutureBuilder.
Then another Future builder should be executed, at least I think it should... But it seems there is no result from there. Whats wrong with it?
FutureBuilder<List<ShortLetter>>(
future: fetchMessages(),
builder: (BuildContext context, AsyncSnapshot<List<ShortLetter>> snapshot) {
...
} else if (snapshot.hasError) {
return _processError(snapshot, context); // I want to get a widget when an error happens
...
},
);
Future<bool> checkConnection() async {
debugPrint('---checkConnection---');
var connectivityResult = await (Connectivity().checkConnectivity());
...
// and returs true or false
}
Widget _processError(AsyncSnapshot snapshot, BuildContext context) {
var errorType = snapshot.error.runtimeType;
debugPrint('AllMessagesView, snapshot error: $errorType');
debugPrint(snapshot.error.toString());
if (errorType == TimeoutException) {
debugPrint('0000000000000000');
//////////////////////////////////////////////////////
// there is any output in console from the FutureBuilder below
// but checkConnection() was executed
FutureBuilder<bool>(
future: checkConnection(),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
if (snapshot.hasData) {
debugPrint('11111111111111 snapshot data: ${snapshot.data}');
if (snapshot.data == true) {
...
}
...
} else if (snapshot.hasError) {
debugPrint('2222222222222');
...
} else {
debugPrint('Error. This should not happen.');
...
}
},
);
...
}
...
}
here is a sample console output and any result from the second FutureBuilder
I/flutter (10556): AllMessagesView, snapshot error: TimeoutException
I/flutter (10556): TimeoutException after 0:00:10.000000: Future not completed
I/flutter (10556): 0000000000000000
I/flutter (10556): ---checkConnection---
Parent FutureBuilder is already been processed, I think we don't need to pass Async data.
This demo widget may help.
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Future<int> parentF() async {
return await Future.delayed(Duration(seconds: 2), () => 4);
}
Future<String> childF(int sec) async {
return await Future.delayed(Duration(seconds: sec), () => "got the child");
}
Widget childFB(final data) {
print(data.runtimeType);
return FutureBuilder(
future: childF(4),
builder: (context, snapshot) => snapshot.hasData
? Text("${snapshot.data!} parent data: $data ")
: const Text("loading second child"));
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
FutureBuilder(
future: parentF(),
builder: (context, parentSnapshot) {
return parentSnapshot.hasData
? FutureBuilder<String>(
future: childF(3),
builder: (context, snapshot) {
return snapshot.hasData
? Text(
"${snapshot.data!} parent data: ${parentSnapshot.data} ")
: const Text("loading child");
},
)
: const Text("loading parent");
},
),
FutureBuilder(
future: parentF(),
builder: (context, parentSnapshot) {
return parentSnapshot.hasData
? childFB(parentSnapshot
.data) // it already have normal data, not async
: const Text("loading parent");
},
),
],
));
}
}

Infinite loop FutureBuilder when use Provider

I have method like this in my provider file to get a list of items :
Future<void> getServerMeals() async {
final QueryBuilder<ParseObject> parseQuery =
QueryBuilder<ParseObject>(ParseObject('UsersEaten'));
final ParseResponse apiResponse = await parseQuery.query();
if (apiResponse.success && apiResponse.results != null) {
List<dynamic>? apiRes = apiResponse.results;
List<EatenItem> newMeals = apiRes!
.map((e) => EatenItem(
id: e['objectId'],
eatenCal: e['eatenCal'],
eatenTitle: e['eatenTitle'],
eatenImageUrl: e['eatenImg'],
userId: e['objectId'],
))
.toList();
_eatenMeals = newMeals;
print(apiResponse.results);
notifyListeners();
}
}
and in my main screen I have a StatelessWidget and FutureBuilder to get that list :
class TimelineWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
child: FutureBuilder(
future: Provider.of<EatenMeals>(context, listen: false).getServerMeals(),
builder: (ctx, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
return Consumer<EatenMeals>(
builder: (ctx, mealsList, child) => ListView.builder(
itemCount: mealsList.eatenMeals.length,
itemBuilder: (ctx, index) =>
DailyEaten(mealsList.eatenMeals[index]),
),
);
},
),
),
],
);
}
as you can see there is nothing in my build method and I've tried to use Consumer as future to avoid infinite loop but it doesn't work for me please tell me what's wrong here I appreciate if u explain with code snippet