This example from the cloud_firestore documentation uses a StreamBuilder and the ConnectionState of an AsyncSnapshot to handle the stream in its different states. Is there a similar way to manage the ConnectionState when accessing the stream via a StreamProvider instead of a StreamBuilder? What is the best way of avoiding it to return null in the short while until it actually has documents from Firestore?
Here the example from the cloud_firestore docs with the StreamBuilder:
class BookList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('books').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.waiting: return new Text('Loading...');
default:
return new ListView(
children: snapshot.data.documents.map((DocumentSnapshot document) {
return new ListTile(
title: new Text(document['title']),
subtitle: new Text(document['author']),
);
}).toList(),
);
}
},
);
}
}
I have a rather basic stream:
List<AuditMark> _auditMarksFromSnapshot(QuerySnapshot qs) {
return qs.documents.map((DocumentSnapshot ds) {
return AuditMark.fromSnapshot(ds);
}).toList();
}
Stream<List<AuditMark>> get auditMarks {
return Firestore.instance
.collection('_auditMarks')
.snapshots()
.map(_auditMarksFromSnapshot);
}
This is accessed via a StreamProvider (have omitted other providers here):
void main() async {
runApp(MultiProvider(
providers: [
StreamProvider<List<AuditMark>>(
create: (_) => DatabaseService().auditMarks, ),
],
child: MyApp(),
));
}
I have tried somehow converting the QuerySnapshot to an AsyncSnapshot<QuerySnapshot> but probably got that wrong.
Could of course give the StreamProvider some initialData like so - but this is cumbersome, error prone and probably expensive:
initialData: <AuditMark>[
AuditMark.fromSnapshot(await Firestore.instance
.collection('_auditMarks')
.orderBy('value')
.getDocuments()
.then((value) => value.documents.first))
...but I am hoping there is a smarter way of managing the connection state and avoiding it to return null before it can emit documents?
I have been dealing with this and didn't want to declare an initialData to bypass this issue.
What I did was creating a StreamBuilder as the child of StreamProvider.
So that I could use the snapshot.connectionState property of StreamBuilder in the StreamProvider.
Here's the code:
return StreamProvider<List<AuditMark>>.value(
value: DatabaseService().auditMarks,
child: StreamBuilder<List<AuditMark>>(
stream: DatabaseService().auditMarks,
builder: (context, snapshot) {
if (!snapshot.hasError) {
switch (snapshot.connectionState) {
case ConnectionState.none: // if no connection
return new Text(
"Offline!",
style: TextStyle(fontSize: 24, color: Colors.red),
textAlign: TextAlign.center,
);
case ConnectionState.waiting
// while waiting the data, this is where you'll avoid NULL
return Center(child: CircularProgressIndicator());
default:
return ListView.builder(
// in my case I was getting NULL for itemCount
itemCount: logs.length,
itemBuilder: (context, index) {
return LogsTile(log: logs[index]);
},
);
}
}
else {
return new Text(
"Error: ${snapshot.error}",
style: TextStyle(fontSize: 17, color: Colors.red),
textAlign: TextAlign.center,
);
}
}
)
);
Probably not the most elegant solution, but I ended up using a simple bool variable which is true while not all StreamProviders have emitted values.
bool _waitForStreams = false;
if (Provider.of<List<AuditMark>>(context) == null) _waitForStreams = true;
if (Provider.of<...>>(context) == null) _waitForStreams = true;
(etc. repeat for every StreamProvider)
// show "loading..." message while not all StreamProviders have supplied values
if (_waitForStreams) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 25.0),
Text('loading...'),
],
)
],
),
);
}
I don't know if it's correct but this is how I implement it.
Since streamProviser does not provide a connection state, I first use streamBuilder and then provider.value to distribute the data:
return StreamBuilder<BusinessM>(
stream: db.businessDetails(), //firebase stream mapped to business model class
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active)
return Provider<BusinessM>.value(
value: snapshot.data,
child: Businesspage(),
);
else
return Center(child: CircularProgressIndicator());
});
For someone who want to use StreamProvider but end up with no ConnectionState state to use. For some of the cases, null represent the state of "waiting for the first data", not "no data".
In StreamProvider, there is no build-in method to detect the state. But we can still warp the state outside of the data:
StreamProvider<AsyncSnapshot<QuerySnapshot?>>.value(
initialData: const AsyncSnapshot.waiting(),
value: FirebaseFirestore.instance
.collection('books')
.snapshots()
.map((snapshot) {
return AsyncSnapshot.withData(ConnectionState.active, snapshot);
}),
child: ...,
);
or for firebaseAuth:
StreamProvider<AsyncSnapshot<User?>>.value(
initialData: const AsyncSnapshot.waiting(),
value: FirebaseAuth.instance.userChanges().map(
(user) => AsyncSnapshot.withData(ConnectionState.active, user),
),
),
Related
Widget build(context) {
return Scaffold(
appBar: header(context, isApp: true, titleText: 'Instagram'),
body: StreamBuilder<QuerySnapshot>(
stream: userRef.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
circularProgress();
}
final List<Text> list = snapshot.data!.docs
.map((user) => Text(user['username']))
.toList();
return Container(
child: ListView(
children: list,
),
);
},
),
);
}
The error message appears to be referring to the null check operator in this line
final List<Text> list = snapshot.data!.docs
.map((user) => Text(user['username']))
.toList();
implying that snapshot.data is null at this point.
Clearly you don't intend for this code to be executed if no data has been returned.
Since this block of code
if (!snapshot.hasData) {
circularProgress();
}
has no return statement, execution continues down the block.
Changing it to
if (!snapshot.hasData) {
return circularProgress();
}
should solve your problem
I have a FutureBuilder with multiple futures, how can I which one of the futures has no data so I can display the proper widget.
Basically I want to be able to do something like:
if snapshot.data[0] has no data display widgetOne
else if snapshot.data[1] has no data display widgetTwo
else if snapshot.data[2] has no data display widgetThree
I tried snapshot.data[0].toString().isEmpty == true, snapshot.data[0] == null. Either of those throws
'[]'
js_primitives.dart:30 Dynamic call of null.
js_primitives.dart:30 Receiver: null
js_primitives.dart:30 Arguments: [0]
Using !snapshot.hasData tells me there's no data in one of the future but I want to know which one specifically so I can return the proper widget.
My actual code:
FutureBuilder(
future: Future.wait([
FirestoreService().getUser(widget.username),
FirestoreService().getUserInventory(widget.username),
FirebaseRTDB().getAllItems()
]),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const AwaitingResultWidget();
} else if (snapshot.hasData) {
// Account data
final AccountModel user = snapshot.data[0];
// Inventory List
final List<InventoryItem> inventoryList = snapshot.data[1];
// Market Data
final Map<String, Item> itemMap = snapshot.data[2];
return Column(
children: [
Column(
children: [
kIsWeb ? webUserHeader(user) : androidUserHeader(user),
],
),
Center(
child: SingleChildScrollView(
child: Column(
children: [
Text('foo'),
],
),
),
)
],
);
} else if (!snapshot.hasData) {
if (snapshot.data[0] != null) {
return Container(
child: Text('foo1'),
);
}
return Container(
child: Text('foo'),
);
} else if (snapshot.hasError) {
print(snapshot.error);
return const SomethingWentWrongWidget();
} else {
return const UserNotFound();
}
},
),
You can add futures to variables and check its data like
Future<String> firstFuture = FirestoreService().getUser(widget.username);
Future<int> secondFuture = FirestoreService().getUserInventory(widget.username);
FutureBuilder(
future: Future.wait([firstFuture, secondFuture]),
builder: (context, AsyncSnapshot<List<dynamic>> snapshot) {
snapshot.data[0]; //first future
snapshot.data[1]; //second future
},
);
product_list_screen.dart
import 'package:flutter/material.dart';
import '../blocs/cart_bloc.dart';
import '../models/cart.dart';
class ProductListScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("eCommerce"),
actions: [
IconButton(
icon: Icon(Icons.shopping_cart),
onPressed: () => Navigator.pushNamed(context, "/cart"),
)
],
),
body: buildProductList(),
);
}
buildProductList() {
return StreamBuilder(
initialData: productBloc.getAll(),
stream: productBloc.getStream,
builder: (context, snapshot) {
return snapshot.data.length > 0 //error
? buildProductListItems(snapshot)
: Center(
child: Text("No data"),
);
},
);
}
buildProductListItems(AsyncSnapshot<Object?> snapshot) {
return ListView.builder(
itemCount: snapshot.data.length, //error
itemBuilder: (BuildContext context, index) {
var list = snapshot.data;
return ListTile(
title: Text(list[index].name), //error
subtitle: Text(list[index].price.toString()), //error
trailing: IconButton(
icon: Icon(Icons.add_shopping_cart),
onPressed: () {
cartBloc.addToCart(Cart(List[index], 1)); //error
},
),
);
});
}
}
The property 'length' can't be unconditionally accessed because the receiver can be 'null'. (Documentation) Try making the access conditional (using '?.') or adding a null check to the target ('!').
I used '!' or '?' but its didn't work. Can you help me? Thanks.
You can try using null aware operator :
snapshot.data?.length ?? 0
Do not use StreamBuilder use FutureBuilder Example:
FutureBuilder<Object?>(
future: _fetchNetworkCall, // async work
builder: (BuildContext context, AsyncSnapshot<Object?>snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting: return Text('Loading....');
default:
if (snapshot.hasError)
return Text('Error: ${snapshot.error}');
else
return Text('Result: ${snapshot.data}');
}
},
)
Try to press ctrl+. On windows or command+. On mac to get suggestions from your code
This will work in 90%of the times
It would be easier by providing return type on StreamBuilder. Since you like receive a list of Item, include
return StreamBuilder<List<ItemModelClass>?>(
builder: (context, snapshot) {
return snapshot.data != null && snapshot.data!.length > 0
? Text("replresenData")
: Text("NO data")
I prefer including error, hasData and empty data state separately.
Find more about StreamBuilder
I want to stop and restart a stream conditionally, how can I do that?
_fireStore
.collection(...)
.doc(...)
.snapshots().takeWhile((element) => _listenable);
Stream stops when _listenable is false but doesn't restart when true ?
Anyone know how to do it?
You can use async to easily control the stream This is how you can do it.
First create a StreamSubsctiprion like this
StreamSubscription<QuerySnapshot>? _eventStream;
and then set a listener for your snapshots stream.
here is how you can store stream query snapshot
Stream<QuerySnapshot> streamSub =_fireStore
.collection(...)
.doc(...)
.snapshots();
add listener for your stream like this
_eventStream = streamSub.listen((snapshot) => _eventStream);
Then you can use your event stream to cancel, pause and resume your stream like this
_eventStream!.pause();
_eventStream!.cancel();
_eventStream!.resume();
here is my final code
class MainScreen extends State<PaymentScreen> {
StreamSubscription<QuerySnapshot>? _eventStream;
#override
Widget build(BuildContext context) {
Stream<QuerySnapshot> myStream = FirebaseFirestore.instance.collection('Users').snapshots();
return Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
StreamBuilder<QuerySnapshot>(
stream: myStream,
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Text("Loading");
}
return Column(
children:
snapshot.data!.docs.map((DocumentSnapshot document) {
Map<String, dynamic> data =
document.data()! as Map<String, dynamic>;
return ListTile(
title: Text(data['firstName']),
);
}).toList(),
);
},
),
FloatingActionButton.extended(
onPressed: () {
_eventStream = myStream.listen((snapshot) => _eventStream);
try {
_eventStream!.pause();
print('paused');
} catch (e, s) {
print(s);
}
},
label: const Text("Pause Stream"),
),
],
),
),
);
}
}
I am using streambuilder to display snapshot data but it is not displaying. The screen is just blank but When I use the future builder with get() methode it display the data but I want realtime changes. I am new to flutter please help me with this. here is code.
class TalentScreen2 extends StatelessWidget {
final Query _fetchFavUser = FirebaseRepo.instance.fetchFavUsers();
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Column(
children: [
Text('Talent Screen 2(Favourites)'),
Expanded(child: _retrieveData(context))
],
),
),
);
}
Widget _retrieveData(BuildContext context) => StreamBuilder<QuerySnapshot>(
stream: _fetchFavUser.snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) return const Text('Something went wrong');
if (!snapshot.hasData) return const Text('Alas! No data found');
if (snapshot.connectionState == ConnectionState.waiting)
return Center(
child: CircularProgressIndicator(
strokeWidth: 2.0,
));
if (snapshot.connectionState == ConnectionState.done)
return theUserInfo(snapshot.data.docs);
return Container();
});
Widget theUserInfo(List<QueryDocumentSnapshot> data) {
return ListView.builder(
shrinkWrap: true,
itemCount: data.length,
itemBuilder: (BuildContext context, int index) {
var uid = data[index]['uid'];
TalentHireFavModel userData = TalentHireFavModel.fromMap(
data[index].data(),
);
return Card(
child: Column(
children: <Widget>[
Text(data[index]['orderBy']),
// Text(userData.name ?? ''),
Text(userData.categories),
Text(userData.skills),
// Text(userData.country ?? ''),
Text(userData.phoneNo),
Text(userData.hourlyRate),
Text(userData.professionalOverview),
Text(userData.skills),
Text(userData.expert),
// Text(userData.createdAt ?? ''),
_iconButton(userData.uid, context),
],
),
);
});
}
Future<DocumentSnapshot> fetch(data) async =>
await FirebaseRepo.instance.fetchWorkerUserData(data);
Widget _iconButton(uid, context) {
return IconButton(
icon: Icon(Icons.favorite),
onPressed: () {
BlocProvider.of<TalentFavCubit>(context).removeTalentFav(uid);
});
}
}
and here is the firestore query methode where I am just applying simple query to fetch all documents and display them. I want real-time changes
Query fetchFavUsers() {
var data = _firestore
.collection('workerField')
.doc(getCurrentUser().uid)
.collection('favourites')
// .where('uid', isNotEqualTo: getCurrentUser().uid)
.orderBy('orderBy', descending: true);
return data;
}
The solution is to just return the function. Get that method out of if statement and place it in just return statement.