New to flutter and need help.
I'm using google sheets as a database. Supposed to use multiple sheets each with different sheetId.
I have 2 function to get the data:
getChildSchedule - to get data from one specific sheet
getAllSchedule - to get data from multiple sheets.
In the widget I'm checking if I'm supposed to display data from a particular sheet or from all the sheets and call the appropriate func. When I'm calling getChildSchedule from the widget all works perfectly and it shows the data.
But when I need the getAllSchedule it gets stuck. It doesn't
stop running but seems as if it's in an infinite loop though there is no such loop to get stuck on.
From the prints and the tracking I did, it calls on the getChild with index 0 but never returns from it - though the child data is being printed inside getChild.
What am I doing wrong here?
Future<List<Lesson>> getChildSchedule(int childId) async {
print('in getChild: child $childId: ${ChildrenManager.children[childId].spreadsheetId}');
spreadsheetId = ChildrenManager.children[childId].spreadsheetId;
await init();
final lessons = await _scheduleSheet.values.allRows(fromRow: 2);
print('in getChild: child $childId lessons: $lessons');
return List.generate(
lessons.length,
(index) => Lesson(
weekDay: lessons[index][0],
startTime: double.tryParse(lessons[index][1] ?? ''),
endTime: double.tryParse(lessons[index][2] ?? ''),
grade: ChildrenManager.children[childId].grade,
teacher: lessons[index][3],
header: lessons[index][4],
description: lessons[index][5],
zoomLink: Uri.tryParse(lessons[index][6] ?? ''),
meetingID: lessons[index][7],
meetingCode: lessons[index][8],
supplies: lessons[index][9],
assignment: Uri.tryParse(lessons[index][10] ?? ''),
),
);
}
Future<List<Lesson>> getAllSchedule() async {
List<List<Lesson>> schedules = List<List<Lesson>>();
for (int i = 0; i < ChildrenManager.children.length; i++) {
print('in getAll schedules: $i');
schedules[i] = await getChildSchedule(i);
print('in getAll schedules: got $i child'); //this never gets printed
}
print('in getAll schedules: $schedules');
List<Lesson> schedule = List<Lesson>();
for (List<Lesson> sc in schedules) {
schedule.addAll(sc);
}
schedule.sort((a, b) {
int result = a.startTime.compareTo(b.startTime);
if (result == 0) {
return a.endTime.compareTo(b.endTime);
}
return result;
});
return schedule;
}
I think the issue here was because of repeated calls that changed the sheet id while the previous was still running.
I've moved the getAll to another class and called it with a new manager object(that contains the getChild) for each child and it solved the issue.
Related
I'm making a reactive model with Getx on a product list, but when I start the list it comes with no value and causes an index error even though there are actually values in the list it appears empty at first, which somehow gets fixed automatically. (this is inside a build of a statelesswidget)
return GetX<CartController>(
init: CartController(),
builder: (controller) {
try {
return Text(
"${StringConvert.toMoney(controller.totalByProduct[productId])}",
style: kSmallTextBold,
);
} catch (e) {
return const Text("Error...");
}
},
);
}
I did try catch to manage this, but the catch part doesn't show up;
this is relevant part of the controller
var totalByProduct = [].obs;
fetchTotal() {
List products = storage.read(StorageKeys.cartProducts);
double currentValue = 0.0;
List currentTotals = [];
for (var item in products) {
currentTotals.add(item['total'] * item['amount']);
currentValue += item['total'] * item['amount'];
}
total.value = currentValue;
totalByProduct.value = currentTotals;
}
I believe it's not the right way to do this, so what do I need to know to fix this correctly?
If helps this is the error:
With a method to read the storage (sharedPreferences) in async mode, with a FutureBuilder it was possible to correct the error, because in the initial state the list takes the value assigned explicitly. Even if it then receives the correct value, accessing the index in its initial state causes the error, this explains why even with the error it works.
I'm writing a flutter app and and using the bloc library. I have a bloc and a cubit, within the state of each is a list of ids of some other documents I need to fetch from firestore. There can be some overlap and some docs are already fetched so I want to get the list of ids from both states, compare them, and then only go to firestore for ones that exist in one but no the other.
I set a new cubit for this:
class CircleRecipesCubit extends Cubit<CircleRecipesState> {
CircleRecipesCubit({
#required RecipesBloc recipesBloc,
#required CirclesCubit circlesCubit,
}) : assert(
recipesBloc != null,
circlesCubit != null,
),
_recipesBloc = recipesBloc,
_circlesCubit = circlesCubit,
super(CircleRecipesInitial());
final RecipesBloc _recipesBloc;
final CirclesCubit _circlesCubit;
StreamSubscription _recipesSubscription;
StreamSubscription _circlesSubscription;
Future<void> getCircleRecipes() async {
// get a list of recipes the user already has loaded
List<String> userRecipesIds;
_recipesSubscription = _recipesBloc.stream.listen((RecipesState event) {
if (event is RecipesLoaded) {
userRecipesIds = event.recipes.map((e) => e.id).toList();
print('*');
print(userRecipesIds);
print('*');
}
});
// get a list of recipes in the circles
List<String> circleRecipeIds;
_circlesSubscription = _circlesCubit.stream.listen((CirclesState event) {
if (event is CirclesLoaded) {
circleRecipeIds = event.circles.fold([],
(previousValue, element) => [...previousValue, ...element.recipes]);
print('|');
print(circleRecipeIds);
print('|');
// List<String> circleOnlyRecipeIds = circleRecipeIds;
// circleRecipeIds.removeWhere((e) => userRecipesIds.contains(e));
// print(circleOnlyRecipeIds);
}
});
// reduce the list of recipes to a set of only circle recipes
//TODO
//------- Try with RX dart
Stream<RecipesState> recipesStream = _recipesBloc.stream;
Stream<CirclesState> circlesStream = _circlesCubit.stream;
Rx.combineLatest2(recipesStream, circlesStream, (
RecipesState recipesState,
CirclesState circlesState,
) {
print("This doesn't print!");
print(recipesState);
print(circlesState);
if (recipesState is RecipesLoaded) {
userRecipesIds = recipesState.recipes.map((e) => e.id).toList();
print('*');
print(userRecipesIds);
print('*');
}
if (circlesState is CirclesLoaded) {
circleRecipeIds = circlesState.circles.fold([],
(previousValue, element) => [...previousValue, ...element.recipes]);
print('|');
print(circleRecipeIds);
print('|');
// List<String> circleOnlyRecipeIds = circleRecipeIds;
// circleRecipeIds.removeWhere((e) => userRecipesIds.contains(e));
// print(circleOnlyRecipeIds);
}
// fetch the set of recipes
});
}
#override
Future<void> close() {
_recipesSubscription.cancel();
_circlesSubscription.cancel();
return super.close();
}
}
So above is my cubit - it listens to the recipesBloc and the circlesCubit. The first two expressions in the getCiricleRecipes() function are only there to prove that its hooked up correctly - when it runs those print statement print the ids I want it to from both the other bloc and the other cubit.
I need the latest values from both though at the same time to compare them - so I thought rx.combinelatest2 would be good. I give it the stream from the bloc and the cubit. But the combiner function doesn't even run even though things seem 'wired up' correctly.
Any help greatly appreciated.
Make sure both streams have already emitted at least one item.
combineLatest documentation states:
The Stream will not emit until all streams have emitted at least one item.
Since the first block (where you subscribe to _circlesCubit) prints, then most likely _recipesBloc is the culprit here.
I have a list called wastedProducts which I want to iterate through and create a new list, wastedProductsSet based on each item in the original list.
This is the code I have to do that:
for (var productHeld in wastedProducts) {
if (wastedProductsSetNames.contains(productHeld.masterCat)) {
print("duplicate ${productHeld.masterCat}");
} else {
_wastedCount = wastedProducts
.where((p) => p.masterCat == productHeld.masterCat)
.fold(0,(amountWasted, product) => amountWasted + product.amountWasted);
_masterCat = Firestore.instance
.collection('masterCategories')
.document(productHeld.masterCat);
// repeats the above for each item before completing the below for each item
_masterCat.get().then(
(value) {
baseUnit = value.data['baseUnit'];
if (baseUnit == null) {
baseUnit = '';
} else {
baseUnit = value.data['baseUnit'];
}
wastedProductsSet.add(
WastedProduct(
productName: productHeld.masterCat,
wastedCount: _wastedCount.toInt(),
baseUnit: baseUnit,
),
);
wastedProductsSetNames.add(productHeld.masterCat);
},
);
}
}
Based on the print statements, I can see it is completing the code up to the _masterCat.get().then( line and doing that for each item in wastedProducts, then completing the code below _masterCat.get().then( for each item.
I assume it must have something to do with the asynchronous nature of the .then but cannot work out what the problem is.
I originally was using .forEach instead of for (var productHeld in wastedProducts) but changed based on the answer in this post My async call is returning before list is populated in forEach loop.
I'm still quite new to Flutter and I use flutter_bloc for state management.
In bloc I'm listening to a repository method Stream<List<UserAlert>> alerts(). In this method I listen both to .onChildAdded and .onChildRemoved events on a Firebase real time database node and update a local List<UserAlert> alerts, but I'm not succeeding in returning it once it updates so I get null value on bloc listener. NoSuchMethodError: The method 'listen' was called on null. I chose to manually manage it as I don't want to download the whole node at every change as it happen when listening to .onValue. I tried return alerts as Stream<List<UserAlert>> inside both firebase snapshot scopes but I get a type cast error.
I tried Stream.value(alerts) both inside each scope and as the very last thing in the method but still returns null.
How can I get alerts to be returned as a stream?
Thank you very much for your help.
Repository method:
Stream<List<UserAlert>> alerts() {
print('alerts() called');
Stream<List<UserAlert>> alertStream;
List<UserAlert> alerts = [];
// Option 2 return a list manually updated by two different events: More complicated but very efficient(bandwidth), lowest data consuming = Lowest Firebase bill.
// returns one event per node record at app start and only new afterwards
_alertSubscription = _databaseReference.onChildAdded.listen((Event event) {
print(' observer .childAdded: added alert is : ${event.snapshot.value}');
UserAlert userAlert = UserAlert.fromSnapshot(event.snapshot);
alerts.add(userAlert);
// alertStream = Stream.value(alerts); // alerts as Stream<List<UserAlert>>;
// return alerts;
print(
'observer .childAdded: alerts are: $alerts and total alerts are ${alerts.length}');
});
// returns one event per node removed record
_alertSubscription =
_databaseReference.onChildRemoved.listen((Event event) {
print(
'observer .childRemoved: removed alert is : ${event.snapshot.value}');
int index = alerts.indexWhere(
(userAlert) => userAlert.id.startsWith(event.snapshot.value['Id']));
print('index to remove is: $index');
alerts.removeAt(index);
// return alerts;
print(
'observer .childRemoved: alerts after removing are: $alerts and total alerts are ${alerts.length}');
// return alerts;
});
// yield* alertStream;
Stream.value(alerts);
}
Bloc listener:
Stream<AlertState> _mapLoadAlertToState() async* {
_alertSubscription?.cancel();
_alertSubscription = _alertRepository
.alerts()
.listen((alerts) => add(AlertsUpdated(alerts)));
//// (List<UserAlert> alerts) {
//// print('_mapLoadAlertToState() userAlterts are: $alerts');
//// add(AlertsUpdated(alerts));
// });
}
You can use the yield statement to return a Stream:
Stream<AlertState> _mapLoadAlertToState() async* {
Stream<List<UserAlert>> _stream = _alertRepository.alerts();
await for(List<UserAlert> alerts in _stream){
yield AlertsUpdated(alerts);
}
}
I changed approach and now I get data from firebase as I was expecting with previous approach.
I decided to split Repository method into two different methods that transform the streams into Stream<UserAlert :
Stream<UserAlert> addedAlert() {
print('addedAlert() called');
handleData(Event event, EventSink<UserAlert> sink) =>
sink.add(UserAlert.fromSnapshot(event.snapshot));
final transformer = StreamTransformer<Event, UserAlert>.fromHandlers(
handleData: handleData);
return _databaseReference.onChildAdded.transform(transformer);
}
Stream<UserAlert> removedAlert() {
print('removedAlert() called');
handleData(Event event, EventSink<UserAlert> sink) =>
sink.add(UserAlert.fromSnapshot(event.snapshot));
final transformer = StreamTransformer<Event, UserAlert>.fromHandlers(
handleData: handleData);
return _databaseReference.onChildRemoved.transform(transformer);
}
and handle the adding to and removing from List<UserAlert> inside the bloc method:
Stream<AlertState> _mapLoadAlertToState() async* {
_addedAlertStreamSubcription =
_alertRepository.addedAlert().listen((UserAlert alert) {
print('received snapshot is:$alert.'); // prints null
alerts.add(alert);
print(
'observer .childAdded: added alert is :$alert, we have ${alerts.length} active alerts, active alerts are: $alerts');
add(AlertsUpdated(alerts));
});
_removedAlertStreamSubscription =
_alertRepository.removedAlert().listen((UserAlert alert) {
int index =
alerts.indexWhere((userAlert) => userAlert.id.startsWith(alert.id));
print('index to remove is: $index');
alerts.removeAt(index);
print(
'observer .childRemoved: removed alert is :$alert, we have ${alerts.length} active alerts, active alerts are: $alerts');
add(AlertsUpdated(alerts));
});
}
AlertUpdated will then trigger:
Stream<AlertState> _mapAlertsUpdatedToState(AlertsUpdated alert) async* {
print(
'_mapAlertsUpdatedToState() called, alerts are: ${alert.alerts} ');
yield AlertLoaded(alert.alerts);
}
_mapAlertsUpdatedToState prints are showing the correct List, but prints from BlocListeneronly show up once with one value in the list.
BlocListener<AlertBloc, AlertState>(
listener: (BuildContext context, AlertState state) {
if (state is AlertLoaded) {
List<UserAlert> userAlerts = (state).alerts;
print(
'AlertListener userAlerts are: $userAlerts and total alerts are : ${userAlerts.length}');
}
}),
This is solved by making AlertState classes not extending Equatable as it would compare previous and new state and find them to be the same.
Thank you very much.
TL;DR: Values being added to StreamSink are for some unknown reason being overridden by the respected StreamBuilder's initialValue
From what it appears, my issue is similar to this issue on Github, but the only difference is that instead of getting a single value, I see two statements on the console log, one with the correct value which was added to the stream which was immediately followed by the initialValue which was passed to the stream builder.
In my case, I was using generic_bloc_provider
Elaboration
I have a bloc udhariBloc.dart(line 99) which listens to a collection reference and adds the value to some sinks.
void _fetchUdhari() {
Firestore.instance
.collection("Users 3.0")
.document("data")
.collection("udhari")
.where("participants", arrayContains: userBloc.phoneNumber)
.where("firstPartyDeleted", isEqualTo: false)
.snapshots()
.listen((QuerySnapshot snapshot) {
//Initialized these to zero because their
//value must be recalculated upon every change.
//These variables were initialized to zero(their default value) when declared.
_udhariList = List<UdhariClass>();
_totalDebit = 0;
_totalCredit = 0;
for (int i = 0; i < snapshot.documents.length; i++) {
_udhariList.add(UdhariClass.fromSnapshot(
snapshot: snapshot.documents[i],
userBloc: userBloc,
));
}
// Remove the udhari records where the participant is secondParty
// and secondParty has already deleted the record
_udhariList.removeWhere((UdhariClass udhari) {
return ((udhari.partyType == PartyType.SecondParty) &&
(udhari.secondPartyDeleted == true));
});
for (int i = 0; i < _udhariList.length; i++) {
if (_udhariList[i].udhariType == Udhari.Borrowed) {
_totalDebit += _udhariList[i].amount;
} else {
_totalCredit += _udhariList[i].amount;
}
}
//The correct values are calculated correctly,
//printed and added to respective sinks as well
print("Debit: $_totalDebit");
print("Credit: $_totalCredit");
print("List: ${_udhariList[0].context}");
_totalDebitSink.add(_totalDebit);
_totalCreditSink.add(_totalCredit);
_udhariListSink.add(_udhariList);
});
}
and here are the streams and their controllers
/// Stream controller for displaying total debit on dashboard
StreamController<double> _totalDebitController =
StreamController<double>.broadcast();
Stream<double> get totalDebitStream => _totalDebitController.stream;
StreamSink<double> get _totalDebitSink => _totalDebitController.sink;
/// Stream controller for displaying list of udhari
StreamController<List<UdhariClass>> _udhariListController =
StreamController<List<UdhariClass>>.broadcast();
Stream<List<UdhariClass>> get udhariListStream =>
_udhariListController.stream;
StreamSink<List<UdhariClass>> get _udhariListSink =>
_udhariListController.sink;
/// Stream controller for displaying total credit on dashboard
StreamController<double> _totalCreditController =
StreamController<double>.broadcast();
Stream<double> get totalCreditStream => _totalDebitController.stream;
StreamSink<double> get _totalCreditSink => _totalDebitController.sink;
and this is my stream builder consuming the above streams
StreamBuilder<double>(
initialData: udhariBloc.getTotalDebit,
stream: udhariBloc.totalDebitStream,
builder: (BuildContext context,
AsyncSnapshot<double> snapshot) {
print(
"============INSIDE DEBIT STREAM BLDR============");
print("Conn State: ${snapshot.connectionState}");
print("Has Data: ${snapshot.hasData}");
print("Data: ${snapshot.data}");
print("Has Error: ${snapshot.hasError}");
print("Error: ${snapshot.error}\n\n");
return Text("₹${snapshot.data.floor()}",
style: _textStyleFooter);
},
),
These sinks are then later consumed in a streamBuilders inside Dashboard.dart(line 145). The problem is that even after adding data to respective sinks(in this case, _totalDebitSink), the values are not updated in the stream builder inside Dashboard class. For investigating further, I attached a listener to the _totalDebitStream inside the UdhariBloc's constructor
totalDebitStream.listen((onData) {
print("CURRENT DEBIT: $onData");
}, onError: (error) {
print("Error listening Debit :$error");
}, onDone: () {
print("Done listening to Debit values");
});
and every time there was a change in the value, I saw this log in the console.
CURRENT DEBIT: 100
CURRENT DEBIT: 0
============INSIDE DEBIT STREAM BLDR============
Conn State: ConnectionState.active
Has Data: true
Data: 0.0
Has Error: false
Error: null
Here, 100 was the updated value from Firestore and 0 was the initialValue which was provided to the StreamBuilder and also assigned to the variables _totalDebit, _totalCredit.
I used a similar technique in dashboardBloc.dart(line 88) and Dashboard.dart(line 212) and it works like a charm.
I haven't been able to find any solutions so far.
Finally, found the solution. Turns out it was a stupid fault on my part.
I accidentally mapped wrong streams and sinks to a wrong controller.
In this case,
Stream<double> get totalCreditStream => _totalDebitController.stream;
StreamSink<double> get _totalCreditSink => _totalDebitController.sink;
Here I accidentally mapped the totalCredtiStream with debitController's stream.
Also, thanks to #pskink for pointing it out.