Use Flutter Bloc one event resulted value in another event under the same bloc - flutter

Here i want to reuse ExpenseFetchList event result in ExpenseAdd event. I do not want to call API again. how i can do it under good practice.
ExpenseBloc() : super(ExpenseInitial()) {
on<ExpenseEvent>((ExpenseEvent event, Emitter<ExpenseState> emit) async {
if (event is ExpenseFetchList) {
emit(ExpenseFetching());
List<Expense> e = dummyExpenseList; //await event.useCase.getAll();
// TODO: remove Future.delayed
// ignore: always_specify_types
await Future.delayed(const Duration(seconds: 5), () {
emit(ExpenseFetched(list: e));
});
return;
}
if (event is ExpenseAdd) {
// Here i do not want to call API again
// concatinate above event fetched values with this and return simply.
return;
}
});
}

First of all, you need to add an optional variable for that API response in your parent state. For example its name is ExpenseState
abstract class ExpenseState {
ExpenseState({required this.expenseResponse});
final ExpenseResponseModel? expenseResponse;
}
After that, extend both of your states i.e ExpenseFetchList and ExpenseAdd will have access to the api response variable.
class ExpenseAdd extends ExpenseState { }
class ExpenseFetchList extends ExpenseState { }

Related

how can I get the other controller's variable inside one controller in flutter using getx

This is an issue related to the getx in flutter.
I have 2 controllers. ContractsController and NotificationController.
In ContractsController I have put the value into observer variable by calling the Api request.
What I want now is to get that variable's data in another controller - NotificationController.
How to get that value using getx functions?
ContractsController
class ContractsController extends GetxController {
ExpiringContractRepository _expiringContractRepository;
final expiringContracts = <ExpiringContract>[].obs; // This is the value what I want in another controller
ContractsController() {
_expiringContractRepository = new ExpiringContractRepository();
}
#override
Future<void> onInit() async {
await refreshContracts();
super.onInit();
}
Future refreshContracts({bool showMessage}) async {
await getExpiringContracts();
if (showMessage == true) {
Get.showSnackbar(Ui.SuccessSnackBar(message: "List of expiring contracts refreshed successfully".tr));
}
}
Future getExpiringContracts() async {
try {
expiringContracts.value = await _expiringContractRepository.getAll(); // put the value from the api
} catch (e) {
Get.showSnackbar(Ui.ErrorSnackBar(message: e.toString()));
}
}
}
The expiringContracts is updated successfully with data after the api request.
Now, I want to get that value in NotificationController
NotificationController
class NotificationsController extends GetxController {
final notifications = <Notification>[].obs;
ContractsController contractsController;
NotificationsController() {
}
#override
void onInit() async {
contractsController = Get.find<ContractsController>();
print(contractsController.expiringContracts); // This shows an empty list ?????
super.onInit();
}
}
Overview
A couple solutions come to mind:
pass the expiringContracts list as a constructor argument to NotificationsController if you only need this done once at instantiation, or
use a GetX worker to update NotificationsController every time expiringContracts is updated
The first solution isn't related to GetX, rather it's just async coordination between ContractsController and NotificationsController, so lets focus on the 2nd solution: GetX Workers.
Details
In NotificationsController, create a method that will receive expiringContracts.
Something like:
class NotificationsController extends GetxController {
void refreshContracts(List<ExpiringContract> contracts) {
// do something
}
}
Please note: none of this code is tested. I'm writing this purely in StackOverflow, so consider this pseudo-code.
In ContractsController we'll supply the above callback method as a constructor arg:
In ContractsController, something like:
class ContractsController {
final expiringContracts = <ExpiringContract>[].obs
final Function(List<ExpiringContract>) refreshContractsCallback;
ContractsController(this.refreshContractsCallback);
#override
void onInit() {
super.onInit();
refreshContracts(); // do your stuff after super.onInit
ever(expiringContracts, refreshContractsCallback);
// ↑ contracts → refreshContractsCallback(contracts)
// when expiringContracts updates, run callback with them
}
}
Here the GetX ever worker takes the observable as first argument, and a function as 2nd argument. That function must take an argument of type that matches the observed variable, i.e. List<ExpiringContract>, hence the Type of refreshContractsCallback was defined as Function(List<ExpiringContract>).
Now whenever the observable expiringContracts is updated in ContractsController, refreshContractsCallback(contracts) will be called, which supplies the list of expiring contracts to NotificationsController via refreshContracts.
Finally, when instantiating the two controllers inside the build() method of your route/page:
NotificationsController nx = Get.put(NotificationsController());
ContractsController cx = Get.put(ContractsController(nx.refreshContracts));
Timeline of Events
NotificationsController gets created as nx.
nx.onInit() runs, slow call of refreshContracts() starts
ContractsController gets created, with nx.refreshContracts callback
your page paints
nx has no contracts data at this point, so you'll prob. need a FutureBuilder or an Obx/ GetX + StatelessWidget that'll rebuild when data eventually arrives
when refreshContracts() finishes, ever worker runs, sending contracts to nx
nx.refreshContracts(contracts) is run, doing something with contracts
Notes
async/await was removed from nx.onInit
ever worker will run when refreshContract finishes
There were some powerful approaches in GetX. I solved this issue with Get.put and Get.find
Here is the code that I added.
ContractsController
class ContractsController extends GetxController {
ExpiringContractRepository _expiringContractRepository;
final expiringContracts = <ExpiringContract>[].obs; // This is the value what I want in another controller
ContractsController() {
_expiringContractRepository = new ExpiringContractRepository();
}
#override
Future<void> onInit() async {
await refreshContracts();
super.onInit();
}
Future refreshContracts({bool showMessage}) async {
await getExpiringContracts();
if (showMessage == true) {
Get.showSnackbar(Ui.SuccessSnackBar(message: "List of expiring contracts refreshed successfully".tr));
}
}
Future getExpiringContracts() async {
try {
expiringContracts.value = await _expiringContractRepository.getAll(); // put the value from the API
// ******************************** //
Get.put(ContractsController()); // Added here
} catch (e) {
Get.showSnackbar(Ui.ErrorSnackBar(message: e.toString()));
}
}
}
NotificationController
class NotificationsController extends GetxController {
final notifications = <Notification>[].obs;
ContractsController contractsController;
NotificationsController() {
}
#override
void onInit() async {
// ******************************** //
contractsController = Get.find<ContractsController>(); // Added here.
print(contractsController.expiringContracts); // This shows the updated value
super.onInit();
}
}
Finally, I have found that GetX is simple but powerful for state management in flutter.
Thanks.

flutter How to process bloc events in parallel?

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

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 share state for many blocs

let's say I want to check for internet connection every time I call Api, if there's no internet the call with throw exception like NoInternetException and then show a state screen to the user tells him to check their connection.
How can I achieve that without creating a new state for every bloc in flutter_bloc library?
You can do this in the bloc that manages your root pages like authentication_page and homepage.
Create a state for noConnectivity.
NoConnectivity extends AuthenticationState{
final String message;
const NoConnectivity({ this.message });
}
Now create an event for noConnectivity.
NoConnectivityEvent extends AuthenticationEvent{}
Finally, create a StreamSubscription in your AuthenticationBloc to continuously listen to connecitvityState change and if the state is connectivity.none we'll trigger the NoConnecitivity state.
class AuthenticationBloc
extends Bloc<AuthenticationEvent, AuthenticationState> {
StreamSubscription subscription;
#override
AuthenticationState get initialState => initialState();
#override
Stream<AuthenticationState> mapEventToState(
AuthenticationEvent event,
) async* {
// ... all other state map
else if(event is NoConnectivityEvent) {
yield* _mapNoConnectivityEventToState();
}
Stream<AuthenticationState> _mapNoConnectivityEventToState() async * {
subscription?.cancel();
//Edit to handle android internet connectivity.
subscription = Connectivity()
.onConnectivityChanged
.listen((ConnectivityResult result) {
if(Platform.isAndroid) {
try {
final lookupResult = InternetAddress.lookup('google.com');
if (lookupResult.isNotEmpty && lookupResult[0].rawAddress.isNotEmpty) {
print('connected');
}
} on SocketException catch (error) {
return add(NoConnectivityState(message: error.message ));
}
} else if(result == ConnectivityResult.none ) {
return add(NoConnectivityState(message: "Noconnection")) ;
}
print("Connected");
});
}
#override
Future<void> close() {
subscription?.cancel();
return super.close();
}
}
This subscription Stream will forever listen to a no connection and push the appropriate page you like to the state.
Required packages
rxdart
connectivity
Hope it helps!
you need base class bloc let's say his name "BaseBloc" and he shall inherit from the "Bloc" class, and implement "mapToStateEvent" method to process "noInternet" exception, and after that call method let's say his name "internalMapToStateEvent" you created, this method it's override method, and inherited all your bloc class from "BaseBloc" and you need same that for pages to draw one widget "noInternetWidget"

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