Multiple StreamBuilder that using same stream work incorrectly? - flutter

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.

Related

How to make Flutter Stream Builder return seamlessly

I have a little problem here where i have logged in with Google Auth using Firebase but everytime i tried to restart the app i expect the app will show the HomePage() without any problem, but i found that before it return, the app had like a bit seconds in LoginPage() before displaying HomePage(), is there any way to make it seamlessly
class AuthService extends StatelessWidget {
const AuthService({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return HomePage();
} else {
return LoginPage();
}
},
),
);
}
}
It is happening because for snapshot to reach snapshot.hasData state it takes time, and meanwhile else part is executed which is LoginPage().
How to overcome this?
Try to wrap within snapshot.connectionState == ConnectionState.active which means once stream is connected then check the condition else return CircularProgressIndicator
Code:
StreamBuilder(
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
if (snapshot.hasData) {
return HomePage();
} else {
return LoginPage();
}
}
return const CircularProgressIndicator();
},
);

Flutter: get bool value from snapshot

I am trying to show an on-boarding screen so to do so I call an async function where I get a Future<bool> weather this is the first time the user launched the app. Since its a Future value I have decided to use FutureBuilder.
Here is part of the code of what I am trying to do:
#override
Widget build(BuildContext context) {
return GetMaterialApp(
home: FutureBuilder(
future: firstLaunch, //Future<bool> value
builder: (context, snapshot) {
if (snapshot.hasData) {
// What I would like to do is use either one of these lines
// return _firstLaunch ? const OnboardingScreen() : const WelcomePage();
// return snapshot.data ? const OnboardingScreen() : const WelcomePage();
} else {
return const WelcomePage();
}
},
),
);
}
The issue is that I am unable to use _firstLaunch within the FutureBuilder since its still considered a future value and snapshot.data does give me the correct value when I print it but the type is AsyncSnapshot<Object?> so I also cant seem to use it.
Does anyone have suggestions on how to solve this or possibly know of a better way to show the on-boarding screen?
Prefer documentation:
https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
Try this:
#override
Widget build(BuildContext context) {
return GetMaterialApp(
home: FutureBuilder<bool>(
future: firstLaunch, //Future<bool> value
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
if (snapshot.hasData) {
return snapshot.data ? const OnboardingScreen() : const WelcomePage();
} else {
return const WelcomePage();
}
},
),
);
}
Use a bool inside has data
if (snapshot.hasData) {
bool check = snapshot.data;
return check ? const OnboardingScreen() : const WelcomePage();
}

StreamBuilder - Bad state: Use multiple StreamBuilder on one screen

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
}
}),

FutureBuilder only works in Debug

I have a FutureBuilder with a ListView to display custom items (Widgets) with values which are read from .txt files.
The problem is that these items are only displayed if I launch the app in Debug-mode or run-mode. When I try to open the app with the AppLauncher (like a "normal" user would do it) the listView is empty. I tried this on an AVD and on a "real" device.
the Future "listFuture" is used to read the values from the files and return a list of Widgets
class Home extends StatefulWidget {
final Future listFuture = setupList();
#protected
#mustCallSuper
void initState() {
print("init complete");
}
#override
State<StatefulWidget> createState() {
return HomeState();
}
}
If the FutureBuilder gets the data correctly a listView with the list of my widgets should be displayed
child: FutureBuilder<List<SubListItem>>(
future: widget.listFuture,
// ignore: missing_return
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return new Text("None");
case ConnectionState.waiting:
return new Text("loading");
default:
if (snapshot.hasError) {
print("Error");
return Center(child: (Text("No data")));
} else {
return subListView(context, snapshot);
}
}
},
),
Widget subListView(BuildContext context, AsyncSnapshot snapshot) {
List<Widget> items = snapshot.data;
//This ScrollConfiguration is used to remove any animations while scrolling
return ScrollConfiguration(
behavior: CustomScrollBehavior(),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 4),
child: new ListView.builder(
itemCount: items.length,
itemBuilder: (BuildContext context, int index) {
return Column(
children: <Widget>[items[index]],
);
},
),
),
);
}
Thanks for helping!
Ok, I solved the problem. You just have to call "setState" when your Widget is built.
#protected
#mustCallSuper
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
//This setState is necessary because it refreshes the listView
setState(() {});
});
}
It's looks like a async data issue, try these changes:
Remove listFuture from your StatefulWidget.
Add the listFuture var inside your State.
Move the setupList() method inside your State.
And finally call directly like this:
child: FutureBuilder<List<SubListItem>>(
future: setupList(),
// ignore: missing_return
builder: (BuildContext context, AsyncSnapshot snapshot) {
if(!snapshot.hasData) {
return new Text("loading");
}
else if (snapshot.hasError) {
print("Error");
return Center(child: (Text("No data")));
} else {
return subListView(context, snapshot);
}
}
},
),

Unable to show Circular progress indicator when FutureBuilder is loading data

I am trying to show Circular Progress Indicator while my data in Future Builder loads, I tried two methods to add it however both of them didn't work. How can I achieve the desired result?
My code
class _MyHomePageState extends State<MyHomePage>{
#override MyHomePage get widget => super.widget;
#override
Widget build(BuildContext context){
//To show the ListView inside the Future Builder
Widget createTasksListView(BuildContext context, AsyncSnapshot snapshot) {
var values = snapshot.data;
return ListView.builder(
itemCount: values == null ? 0 : values.length,
itemBuilder: (BuildContext context, int index) {
return values.isNotEmpty ? Ink(
.....
) : new CircularProgressIndicator(); //TRIED TO ADD CIRCULAR INDICATOR HERE
},
);
}
//Future Builder widget
Column cardsView = Column(
children: <Widget>[...
Expanded(
child: FutureBuilder(
future: //API CALL,
initialData: [],
builder: (context, snapshot) {
if (!snapshot.hasData) return Center(child: CircularProgressIndicator()); //CIRCULAR INDICATOR
return createTasksListView(context, snapshot);
}),
),
],
);
return Scaffold(
...);
}
}
Try with:
FutureBuilder(
future: //API CALL,
initialData: [],
builder: (context, snapshot) {
if (!snapshot.hasData)
return Center(child: CircularProgressIndicator()); //CIRCULAR INDICATOR
else
return createTasksListView(context, snapshot);
}),
),
When the value of the future is an empty list, which is your initialData, you are rendering a ListView with 0 items, so you cannot render a CircularProgressIndicator in its itemBuilder.