Dart flutter stream stop and start with condition? - flutter

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"),
),
],
),
),
);
}
}

Related

Avoid ListView's unwanted refresh

As the following animation displays, when I tap one of the list items that StreamBuilder() is querying, it shows the items data on the right darker container (it's always Instance of '_JsonQueryDocumentSnapshot'). But at the same time in each tap, the whole list is refreshing itself, which is not very cost-effective I believe.
How can I avoid this unwanted refresh?
Answers with GetX state management dependency are also welcome.
class Schedule extends StatefulWidget {
#override
_ScheduleState createState() => _ScheduleState();
}
class _ScheduleState extends State<Schedule> {
final FirebaseFirestore _db = FirebaseFirestore.instance;
final DateTime _yesterday = DateTime.now().subtract(Duration(days: 1));
var _chosenData;
#override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: StreamBuilder<QuerySnapshot>(
stream: _db.collection('Schedule').where('date', isGreaterThan: _yesterday).limit(10).orderBy('date').snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
return ListView.builder(
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) {
var data = snapshot.data!.docs[index];
return ListTile(
leading: Icon(Icons.person),
title: Text(data['project'], style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(data['parkour']),
onTap: () {
setState(() {_chosenData = data;});
},
);
},
);
} else {
return Center(child: CupertinoActivityIndicator());
}
},
),
),
VerticalDivider(),
Expanded(
child: Container(
alignment: Alignment.center,
color: Colors.black26,
child: Text('$_chosenData'),
),
),
],
);
}
}
To me the easiest solution would be just make it stateless and use a Getx class.
class ScheduleController extends GetxController {
var chosenData;
void updateChosenData(var data) {
chosenData = data;
update();
}
}
And your Schedule.dart would look like this:
class Schedule extends StatelessWidget {
final FirebaseFirestore _db = FirebaseFirestore.instance;
final DateTime _yesterday = DateTime.now().subtract(Duration(days: 1));
#override
Widget build(BuildContext context) {
final controller = Get.put(ScheduleController());
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: StreamBuilder<QuerySnapshot>(
stream: _db
.collection('Schedule')
.where('date', isGreaterThan: _yesterday)
.limit(10)
.orderBy('date')
.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
return ListView.builder(
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) {
var data = snapshot.data!.docs[index];
return ListTile(
leading: Icon(Icons.person),
title: Text(data['project'],
style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(data['parkour']),
onTap: () => controller.updateChosenData(data), // calls method from GetX class
);
},
);
} else {
return Center(child: CupertinoActivityIndicator());
}
},
),
),
VerticalDivider(),
Expanded(
child: Container(
alignment: Alignment.center,
color: Colors.black26,
child: GetBuilder<ScheduleController>(
builder: (controller) => Text('${controller.chosenData}'), // only this rebuilds
),
),
),
],
);
}
}
This way the listview.builder never rebuilds, only the Text widget directly inside the GetBuilder gets rebuilt when you selected a different ListTile.
Calling setState() notifies the framework that the state of Schedule has changed, which causes a rebuild of the widget and so your StreamBuilder.
You could move your stream logic to an upper level of the widget tree. So, setState() will not trigger a rebuild of StreamBuilder.
class ParentWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('Schedule')
.where(
'date',
isGreaterThan: DateTime.now().subtract(Duration(days: 1)),
)
.limit(10)
.orderBy('date')
.snapshots(),
builder: (context, snapshot) {
return Schedule(snapshot: snapshot); // Pass snapshot to Schedule
},
);
}
}
Another approach would be using Stream.listen in initState() which is called once. This way your stream won't be subscribed for each time setState() is called.
...
late StreamSubscription<QuerySnapshot> _subscription;
#override
void initState() {
_subscription = _db
.collection('Schedule')
.where('date', isGreaterThan: _yesterday)
.limit(10)
.orderBy('date')
.snapshots()
.listen((QuerySnapshot querySnapshot) {
setState(() {
_querySnapshot = querySnapshot;
});
});
super.didChangeDependencies();
}
#override
void dispose() {
_subscription.cancel(); // Cancel the subscription
super.dispose();
}
...

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

StreamBuilder is not showing data from firestore

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.

I am failing to get data from cloud firestore while using flutter

At first, when i started writing my calls to get data from firestore, it worked. But when i tried writing more docs to my collection, it failed to bring data for the docs i recently added. Then, when i deleted the first one i added, i stopped receiveing data from firestore all together. I have tried several methods, but have all ended in failure.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
class collect extends StatefulWidget {
#override
_collectState createState() => _collectState();
}
class _collectState extends State<collect>
{
Future _data;
void initState()
{
super.initState();
_data = getStuff();
}
Future getStuff()
async {
var firestore = FirebaseFirestore.instance;
QuerySnapshot qn = await firestore.collection("buses").get();
return qn.docs;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: _data,
builder: (_, snapshot)
{
if(snapshot.connectionState == ConnectionState.waiting)
{
return Center(
child:Text("Loading")
);
}
else if(snapshot.connectionState == ConnectionState.done)
{
return ListView.builder(itemCount: snapshot.data.length,itemBuilder:(_, index)
{
return Container(
child: ListTile(
title: Text(snapshot.data[index].data()["name"].toString()),
subtitle: Text(snapshot.data[index].data()["price"].toString()),
),
);
});
}
},
),
);
}
}
```![enter image description here](https://i.stack.imgur.com/L7FqF.jpg)
Define your database call as,
Future getStuff() async {
var docs;
await FirebaseFirestore.instance
.collection("buses")
.get()
.then((querySnapshot) {
docs = querySnapshot.docs;
});
return docs;
}
Then use the FutureBuilder in the build() function as,
return Scaffold(
body: Center(
child: FutureBuilder<dynamic>(
future: getStuff(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (_, index) {
return Container(
child: ListTile(
title: Text(
snapshot.data[index].data()["name"].toString()),
subtitle: Text(
snapshot.data[index].data()["price"].toString()),
),
);
});
} else {
return CircularProgressIndicator();
}
},
),
),
);
I wrapped the FutureBuilder inside a Center just for clarity, you may remove that Center widget.

How check the ConnectionState of Firestore snapshot stream with StreamProvider?

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