How to retry on error with Flutter StreamBuilder? - flutter

I have a StreamBuilder object to render a list from a FireStore collection:
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('posts').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) return new Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
default:
return new ListView(
children:
snapshot.data.documents.map((DocumentSnapshot document) {
return Post(document: document);
}).toList());
}
});
}
I'm trying to make it so that if the snapshot.hasError, the StreamBuilder tries again. How can i do that?

Generally, you should always combine StreamBuilder with a stateful widget. Otherwise the stream would be recreated every time the build method is called.
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Stream<QuerySnapshot> postsStream;
#override
void initState() {
super.initState();
postsStream = Firestore.instance.collection('posts').snapshots();
}
void retryLoad() {
setState(() {
postsStream = Firestore.instance.collection('posts').snapshots();
})
}
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: postsStream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return RaisedButton(
child: Text('Retry'),
onPressed: retryLoad,
);
}
// ...
},
);
}
}

Related

How to use querySnapshot in a listview builder? (flutter)

I'm trying to fetch documents from my firebase DB and use them to create a social media feed. Here I'm trying to get the length of the fetched collection but I cannot manage to call the variable. Any help would be appreciated. Example code
class LoadDataFromFirestore extends StatefulWidget {
#override
_LoadDataFromFirestoreState createState() => _LoadDataFromFirestoreState();
}
class _LoadDataFromFirestoreState extends State<LoadDataFromFirestore> {
#override
void initState() {
super.initState();
CollectionReference _collectionRef =
FirebaseFirestore.instance.collection('fish');
Future<void> getData() async {
// Get docs from collection reference
QuerySnapshot querySnapshot = await _collectionRef.get();
// Get data from docs and convert map to List
final allData = querySnapshot.docs.map((doc) => doc.data()).toList();
print(allData);
}
}
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: querySnapshot.docs.length,
itemBuilder: (BuildContext context, int index) {
return _postView();
},
),
);
}
}
First of all it is not ok to call future function in initstate, you need to use FutureBuilder like this:
class LoadDataFromFirestore extends StatefulWidget {
#override
_LoadDataFromFirestoreState createState() => _LoadDataFromFirestoreState();
}
class _LoadDataFromFirestoreState extends State<LoadDataFromFirestore> {
late CollectionReference _collectionRef;
#override
void initState() {
super.initState();
_collectionRef = FirebaseFirestore.instance.collection('fish');
}
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<QuerySnapshot>(
future: _collectionRef.get(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Text('Loading....');
default:
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
QuerySnapshot? querySnapshot = snapshot.data;
return ListView.builder(
itemCount: querySnapshot?.docs?.length ?? 0,
itemBuilder: (BuildContext context, int index) {
var data = querySnapshot?.docs?[index].data();
print("data = $data");
return _postView();
},
);
}
}
},
),
);
}
}
inside listview's builder you can use data to parse your data and use it.
You can use FutureBuilder like this:
class LoadDataFromFirestore extends StatefulWidget {
const LoadDataFromFirestore({super.key});
#override
State<LoadDataFromFirestore> createState() => _LoadDataFromFirestoreState();
}
class _LoadDataFromFirestoreState extends State<LoadDataFromFirestore> {
//TODO change Map<String, dynamic> with your data type with fromJson for example
Future<List<Map<String, dynamic>>> _getData() async {
final querySnapshot = await FirebaseFirestore.instance.collection('fish').get();
return querySnapshot.docs.map((doc) => doc.data()).toList();
}
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<List<Map<String, dynamic>>>(
future: _getData(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
return _postView(/* Ithink you have to pass here your item like snapshot.data[index]*/);
},
);
} else {
return const Center(child: CircularProgressIndicator());
}
},
),
);
}
}

Future.wait returning null

class AdView extends StatefulWidget {
const AdView({Key? key, required String id}) : super(key: key);
final id = '2';
#override
_AdViewState createState() => _AdViewState();
}
class _AdViewState extends State<AdView> {
final _adService = NewsService();
Future<AdBanner?> futureAdd() async {
_adService.getAds('2');
}
Future<Categories?> futureCatergoriess() async {
_adService.getAllCategories();
}
#override
void initState() {
futureAdd();
futureCatergoriess();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: Colors.grey[200],
body: FutureBuilder(
future: Future.wait([futureCatergoriess(), futureAdd()]),
builder: (context, AsyncSnapshot<List<dynamic>> snapshot) {
if (snapshot.hasData) {
final advertisements = snapshot.data![0];
return ListView.builder(
itemCount: advertisements!.length,
itemBuilder: (BuildContext context, int index) {
//return bannerListTile(advertisements, index, context);
return const Text('index');
});
} else {
if (snapshot.hasError) {
return NewsError(
errorMessage: '${snapshot.hasError}',
);
}
return const NewsLoading(
text: 'Loading...',
);
}
},
),
);
}
}
snapshot.data![0]; returning null value. I tried already many versions ([1] or snapshot.data.data but I cannot call the results.
I am using future.wait first time. There is no problem if I use any of API with traditional Future.builder.
any help?
after the advice of #ChristopherMoore I modified the code but the main problem is still continue. This code gives as output:
index
index
modified code
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: Future.wait([futureCatergoriess(), futureAdd()]),
builder: (context, AsyncSnapshot<List<dynamic>> snapshot) {
if (snapshot.hasData) {
final advertisements = snapshot.data!;
return ListView.builder(
itemCount: advertisements.length,
itemBuilder: (BuildContext context, int index) {
//return bannerListTile(advertisements, index, context);
return const Text('index');
});
This original line gives this error:
final advertisements = snapshot.data![0];
The getter 'length' was called on null. Receiver: null Tried calling: length The relevant error-causing widget was FutureBuilder<List<Object?>> lib/view/ad_view.dart:37

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

Error: 'await' can only be used in 'async' or 'async*' methods

I am trying to add distance from user to the Location object, but this requires using an asynchronous call that I can't figure out where and how to do exactly. From there I will sort Locations by distance from user. I tried the code below bc it's where the sorted locations would be used, but I get an error saying "await" can only be used in "async" or "async*" methods even though it is being used with an async function. How do I add distance from user to a Location object given it requires an asynchronous call?
class MapWidget extends StatefulWidget {
...
#override
_MapWidgetState createState() => _MapWidgetState();
}
class _MapWidgetState extends
State<MapWidget> {
Future <List<Location>> sortLocations() async {
return null;//function not done
}
#override
Widget build(BuildContext context) {
final List<Location> sortedLocations = await sortLocations();
...
You cannot use await functions in build method because it cannot be async.To use async operations in build you must use FutureBuilder or StreamBuilder.
Future<List<Location>> sortLocations() {
...
return <Location>[];
}
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Location>>(
future: sortLocations(),
builder: (context, snapshot) {
if(snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator()));
}
return ListView(...);
},
);
}
Future<List<Location>> sortLocations() {
...
return <Location>[];
}
#override
Widget build(BuildContext context) {
return StreamBuilder<List<Location>>(
stream: sortLocations().asStream(),
builder: (context, snapshot) {
if(snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator()));
}
return ListView(...);
},
);
}
In Flutter there is a widget call FutureBuilder, that helps you build UI after the data is returned from an async function. You can use it as:
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Location>>(
future: sortLocations(),
builder: (context, snapshot) {
if (!snapshot.hasData) return Container(child: Center(child: CircularProgressIndicator()));
var sortedLocations = snapshot.data;
// Build your UI here
return ...
}
);
you cannot use await in build method instead use it in initState
final List<Location> sortedLocations= new List();
#override
void initState(){
super.initState();
getdata();
}
getdata()async{
sortedLocations.clear();
sortedLocations = await sortLocations();
setState((){});
}

ConnectionState changes twice in main class

I have this app where I have an intro screen that I want to hide if the user has already skipped once.
I'm using bloc with a Provider.
My issue is the connection state changes twice when I hot restart the app and I've been spending hours without understanding the reason.
Here is my code:
my main class
void main() => runApp(StatsApp());
class StatsApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => IntroProvider(),
)
],
child: Consumer<IntroProvider>(builder: (context, value, child) {
return MaterialApp(
home: FutureBuilder(
future: value.bloc.checkSkipped(),
builder: (BuildContext context, snapshot) {
print(snapshot.connectionState);
print(snapshot.data);
return SplashScreen();
},
));
}),
);
}
}
my bloc
enum IntroEvents {
ReadLocalStorage,
SetIntroSkipped,
}
class IntroBloc extends Bloc<IntroEvents, bool> {
PrefsManager _prefsManager = PrefsManager.instance;
Future<bool> checkSkipped() async {
this.add(IntroEvents.ReadLocalStorage);
final skipped =
await _prefsManager.getValue(PrefTypes.Bool, "skippedIntro");
return skipped;
}
#override
// TODO: implement initialState
bool get initialState => false;
#override
Stream<bool> mapEventToState(IntroEvents event) async* {
switch (event) {
case IntroEvents.SetIntroSkipped:
_prefsManager.setValue(PrefTypes.Bool, "skippedIntro", true);
yield true;
break;
case IntroEvents.ReadLocalStorage:
final skipped =
await _prefsManager.getValue(PrefTypes.Bool, "skippedIntro");
yield skipped;
break;
default:
print("wtffffff");
}
}
}
my provider
class IntroProvider with ChangeNotifier {
IntroBloc _bloc;
IntroProvider(){
print("called IntroProvider");
_bloc = IntroBloc();
}
IntroBloc get bloc => _bloc;
}
Any help would be highly appreciated.
When working with snapshots you're able to check whether the snapshot.hasData. Normally you'd wrap the functionality you want to run when the snapshot has data in an if statement and provide some kind of default Widget when it does not.
FutureBuilder(
future: value.bloc.checkSkipped(),
builder: (BuildContext context, snapshot) {
print(snapshot.connectionState);
if (snapshot.hasData) {
print(snapshot.data);
}
return SplashScreen();
},
);