How to generate stream in flutter - flutter

I am making some experiment with flutter stream. I have a class for generating stream of int. Here is the class :
class CounterRepository {
int _counter = 123;
void increment() {
_counter++;
}
void decrement() {
_counter--;
}
Stream<int> watchCounter() async* {
yield _counter;
}
}
I expect with the change of _counter, watchCounter() will yield updated counter value. When I call increment() or decrement() from UI, it seems the value of _counter is changing but watchCounter doesn't yield the updated _counter value. How to yield updated _counter value here? I am using StreamBuilder from UI to get the streamed data.

You have created your streams using -
Stream<int> watchCounter() async* {
yield _counter;
}
But to reflect the changes of your stream, you need to receive those stream events. You can control those stream events using a StreamController
Creating a stream
Future<void> main() async {
var stream = watchCounter();
}
Using that stream
stream.listen
Subscribes to the stream by calling the listen function and supplys it
with a Function to call back to when there's a new value available.
stream.listen((value) {
print('Value from controller: $value');
});
There are many other approaches to control and manage streams but for your particular question .listen will do the job.

You are missing an infinite while loop, it's a manual stream.
Stream<dynamic> watchCounter() async* {
while (true) {
// you can change it to 5 seconds or higher
await Future.delayed(const Duration(seconds: 1));
yield _counter;
}
}
And after that you just need to call listen:
watchCounter().listen((value) {
// hear can be empty if you want.
});
You can put it in your init state to run, don't put it in your widget build, please.
This should work fine

Related

Multiple notifyListeners in flutter ChangeNotifier trigger only one build in widget

I have Flutter app with simple Provider and Consumer flow class SomeProvider with ChangeNotifier. My SubscriptionProvider has next methods:
methodA() async {
isLoading = true;
data = {};
notifyListeners();
await Future.delayed(Duration.zero, otherMethod);
methodB();
}
methodB() {
isLoading = false;
data = {
notifyListeners();
}
Widget Code:
Consumer<SomeProvider>(
builder: (ctx, someProvider, _) {
if (someProvider.isLoading) {
return WidgetC();
}
return WidgetD();
}
)
My problem is that when methodA is called build runs only once. On the other hand if I run await Future.delayed(Duration(seconds: 2), otherMethod); instead of await Future.delayed(Duration.zero, otherMethod); everything works fine and I have two builds. Are there any ways to perform build every time I call notifyListeners in provider.

What is the difference between "return <Stream>" and "yield* <Stream>" in flutter?

I noticed some strange behaviour of streams when working with Streams in Flutter.
Setup
An EventChannel provides a stream of events.
In my Cubit I listen on that stream and cancel the StreamSubscription in the close method of the Cubit.
The next screen also uses the same EventChannel to listen for events.
When I enter the second screen, the onCancel method in Android was called twice and therefore no events where passed through to the second Cubit.
The Scanner is a singleton, so both Cubits use the same instance.
Function for the Stream
class Scanner {
final eventChannel = EventChannel("events");
Stream<ScanEvent> getScanEvent() {
return _scanEvents.receiveBroadcastStream().map((event) => ScanEvent.fromJson(jsonDecode(event)));
}
}
Code in the Cubits
Scanner scanner = get<Scanner>();
Future<void> listenForScan() async {
_streamSubscription = _scanner.getScanEvent().listen((event) => submitSerialText(event.scanData));
}
#override
Future<void> close() {
_streamSubscription?.cancel();
return super.close();
}
Fix
When I use async* with yield* like so it works:
Fixed Function for the Stream
class Scanner {
final eventChannel = EventChannel("events");
Stream<ScanEvent> getScanEvent() async* {
yield* _scanEvents.receiveBroadcastStream().map((event) => ScanEvent.fromJson(jsonDecode(event)));
}
}
Question
Why is the stream in the first approach canceled twice?

Dart/Flutter Yield state after timer

I am using flutter_bloc and I am not sure how to yield state from within a callback.
Am trying to start timer and return a new state after a pause. Reason I am using a timer is to get the ability to cancel previous timer so it always returns a new state after an idle state.
#override
Stream<VerseState> mapEventToState(
VerseEvent event,
) async* {
if (event is ABCEvent) {
Timer(const Duration(seconds: 3), () {
print("Here");
_onTimerEnd(); // adding yield here returns an error.
})
}
Stream<XYZState> _onTimerEnd() async* {
print("Yielding a state");
yield myNewState();
}
I can see that the code is getting inside the timer callback as I can see the print statements in the callback but not in the timerEnd() method.
State should be yielded by the stream used in bloc that you are current working on. Like
mapEventToState

How to print the correct value from future instance in flutter

I am trying to fetch a field name "total" from mongodb and add all the total for a particular user as shown below
getCartTotal() async{
var total = 0;
Db db = new Db("mongodb://3.133.123.227/Kartofilldatabasetest");
await db.open();
var collection = db.collection('cart');
await collection.find({"customerId":phoneNumber}).forEach((v) {
total = total + v["total"];
});
db.close();
return total;
}
I am trying to use the returned value of total as follow
in the init function i am calling it everytime the page is opened.
#override
void initState() {
// TODO: implement initState
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await getTotal();
});
}
getTotal() async {
totalCartValue = await Mongocart(phoneNumber: widget.phoneNumber,description: '',image: '',price: '',quantity: '',total: '').getCartTotal();
}
#override
Widget build(BuildContext context) {
print(totalCartValue);
return Container();
}
}
Whenever i am trying to print the value it shows instance of future and not the value is printed.
This is what is getting printed Instance of 'Future'
Below is my database screenshot
I want fetch the field total of all the object and assign it in the totalcartvalue currently it is printing 0.
Since you are using async in the getTotal() function, the result should be using await, otherwise it would just build concurrently with the data is being retrieve, thus no data is displayed on build.
#override
void initState() {
// TODO: implement initState
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await getTotal();
});
}

Flutter BLoC mapEventToState gets called only the first time for an event and not called each next time that event is fired

I have Courses and Tasks. Each Course has many Tasks. That is why I am using different screens in the app to show a list of courses and after a tap on a course, I am navigating to the next screen - a list of tasks. Here is my onTap method of the list of courses:
onTap: () {
TasksPageLoadedEvent pageLoadedEvent =
TasksPageLoadedEvent(
courseId: state.courses[index].id,
truckNumber: this.truckNumber,
);
serviceLocator<TaskBloc>().add(pageLoadedEvent);
Routes.sailor(
Routes.taskScreen,
params: {
Routes.courseNumber:
state.courses[index].courseNumber,
Routes.truckNumber: this.truckNumber,
Routes.courseId: state.courses[index].id,
},
);
}
I create a TasksPageLoadedEvent, pass it to the TaskBloc and navigate to the Tasks page.
Here is the TaskBloc and how it handles the mapping Event - State:
#override
Stream<TaskState> mapEventToState(
TaskEvent event,
) async* {
if (event is TasksLoadingEvent) {
yield TasksLoadingState();
} else if (event is TasksReloadingErrorEvent) {
yield TasksErrorState();
} else if (event is TasksFetchedFailureEvent) {
yield TaskFetchedStateFailureState(error: event.failure);
} else if (event is TasksPulledFromServerEvent) {
yield TasksPulledFromServerState(
truckNumber: event.truckNumber,
courseNumber: event.courseNumber,
courseId: event.courseId,
);
} else if (event is TasksPageLoadedEvent) {
yield TasksLoadingState();
final networkInfoEither = await this.getNetworkInfoQuery(NoQueryParams());
yield* networkInfoEither.fold((failure) async* {
yield TasksErrorState();
}, (success) async* {
if (success) {
final getTasksEither = await getTasksQuery(
GetTasksParams(
truckNumber: event.truckNumber,
courseId: event.courseId,
),
);
yield* getTasksEither.fold((failure) async* {
yield TaskFetchedStateFailureState(error: "coursesDatabaseError");
}, (result) async* {
if (result != null) {
yield TasksFetchedState(tasks: result);
} else {
yield TaskFetchedStateFailureState(
error: "coursesFetchFromDatabaseError");
}
});
} else {
yield TasksNoInternetState();
}
});
}
}
When I get navigated to the Tasks page, the BlocBuilder checks the state and handles the building accordingly. I have a Go Back functionality that navigates back to the Courses page:
onPressed: () {
serviceLocator<CourseBloc>().add(
CoursesPageLoadedEvent(truckNumber: this.truckNumber),
);
Navigator.of(context).pop(true);
},
This fires the similar event for the previous page and it gets re-loaded.
The problem I am facing happens if I want to go to another course and see its tasks. If I tap on another item in the list and therefore fire a new TasksPageLoadedEvent (with new properties) the mapEventToState() doesn't get called at all.
I have had similar issues with BLoC before, but they were regarding the BlocListener and states extending Equatable. That is why I had my events NOT extending Equatable (although I am not sure whether this was the issue here). But still nothing happens.
Here are my Events:
abstract class TaskEvent {
const TaskEvent();
}
class TasksPageLoadedEvent extends TaskEvent {
final String truckNumber;
final int courseId;
TasksPageLoadedEvent({
this.truckNumber,
this.courseId,
});
}
class TasksFetchedFailureEvent extends TaskEvent {
final String failure;
TasksFetchedFailureEvent({
this.failure,
});
}
class TasksLoadingEvent extends TaskEvent {}
class TasksReloadingErrorEvent extends TaskEvent {}
class TasksPulledFromServerEvent extends TaskEvent {
final String courseNumber;
final String truckNumber;
final int courseId;
TasksPulledFromServerEvent({
#required this.courseNumber,
#required this.truckNumber,
#required this.courseId,
});
}
How should I handle my back-and-forth between the two pages using two BLoCs for each page?
OK, I found an answer myself!
The problem, of course, as Federick Jonathan implied - the instance of the bloc. I am using a singleton instance created by the flutter package get_it. Which is really useful if you are implementing dependency injection (for a clean architecture for example).
So the one instance was the problem.
Luckily the package has implemented the neat method resetLazySingleton<T>.
Calling it upon going back resets the bloc used in that widget. Therefore when I navigate again to the Tasks page I am working with the same but reset instance of that bloc.
Future<bool> _onWillPop() async {
serviceLocator.resetLazySingleton<TaskBloc>(
instance: serviceLocator<TaskBloc>(),
);
return true;
}
I hope this answer would help someone in trouble with singletons, dependency injections and going back and forth within a flutter app with bloc.
for anyone else who has similar issue:
in case you are listening to a repository stream and looping through emitted object, it cause mapEventToState gets blocked. because the loop never ends.
Stream<LoaderState<Failure, ViewModel>> mapEventToState(
LoaderEvent event) async* {
yield* event.when(load: () async* {
yield const LoaderState.loadInProgress();
await for (final Either<Failure, Entity> failureOrItems in repository.getAll()) {
yield failureOrItems.fold((l) => LoaderState.loadFailure(l),
(r) => LoaderState.loadSuccess(mapToViewModel(r)));
}
});
}
what you should do instead of await for the stream, listen to stream and then raise another event, and then process the event:
watchAllStarted: (e) async* {
yield const NoteWatcherState.loadInProgress();
_noteStreamSubscription = _noteRepository.watchAll().listen(
(failureOrNotes) =>
add(NoteWatcherEvent.notesReceived(failureOrNotes)));
},
notesReceived: (e) async* {
yield e.failureOrNotes.fold(
(failure) => NoteWatcherState.loadFailure(failure),
(right) => NoteWatcherState.loadSuccess(right));
},