flutter How to process bloc events in parallel? - flutter

Consider an app for counting colors.
A server provides a list of colors.
The user can click on a color in the app UI
The clicks per color are counted and each click is stored on the server.
I have build a BLoC to manage the "color-counters".
class ColorsBloc extends Bloc<ColorsEvent, ColorsState> {
final ColorRepository colorRepository;
ColorsBloc({required this.colorRepository}) : super(ColorsState.initial());
#override
Stream<ColorsState> mapEventToState(
ColorsEvent event,
) async* {
if (event is ColorsFetchRequested) {
yield ColorsState.loading();
try {
final colors = await colorRepository.getColors();
yield ColorsState.success(colors);
} catch (e) {
yield ColorsState.error();
}
} else if (event is ColorCounted) {
yield* _mapColorCountedToState(event);
}
}
Stream<ColorsState> _mapColorCountedToState(ColorCounted event) async* {
yield state.copyWith(
sendingByColorId: {...state.sendingByColorId, event.colorId},
);
await colorRepository.storeColor(Color(
colorId: event.colorId,
timestamp: DateTime.now().millisecondsSinceEpoch,
));
final colors = await colorRepository.getColors();
yield state.copyWith(
status: Status.success,
colors: colors,
sendingByColorId: {...state.sendingByColorId}..remove(event.colorId),
);
}
}
Sending a color-click takes some time (let's say 1 second on a slow network). The user may not click a color again before it is stored to the server (what the sendingByColorId set keeps track of).
PROBLEM
The user however may click on different colors very fast. The counters are working in that case, but they lag behind because events are processed FIFO (including the await colorRepository.storeColor(...) and the await to get the updated colors list).
I want the sending state to update immediately after any click even if there are previous clicks which are currently in the process of storing it to the repository.
How can I enable the BLoC to keep on processing new events while another one is awaiting the API response?

Notice the main idea of using Bloc is predictability - you will lose that predictability to some degree (depending on your concrete implementation). If you are using flutter_bloc you could follow this suggestion and override the default event stream processing on your bloc.
#override
Stream<Transition<MyEvent, MyState>> transformEvents(
Stream<MyEvent> events, transitionFn) {
return events.flatMap(transitionFn);
}
You could also look into isolates and maybe especially flutters compute which lets you spin up an isolate to run your code. I found this to be a good source.
While I'm very sure there is a better way to do this I came up with the following. I've cut out some of your logic for it to be a little more generic.
I'm not familiar with the performance details of compute and isolate in dart, so I want to make the disclaimer that this might not be a best practice approach, but maybe it helps you getting started.
import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:async';
void main() {
runApp(ExampleApp());
}
class ExampleApp extends StatelessWidget {
static ExampleBloc bloc = ExampleBloc();
#override
Widget build(BuildContext context) {
return MaterialApp(
home: TextButton(
onPressed: () => bloc.add(ExampleStartingEvent()),
child: Text("Trigger"),
),
);
}
}
// Top level function that is computed in isolate
Future<void> _mockRequest(String body) async {
// Do your async request here and await response
Future.delayed(Duration(seconds: 5));
ExampleBloc.successfulCompute("Successful!");
}
// Bloc
class ExampleBloc extends Bloc<ExampleEvent, ExampleState> {
ExampleBloc() : super(ExampleStateInitial());
static successfulCompute(String response) {
ExampleApp.bloc.add(ExampleEventSuccess(response));
}
#override
Stream<ExampleState> mapEventToState(
ExampleEvent event,
) async* {
if (event is ExampleEventSuccess) {
print(event.response);
yield ExampleStateSuccess(event.response);
}
if (event is ExampleStartingEvent) {
compute(_mockRequest, "body");
}
}
}
// Events
class ExampleEvent {}
class ExampleStartingEvent extends ExampleEvent {}
class ExampleEventSuccess extends ExampleEvent {
final response;
ExampleEventSuccess(this.response);
}
// States
class ExampleState {}
class ExampleStateInitial extends ExampleState {}
class ExampleStateSuccess extends ExampleState {
final response;
ExampleStateSuccess(this.response);
}
class ExampleStateError extends ExampleState {}

Just to show a solution based on #kohjakob 's proposal but with:
no static methods
complete error handling routines
The idea is basically to wrap the repository call into an async method (_sendClick(...)) and call it non-blocking (i.e. without await) while the status update on the sending state is done synchronously.
The _sendClick(...) waits for the repository and adds a ColorSendSuccess or ColorSendFailed event to the bloc once it's done. These events are then handle in their own run of the mapEventToState(...) routine.
class ColorsBloc extends Bloc<ColorsEvent, ColorsState> {
final ColorRepository colorRepository;
ColorsBloc({required this.colorRepository}) : super(ColorsState.initial());
#override
Stream<ColorsState> mapEventToState(
ColorsEvent event,
) async* {
if (event is ColorsFetchRequested) {
yield ColorsState.loading();
try {
final colors = await colorRepository.getColors();
yield ColorsState.success(colors);
} catch (e) {
yield ColorsState.error();
}
} else if (event is ColorCounted) {
yield* _mapColorCountedToState(event);
} else if (event is ColorSendSuccess) {
yield _mapColorSendSuccessToState(event);
} else if (event is ColorSendFailed) {
yield _mapColorSendFailedToState(event);
}
}
Stream<ColorsState> _mapColorCountedToState(ColorCounted event) async* {
yield state.copyWith(
sendingByColorId: {...state.sendingByColorId, event.colorId},
);
// non-blocking <----------------
_sendClick(Color(
colorId: event.colorId,
timestamp: DateTime.now().millisecondsSinceEpoch,
));
final colors = await colorRepository.getColors();
yield state.copyWith(
status: Status.success,
colors: colors,
sendingByColorId: {...state.sendingByColorId}..remove(event.colorId),
);
}
Future<void> _sendClick(Color color) async {
try {
int newId = await colorRepository.storeColor(color);
Color storedColor = color.copyWith(id: () => newId);
add(ColorSendSuccess(color: storedColor));
} on StoreColorClickException catch (_) {
add(ColorSendFailed(color: color));
}
}
ColorsState _mapColorSendSuccessToState(ColorCounted event) async* {
return state.copyWith(
colors: [...state.colors]
// replace the local color-click with the stored one
..removeWhere((element) => element.localId == event.color.localId)
..add(event.color.copyWith(localId: () => null)),
sendingByColorId: {...state.sendingByColorId}..remove(event.color.id),
);
}
ColorsState _mapColorSendFailedToState(ColorCounted event) async* {
return state.copyWith(
colors: [...state.colors]
// remove the color that has not been stored
..removeWhere((element) => element.localId == event.color.localId),
sendingByColorId: {...state.sendingByColorId}..remove(event.color.localId),
// mark the color as failed
errorByColorId: {...state.errorByColorId, event.color.localId},
);
}
}

Related

How to update state of widgets using bloc in flutter?

I have 2 widgets on a screen - A and B. There is an event
performed on widget B, on which the state of A and B should
get updated.
I have used different blocs and states for each of them and used the approach of Futures to get data from api while loading the screen.
On an event of widget B, how can I update state of both the widgets A and B without calling the api again as I can get the data from previous session and also from the response of event on widget B, it is possible to build the state on the UI itself.
Adding code:
class BlocA extends Bloc<BlocAEvent,BlocAState>
{
BlocA():super(InitialState()){
on<GetData>(_getBlocAData);
}
}
void _getBlocAData(GetData event, Emitter<BlocAState>emit)
async{
try {
List<User> getDataResponse = await
DataService().getWidgetAData(
event.userid);
emit(BlocALoadedState(BlocAData: getDataResponse));
}
catch(e){
rethrow;
}}
class InitialState extends BlocAState{}
class BlocALoadedState extends BlocAState{
final List<User> BlocAData;
BlocALoadedState({required this.BlocAData});
}
BlocB:
abstract class BlocBStates {}
class BlocBLoadedState extends BlocBStates{
List<User> BlocBdata;
BlocBLoadedState({required this.BlocBdata});
}
class BlocBAcceptedState extends BlocBStates{
User user;
BlocBAcceptedState({required this.user});
}
Now, BlocB has event which fetches data from a different
enter code heresource.
class BlocB extends Bloc<BlocBEvent,BlocBState>
{
BlocB():super(InitialState()){
on<GetData>(_getBlocBData);
on<BlocBevent>(_onClickofaButtoninWidgetB);
}
}
void _getBlocBData(GetData event,
Emitter<BlocBState>emit)
async{
try {
List<User> getDataResponse = await
DataService().getWidgetBData(
event.userid);
emit(BlocBLoadedState(BlocBData: getDataResponse));
}
catch(e){
rethrow;
}}
void _onClickofaButtoninWidgetB(BlocBevent event,
Emitter<BlocBStates>emit) {
User blocBEventResponse = await
DataService().acceptRequest(
event.senderId,event.receiverId)
// From this response, I want to add this entry to bloc A's
// state and remove from bloc B's state
}
use BlocListener
BlocProvider(
create: BlocA(),
child: BlocListener<BlocB, StateB>(
listener: (context, state) {
if (state is StateBLoading) {
context.read<BlocA>().add(EventALoading());
} else if (state is StateBLoaded) {
context.read<BlocA>().add(EventALoaded(state.someData));
}
},
child: WidgetA();
);
}

extending extended class or how to get code out of BLoC

since one cannot extend an extension of a class such as class MyBloc extends Bloc<MyEvent, MyState>, what is a cleanest way to get some functions out of my bloc file? Since the logic is a bit more complex, I would like to get some sub functions out like in
#override
Stream<MyState> mapEventToState(MyEvent event) async* {
yield* event.map(
loadRequested: (e) => _mapLoadRequestedToState(),
dataEntered: (e) => _mapDataEnteredToState(),
);
}
Stream<LogicChainState> _mapLoadRequestedToState() async* {
final dataRaw = loadData();
final dataProc = initData(dataRaw);
yield doSomeMore(dataProc);
}
I don't like the idea of using global functions. I could create a class
class MyBlocUtils {
MyData initData(MyData dataRaw) {
...
}
MyData doSomeMore(MyData dataProc) {
...
}
}
which still isn't as nice as using a function defined within the class MyBloc.
Any advice?
If all you want is to separate your methods into multiple files, but keep them in the same class, you could use extension methods.
my_bloc.dart
part 'my_bloc_utils.dart';
class MyBloc extends Bloc<MyEvent, MyState> {
Stream<LogicChainState> _mapLoadRequestedToState() async* {
final dataRaw = loadData();
final dataProc = initData(dataRaw);
yield doSomeMore(dataProc);
}
}
my_bloc_utils.dart
part of 'my_bloc.dart';
extension MyBlocUtils on MyBloc {
#override
Stream<MyState> mapEventToState(MyEvent event) async* {
yield* event.map(
loadRequested: (e) => _mapLoadRequestedToState(),
dataEntered: (e) => _mapDataEnteredToState(),
);
}
}
You can access the methods in just the same way as you keep everything in a single file:
import 'my_bloc.dart';
final myBloc = MyBloc();
final stream = myBloc.mapEventToState(MyEvent());

How do I cancel a StreamSubscription inside a Cubit?

I have a cubit that listens to a stream of messages, and emits a state which holds the messages.
In the screen, I am using a BlocProvider to access the cubit, and a BlocBuilder to display
the messages.
In instances like below, do I need to close the StreamSubscription created on listen()? Is there a clean way to do it?
class MessageCubit extends Cubit<MessageState> {
final GetMessagesUseCase getMessagesUseCase;
MessageCubit({this.getMessagesUseCase}) : super(MessageInitial());
Future<void> getMessages({String senderId, String recipientId}) async {
emit(MessageLoading());
try {
final messagesStreamData = getMessagesUseCase.call();
//This is where I listen to a stream
messagesStreamData.listen((messages) {
emit(MessageLoaded(messages: messages));
});
} on SocketException catch (_) {
emit(MessageFailure());
} catch (_) {
emit(MessageFailure());
}
}
}
You don't need to close the subscription, but you should as good practice to avoid potential memory leaks. Since it is so straightforward it's not any sacrifice.
Create a class variable of type StreamSubscription<your type>. Let's say it's named sub.
In getMessages before listen: await sub?.cancel()
Then sub = messagesStreamData.listen(...
Override the Cubit's close method and run the same command as in bullet 2.
Full code:
class MessageCubit extends Cubit<MessageState> {
final GetMessagesUseCase getMessagesUseCase;
// Added
StreamSubscription<YOUR_MESSAGES_TYPE> sub;
MessageCubit({this.getMessagesUseCase}) : super(MessageInitial());
Future<void> getMessages({String senderId, String recipientId}) async {
emit(MessageLoading());
try {
final messagesStreamData = getMessagesUseCase.call();
// Added
await sub?.cancel();
//This is where I listen to a stream
sub = messagesStreamData.listen((messages) {
emit(MessageLoaded(messages: messages));
});
} on SocketException catch (_) {
emit(MessageFailure());
} catch (_) {
emit(MessageFailure());
}
}
// Added
#override
Future<void> close() async {
await sub?.cancel();
return super.close();
}
}

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

Flutter Bloc . I can't get hold of a value to pass in the state I yield in mapEventToState()`

I'm just starting with Flutter and I'm still uncertain about the logic to structure event/state/BLoc/Repositoy classes in order to use this pattern correctly. I'm stuck at getting a location value out of the bloc back to the UI when yielding a state that has the value as the input.
Starting from the repository I have getLocation() method that get coordinates from Geolocator locationManager :
Future<LatLng> getLocation() async {
try {
locationManager
.getCurrentPosition(
desiredAccuracy: LocationAccuracy.bestForNavigation)
.timeout(Duration(seconds: 5));
} catch (error) {
print(
'getLocation(): error getting current location: ${error.toString()}');
}
}
then I have the GetLocation event
class GetLocation extends MapEvent {
final LatLng location;
const GetLocation(this.location);
#override
List<Object> get props => [location];
#override
String toString() => 'GetLocation { current location: $location}';
}
that gets sent to bloc at screen loading to present the map, and from a button to center the map on user position.
BlocProvider<MapBloc>(create: (context) {
return MapBloc(mapRepository: MapRepository())..add(GetLocation());
}),
then the MapLoaded state that holds the location:
class MapLoaded extends MapState {
final LatLng location;
const MapLoaded(this.location);
#override
List<Object> get props => [location];
#override
String toString() => 'MapLoaded {location: $location}';
}
this is the Widget that gets built on that state:
BlocBuilder<MapBloc, MapState>(
bloc: MapBloc(mapRepository: _mapRepository),
builder: (BuildContext context, MapState state) {
final LatLng location = (state as MapLoaded).location;
return Container
and finally the bloc where I can't find a way to pass the location as the input of MapState()in response to an event :
class MapBloc extends Bloc<MapEvent, MapState> {
final MapRepository _mapRepository;
StreamSubscription _locationStreamSubscription;
MapBloc(
{#required MapRepository mapRepository,
#required StreamSubscription streamSubscription})
: assert(mapRepository != null || streamSubscription != null),
_mapRepository = mapRepository,
_locationStreamSubscription = streamSubscription;
MapState get initialState => MapLoading();
#override
Stream<MapState> mapEventToState(MapEvent event) async* {
if (event is GetLocation) {
yield* _mapGetLocationToState();
}
if (event is GetTracking) {
yield* _mapGetTrackingToState();
}
if (event is LocationUpdated) {
// CANT GET LOCATION TO PASS IN MapLoaded()
yield MapLoaded();
}
}
Stream<MapState> _mapGetLocationToState() async* {
_mapRepository.getLocation();
(location) => add(LocationUpdated(location));
// CANT GET LOCATION TO PASS IN MapLoaded()
yield MapLoaded(l);
}
Stream<MapState> _mapGetTrackingToState() async* {
_mapRepository.setTracking();
}
}
Inside Stream<MapState> _mapGetLocationToState() I tried to send a add(LocationUpdated(location)) event and in Stream<MapState> mapEventToState to yield MapLoaded() but I can't find any way to pass in the location. I also tried to yield it directly in Stream<MapState> _mapGetLocationToState() but with the same result.
Can you spot what I'm doing wrong? Switching to reactive programming is not being that easy but I'm getting there.. This is my first attempt to this pattern and I haven't wrapped my head around all concepts completely so I surely thought some classes wrongly.
Many thanks for your time and help and sorry for the long question.
Cheers.
Your _mapGetLocationToState() doesn't have event as param.
#override
Stream<MapState> mapEventToState(MapEvent event) async* {
if (event is GetLocation) {
yield* _mapGetLocationToState(event);
}
if (event is GetTracking) {
yield* _mapGetTrackingToState();
}
if (event is LocationUpdated) {
// CANT GET LOCATION TO PASS IN MapLoaded()
yield MapLoaded();
}
}
Stream<MapState> _mapGetLocationToState(GetLocation event) async* {
// now you have event.location
_mapRepository.getLocation();
(location) => add(LocationUpdated(location));
// CANT GET LOCATION TO PASS IN MapLoaded(event.location)
yield MapLoaded(event.location);
}
Stream<MapState> _mapGetTrackingToState() async* {
_mapRepository.setTracking();
}
}
EDIT: BlocProvider task is to make an instance of the Bloc class you want to have. In this case MapBloc. As you can see, your MapBloc class has 2 dependencies, MapRepository and StreamSubscription. So when BlocProvider wants to make an instance of it, you need to provide those things it needs through the constructor. The same thing as GetLocation, you need to provide LatLng because it depends on it.
BlocProvider<MapBloc>(create: (context) {
return MapBloc(
mapRepository: MapRepository(),
streamSubscription: ...?
)..add(GetLocation(...?));
}),